test(e2e): convert the storefront and filter specs onto page objects (#137)
storefront, storefront-errors, theme, error-boundary and filters. filters.spec.ts carried the last hand-rolled copies of createCategory, createTag and createItem, and the hardcoded `http://localhost:5173` that meant changing the port in the config would have moved every test except this one. Seeding now goes through support/api, and the host it needs lives in one constant. It has to be a constant rather than the config's baseURL: beforeAll runs with worker-scoped fixtures only and cannot read a test-scoped option, which is why the URL was inlined there in the first place. New FilterDrawer object. The two rules it encodes are the ones the tests exist to pin down and neither is guessable from a locator: categories are a tree because the filter matches a node and everything filed beneath it, and tags combine with AND rather than OR, so selecting two means "must have both". The active-filter chips go on StorefrontPage rather than the drawer, because that is where they render — and the scoping matters, since the drawer carries a "Clear all" of its own that an unscoped locator also matches. StorefrontPage gains the three things the catalogue says instead of listing items. They are named together deliberately: the distinction between "No items yet" and "Couldn't load items" is the point, and several tests assert one is showing while the other is not, because telling a customer the shop is empty when the server is broken hides the outage. The theme switch and the attribute it writes are both on Header now. The switch is in the header and `data-theme` lands on <body>, so a spec previously had to know about `body` to observe the control it had just clicked. Verified: 12/12 across the four small specs, 8/8 on filters, tsc clean, lint unchanged at the 30-warning src baseline. Refs #137
This commit is contained in:
@@ -2,7 +2,7 @@ import { Locator, Page, expect } from '@playwright/test';
|
||||
import { Header } from './Header';
|
||||
|
||||
/**
|
||||
* The public catalogue.
|
||||
* The public catalogue, and the states it shows instead of one.
|
||||
*
|
||||
* The item card locator is the one place in the suite that knows the storefront
|
||||
* renders items into `.item-card` — three specs reached for that class directly,
|
||||
@@ -13,14 +13,39 @@ import { Header } from './Header';
|
||||
export class StorefrontPage {
|
||||
readonly header: Header;
|
||||
readonly filtersButton: Locator;
|
||||
readonly privacyPolicyLink: Locator;
|
||||
/**
|
||||
* The chip row summarising what is filtered, which lives on the storefront
|
||||
* rather than in the drawer. Scoping matters: the drawer carries a "Clear
|
||||
* all" of its own, so an unscoped one matches both.
|
||||
*/
|
||||
readonly activeFilters: Locator;
|
||||
|
||||
/**
|
||||
* The three things the catalogue can say instead of listing items. Named
|
||||
* together because the distinction between them is the point: telling a
|
||||
* customer "No items yet" while the server is broken reads as an empty shop
|
||||
* and hides the outage, so several tests assert one is showing and another
|
||||
* is not.
|
||||
*/
|
||||
readonly emptyNotice: Locator;
|
||||
readonly loadFailureNotice: Locator;
|
||||
readonly retryButton: Locator;
|
||||
readonly loadFailureHeading: Locator;
|
||||
|
||||
/** What the nearest error boundary renders when the grid itself throws. */
|
||||
readonly catalogueBoundaryHeading: Locator;
|
||||
|
||||
constructor(private readonly page: Page) {
|
||||
this.header = new Header(page);
|
||||
this.filtersButton = page.getByRole('button', { name: /Filters/ });
|
||||
this.privacyPolicyLink = page.getByRole('link', { name: 'Privacy Policy' });
|
||||
this.activeFilters = page.getByRole('group', { name: 'Active filters' });
|
||||
|
||||
this.emptyNotice = page.getByText('No items yet');
|
||||
this.loadFailureNotice = page.getByText("Couldn't load items");
|
||||
this.retryButton = page.getByRole('button', { name: 'Retry' });
|
||||
this.loadFailureHeading = page.getByRole('heading', { name: "The item list didn't load" });
|
||||
|
||||
this.catalogueBoundaryHeading = page.getByRole('heading', { name: "The item list didn't load" });
|
||||
}
|
||||
|
||||
async goto(): Promise<void> {
|
||||
@@ -59,6 +84,14 @@ export class StorefrontPage {
|
||||
await this.filtersButton.click();
|
||||
}
|
||||
|
||||
removeFilterChip(name: string): Locator {
|
||||
return this.page.getByRole('button', { name: `Remove filter ${name}` });
|
||||
}
|
||||
|
||||
async clearAllFilters(): Promise<void> {
|
||||
await this.activeFilters.getByRole('button', { name: 'Clear all' }).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until the catalogue has rendered something.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user