The last nine: admin-taxonomy, admin-save-failures, admin-theme, admin-inline-category, admin-item-preview, admin-disable-customer, admin-reserved-items, admin-inventory-filters, email-templates and admin-email-settings. Every spec in tests/e2e now goes through page objects. Measured rather than asserted: - raw CSS and antd-internal locators in spec files: 0 (was 13) - local `register` helpers: 0 (was 9) - hardcoded http://localhost:5173: 0 (was 3) The antd knowledge that was spread across seven spec files is now in four page objects, each with the reason written next to it. Three of those are things no reader could have guessed from the locator: Segmented hides its real radio behind a styled label, so the input is found by role and cannot be clicked — the title attribute is the handle. The status multi-select renders an invisible role="listbox" shim beside the real list, so getByRole('option') finds something zero-sized; and a selected status renders again as a tag carrying the same title, so an unscoped getByTitle is ambiguous. Matching the visible option class avoids both. The dropdown is also opened only when closed, because antd keeps it open after a selection in multiple mode. Select popups render into a portal at the end of <body>, outside the tab panel they belong to, so a dropdown cannot be found by scoping to the panel. AdminPage.tab gained an `exact` flag for one specific collision: the Emails tab contains a rail that also renders tabs, and "Email verification" contains "Email". Without exact matching, opening the Emails tab is ambiguous with the template inside it. Two helpers stayed local rather than moving into page objects, because they belong to their file's subject rather than to a surface: favorites-filter's `favorite()`, which settles the opt-in modal and waits for its fade before the wrapper stops intercepting pointer events, and admin-theme's `luminance()`, which is a WCAG calculation and not a locator. Both now take page objects as parameters instead of reaching for locators themselves. Verified: 26/26 spec files converted, tsc clean over the whole tree, lint at the 30-warning src baseline with nothing added. Full suite 123 passed / 5 failed; all five pass in a 33/33 serial re-run, which is the load-related flakiness this suite has had throughout and not a change here — the backend hashes passwords with bcryptjs, a pure-JS implementation that blocks the event loop for every request while it runs. Closes #137
112 lines
3.8 KiB
TypeScript
112 lines
3.8 KiB
TypeScript
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;
|
|
readonly themeToggle: Locator;
|
|
readonly themedBody: Locator;
|
|
/**
|
|
* antd renders Select popups into a portal at the end of <body>, outside the
|
|
* panel they belong to — so a dropdown cannot be found by scoping to the tab.
|
|
*/
|
|
readonly selectDropdown: 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();
|
|
this.themeToggle = page.getByRole('switch');
|
|
this.themedBody = page.locator('body');
|
|
this.selectDropdown = page.locator('.ant-select-dropdown').first();
|
|
}
|
|
|
|
async goto(): Promise<void> {
|
|
await this.page.goto('/admin');
|
|
}
|
|
|
|
/**
|
|
* A top-level admin tab.
|
|
*
|
|
* `exact` matters on the Emails tab: the template rail inside it also renders
|
|
* tabs, and "Email verification" contains "Email". Exact matching keeps the
|
|
* shell's tabs and the rail's apart.
|
|
*/
|
|
tab(name: AdminTab | RegExp, exact = false): Locator {
|
|
return typeof name === 'string'
|
|
? this.page.getByRole('tab', { name, exact })
|
|
: 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<void> {
|
|
await this.goto();
|
|
await this.openTab(name);
|
|
}
|
|
|
|
/** The tree or table inside whichever panel is showing. */
|
|
get activeTree(): Locator {
|
|
return this.activePanel.locator('.ant-tree').first();
|
|
}
|
|
|
|
get activeTable(): Locator {
|
|
return this.activePanel.locator('.ant-table').first();
|
|
}
|
|
|
|
/**
|
|
* Puts the admin in dark mode, whatever it was in before.
|
|
*
|
|
* Idempotent rather than a toggle: the theme persists across reloads, so a
|
|
* blind click leaves the state depending on what the previous test chose.
|
|
*/
|
|
async switchToDark(): Promise<void> {
|
|
await this.goto();
|
|
if ((await this.themeToggle.getAttribute('aria-checked')) !== 'true') {
|
|
await this.themeToggle.click();
|
|
}
|
|
await expect(this.themedBody).toHaveAttribute('data-theme', 'dark');
|
|
}
|
|
|
|
/** The computed background of an element, for the contrast assertions. */
|
|
static async backgroundOf(locator: Locator): Promise<string> {
|
|
return locator.evaluate((el) => getComputedStyle(el).backgroundColor);
|
|
}
|
|
|
|
static async colorOf(locator: Locator): Promise<string> {
|
|
return locator.evaluate((el) => getComputedStyle(el).color);
|
|
}
|
|
|
|
/** Switches tabs without renavigating, for a test that visits two of them. */
|
|
async openTab(name: AdminTab): Promise<void> {
|
|
await this.tab(name, true).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();
|
|
}
|
|
}
|