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 { AdminTaxonomy } from './pages/AdminTaxonomy'; import { AdminCustomers } from './pages/AdminCustomers'; import { AdminEmails } from './pages/AdminEmails'; import { AdminSettings } from './pages/AdminSettings'; 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'; export { AdminTaxonomy } from './pages/AdminTaxonomy'; export { AdminCustomers } from './pages/AdminCustomers'; export { AdminEmails } from './pages/AdminEmails'; export { AdminSettings } from './pages/AdminSettings'; 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; adminTaxonomy: AdminTaxonomy; adminCustomers: AdminCustomers; adminEmails: AdminEmails; adminSettings: AdminSettings; } 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({ 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)); }, adminTaxonomy: async ({ page }, use) => { await use(new AdminTaxonomy(page)); }, adminCustomers: async ({ page }, use) => { await use(new AdminCustomers(page)); }, adminEmails: async ({ page }, use) => { await use(new AdminEmails(page)); }, adminSettings: async ({ page }, use) => { await use(new AdminSettings(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 } ] });