import { expect, test as base } from '@playwright/test'; import { Account } from './accounts'; import { Evidence } from './evidence'; import { ApiCall, captureApiCall } from './http-capture'; import { BookingDetailPage, BookingsPage } from '../pages/bookings.page'; import { CheckoutPage } from '../pages/checkout.page'; import { DashboardPage } from '../pages/dashboard.page'; import { LoginPage } from '../pages/login.page'; import { SearchPage } from '../pages/search.page'; import { ShellPage } from '../pages/shell.page'; interface Fixtures { login: LoginPage; shell: ShellPage; search: SearchPage; checkout: CheckoutPage; bookings: BookingsPage; bookingDetail: BookingDetailPage; dashboard: DashboardPage; /** Signs in and lands on the dashboard. */ signInAs: (account: Account) => Promise; /** Records screenshots and observed data for the run report. */ evidence: Evidence; /** Console and uncaught errors seen during the test; asserted empty at the end. */ consoleErrors: string[]; /** Records every /api/** request and response for the run report. */ apiTraffic: void; } export const test = base.extend({ login: async ({ page }, use) => use(new LoginPage(page)), shell: async ({ page }, use) => use(new ShellPage(page)), search: async ({ page }, use) => use(new SearchPage(page)), checkout: async ({ page }, use) => use(new CheckoutPage(page)), bookings: async ({ page }, use) => use(new BookingsPage(page)), bookingDetail: async ({ page }, use) => use(new BookingDetailPage(page)), dashboard: async ({ page }, use) => use(new DashboardPage(page)), signInAs: async ({ login }, use) => { await use(async (account: Account) => { await login.signIn(account); }); }, evidence: async ({ page }, use, testInfo) => { await use(new Evidence(page, testInfo)); }, // Every test also asserts the browser console stayed quiet. A stack trace in // the console is a real defect even when the assertions happen to pass. consoleErrors: [ async ({ page }, use) => { const errors: string[] = []; page.on('console', (message) => { if (message.type() !== 'error') { return; } // The browser logs this for any non-2xx response, including the 401s // and 404s these tests deliberately provoke. It says nothing about // whether the application handled them, so it is not a defect signal. if (message.text().startsWith('Failed to load resource:')) { return; } errors.push(`console.error: ${message.text()}`); }); page.on('pageerror', (error) => { errors.push(`uncaught: ${error.message}`); }); await use(errors); expect(errors, 'the browser console should be clean').toEqual([]); }, { auto: true }, ], // Records the request/response of every API call the test made, attached as // evidence-http for the reporter. Auto, so no test has to opt in. apiTraffic: [ async ({ page }, use, testInfo) => { const pending: Array> = []; page.on('response', (response) => { pending.push(captureApiCall(response)); }); await use(); const calls = (await Promise.all(pending)).filter((c): c is ApiCall => c !== null); if (calls.length > 0) { await testInfo.attach('evidence-http', { body: Buffer.from(JSON.stringify(calls, null, 2), 'utf8'), contentType: 'application/json', }); } }, { auto: true }, ], }); export { expect } from '@playwright/test'; /** * A surname unique to this test run, so a booking can be found again in a list * that other tests and the seed data also write to. */ export function uniqueSurname(prefix = 'E2E'): string { return `${prefix}${Date.now().toString(36).toUpperCase()}`; }