Files
redefined-designs/frontend/tests/e2e/fixtures.ts
T
bermudalamb 549a08038e test(e2e): convert the favorites, availability and orders specs (#137)
favorites, favorites-filter, sold-filter, orders and pending-publish. Five more copies of "register a customer" and three more of the hardcoded base URL go with them.

Two locators that were hiding real knowledge are now named. `gridCell` is the item's whole antd column rather than its card, needed because the SOLD ribbon renders outside the card — three specs reached for `.ant-col` directly to get at it. And `chooseAvailability` goes through the title attribute because antd's Segmented hides the real radio behind a styled label, so the input is found by role and cannot be clicked; that fact was written out twice in comments and is now written once in code.

The favorite control is located page-wide rather than within a card. The storefront paginates as items accumulate and the control is named for its item anyway, so scoping to a card bought nothing and broke whenever the card was on another page.

favorites-filter keeps its local `favorite()` helper. It is genuinely local — decline the opt-in, wait for the fading modal to stop intercepting pointer events, confirm the heart flipped — and belongs to that file's subject rather than to the storefront. It now takes page objects as parameters instead of reaching for locators itself, which is what a spec-level helper should look like.

Two specs still drive the registration form rather than taking the `customer` fixture, and deliberately. Both are about a signed-out visitor being interrupted mid-action — favoriting an item, or switching on the favorites filter — and the claim is that the thing they asked for survives the interruption. Replacing the interruption with an API call would delete the test.

Verified: favorites 6/6, favorites-filter 7/7, sold-filter 6/6, orders and pending-publish 8/8. Notably favorites-filter's "keeps showing a favorite after it sells" passes, which had been failing on a strict-mode violation from two items sharing a name across runs.

Refs #137
2026-08-23 19:43:45 -05:00

180 lines
6.3 KiB
TypeScript

import { test as base, APIRequestContext, expect } from '@playwright/test';
import { mkdirSync, writeFileSync } from 'fs';
import { randomUUID } from 'crypto';
import path from 'path';
import { Header } from './pages/Header';
import { AuthModal } from './pages/AuthModal';
import { AccountModal } from './pages/AccountModal';
import { StorefrontPage } from './pages/StorefrontPage';
import { AdminPage } from './pages/AdminPage';
import { PasswordResetPages } from './pages/PasswordResetPages';
import { FilterDrawer } from './pages/FilterDrawer';
import { FavoritePrompt } from './pages/FavoritePrompt';
import { OrdersPage } from './pages/OrdersPage';
import { AdminInventory } from './pages/AdminInventory';
import { uniqueEmail } from './support/api';
// Re-exported so specs can import everything from here — expect, Page,
// APIRequestContext — and get the coverage-collecting `test` at the same time.
// The explicit `test` below wins over the star export.
export * from '@playwright/test';
export * from './support/api';
export { Header } from './pages/Header';
export { AuthModal } from './pages/AuthModal';
export { AccountModal } from './pages/AccountModal';
export { StorefrontPage } from './pages/StorefrontPage';
export { AdminPage } from './pages/AdminPage';
export { PasswordResetPages } from './pages/PasswordResetPages';
export { FilterDrawer } from './pages/FilterDrawer';
export { FavoritePrompt } from './pages/FavoritePrompt';
export { OrdersPage } from './pages/OrdersPage';
export { AdminInventory } from './pages/AdminInventory';
const NYC_OUTPUT = path.resolve(__dirname, '..', '..', '.nyc_output');
const collectingCoverage = process.env.COVERAGE === 'true';
// Set once any page reports instrumentation. Checked by the collection script so
// an empty run fails loudly instead of publishing 0% — see the note below.
const MARKER = path.join(NYC_OUTPUT, '.collected');
/** The password every seeded customer gets. Long enough to pass the 8-char rule. */
export const PASSWORD = 'supersecret123';
export interface RegisteredCustomer {
email: string;
password: string;
firstName: string;
lastName: string;
}
interface Pages {
header: Header;
authModal: AuthModal;
accountModal: AccountModal;
storefront: StorefrontPage;
admin: AdminPage;
passwordReset: PasswordResetPages;
filterDrawer: FilterDrawer;
favoritePrompt: FavoritePrompt;
orders: OrdersPage;
adminInventory: AdminInventory;
}
interface Data {
/**
* A request context for the admin API, on the configured baseURL.
*
* Specs used to build their own with `playwright.request.newContext(...)`, one
* of them against a hardcoded `http://localhost:5173`, which meant changing
* the port in the config moved every test except that one.
*/
adminApi: APIRequestContext;
/**
* A freshly registered customer, already signed in on `page`.
*
* Registering through the API rather than the form, deliberately. Nine specs
* drove the registration form purely to arrive at a signed-in session, so a
* broken form failed a hundred tests that were not about it — and each paid
* for a bcrypt round-trip through the UI. The specs that are about
* registration use `authModal` and drive it properly.
*
* `page.request` shares the browser context's cookie jar, so the session
* cookie the endpoint sets belongs to `page`. The caller still has to
* navigate: the session exists, but a page loaded before it does not know.
*/
customer: RegisteredCustomer;
}
/**
* Flushes istanbul's per-page counters after each test.
*
* Coverage lives in `window.__coverage__` on the page and dies with it, so it
* has to be read before the page closes — one file per test, because the suite
* runs fullyParallel and workers would otherwise overwrite each other.
*/
export const test = base.extend<Pages & Data & { collectCoverage: void }>({
header: async ({ page }, use) => {
await use(new Header(page));
},
authModal: async ({ page }, use) => {
await use(new AuthModal(page));
},
accountModal: async ({ page }, use) => {
await use(new AccountModal(page));
},
storefront: async ({ page }, use) => {
await use(new StorefrontPage(page));
},
admin: async ({ page }, use) => {
await use(new AdminPage(page));
},
passwordReset: async ({ page }, use) => {
await use(new PasswordResetPages(page));
},
filterDrawer: async ({ page }, use) => {
await use(new FilterDrawer(page));
},
favoritePrompt: async ({ page }, use) => {
await use(new FavoritePrompt(page));
},
orders: async ({ page }, use) => {
await use(new OrdersPage(page));
},
adminInventory: async ({ page }, use) => {
await use(new AdminInventory(page));
},
adminApi: async ({ playwright, baseURL }, use) => {
const context = await playwright.request.newContext({ baseURL });
await use(context);
await context.dispose();
},
customer: async ({ page }, use) => {
const details: RegisteredCustomer = {
email: uniqueEmail(),
password: PASSWORD,
firstName: 'Test',
lastName: 'Customer'
};
const res = await page.request.post('/api/customers/register', {
data: {
email: details.email,
password: details.password,
firstName: details.firstName,
lastName: details.lastName,
marketingConsent: false
}
});
// Loudly, and naming the address: a failure here is a fixture failing to
// build, and every assertion after it would fail for a reason that has
// nothing to do with what the test is about.
expect(res.ok(), `registering ${details.email}`).toBeTruthy();
await use(details);
},
collectCoverage: [
async ({ page }, use) => {
await use();
if (!collectingCoverage) return;
// The page may already be closed by a test that navigated away or crashed;
// a missing sample is not worth failing a passing test over. The run-level
// check catches the case that actually matters — no samples at all.
const coverage = await page
.evaluate(() => (window as unknown as { __coverage__?: unknown }).__coverage__)
.catch(() => undefined);
if (!coverage) return;
mkdirSync(NYC_OUTPUT, { recursive: true });
writeFileSync(path.join(NYC_OUTPUT, `${randomUUID()}.json`), JSON.stringify(coverage));
writeFileSync(MARKER, 'ok');
},
{ auto: true }
]
});