import { Locator, Page, expect } from '@playwright/test'; /** The admin tabs, in the order the shell renders them. */ export type AdminTab = | 'Inventory' | 'Categories' | 'Tags' | 'Customers' | 'Emails' | 'Settings'; /** * The admin shell: the tab strip and the panel it swaps. * * Only the active tab's panel is mounted, which is what keeps the locators * inside each panel unambiguous — the email editors used to be stacked and a * locator for "Save" matched all six. * * `activePanel` exists because several specs reached for * `.ant-tabs-tabpane-active .ant-table` and similar to scope themselves to the * visible panel. That knowledge belongs here rather than in five spec files. */ export class AdminPage { readonly tabList: Locator; readonly activePanel: Locator; readonly activeTabLabel: Locator; constructor(private readonly page: Page) { this.tabList = page.getByRole('tablist'); this.activePanel = page.locator('.ant-tabs-tabpane-active').first(); this.activeTabLabel = page.locator('.ant-tabs-tab-active .ant-tabs-tab-btn').first(); } async goto(): Promise { await this.page.goto('/admin'); } tab(name: AdminTab | RegExp): Locator { return this.page.getByRole('tab', { name }); } /** * Opens a tab from wherever the browser is, navigating to /admin first. * * Takes the navigation rather than assuming it, because every caller in the * suite did both and half of them wrote the goto themselves. */ async open(name: AdminTab): Promise { await this.goto(); await this.openTab(name); } /** Switches tabs without renavigating, for a test that visits two of them. */ async openTab(name: AdminTab): Promise { await this.tab(name).click(); // The panel being mounted is the completion of the click. Without this a // caller's first locator resolves against the outgoing panel. await expect(this.activePanel).toBeVisible(); } }