A complete loop for defining, generating, running and gating tests: - qa/: scenario-card template + examples (API/UI/binding), the Claude generation prompt, and a QA runbook tying it together - e2e/CLAUDE.md: conventions that keep AI-generated tests consistent - run-tests.ps1: one runner for every layer (backend/api/web/ui/binding), logs per run under .testlogs/, JUnit output, evidence report, and a re-seed guard against stale demo data - .githooks/ + scripts/install-hooks.ps1: pre-commit secret scan and pre-push unit tests (shift-left before CI) - evidence report now captures each API call's request, payload and response (Authorization redacted); JUnit reporter added for CI - scripts/db.mjs: db:check freshness guard Azure DevOps pipeline deferred; the runner already emits JUnit + the evidence report for any CI to publish. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
115 lines
3.8 KiB
TypeScript
115 lines
3.8 KiB
TypeScript
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<void>;
|
|
|
|
/** 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<Fixtures>({
|
|
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<Promise<ApiCall | null>> = [];
|
|
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()}`;
|
|
}
|