import path from 'node:path' import { test as base, expect, type ConsoleMessage, type Response, type WebError, } from '@playwright/test' type FailureKind = 'console.error' | 'pageerror' | 'same-origin 5xx' | 'weberror' type BrowserFailure = { kind: FailureKind detail: string } type E2EFailureGuardFixtures = { e2eFailureGuard: void /** * Same-origin paths whose failed responses this spec provokes on purpose. * * A `fetch` the app handles in code still makes the browser log "Failed to * load resource", so a spec asserting a refusal (B40: signing in with a * revoked passkey) would otherwise fail the guard for doing its job. Declaring * the path — rather than allowing 4xx broadly — keeps an unhandled request * failure in any other spec a failure. It is an option so a spec declares it * once with `test.use`, instead of pushing at exactly the right moment * mid-test. * * A `RegExp` is accepted for a door whose path carries an id (C226: the still * of a piece is `/conteudos//frame` and answers 404 until the self-heal * lands, so any spec rendering a video card would trip the guard). A literal * is still the default, and a pattern still has to name one surface. */ expectedRequestFailurePaths: (string | RegExp)[] } const loadFailurePrefix = 'Failed to load resource' const missingFaviconConsoleError = 'Failed to load resource: the server responded with a status of 404 (Not Found)' const errorDetail = (error: Error): string => error.stack ?? error.message const responseDetail = (response: Response): string => `${response.status()} ${response.request().method()} ${response.url()}` const consoleDetail = (message: ConsoleMessage): string => { const { url, lineNumber, columnNumber } = message.location() const location = url ? ` (${url}:${lineNumber}:${columnNumber})` : '' return `${message.text()}${location}` } const isAllowedConsoleError = ( message: ConsoleMessage, origin: string | null, expectedRequestFailurePaths: (string | RegExp)[], ): boolean => { const { url } = message.location() if (!url || origin === null) return false const location = new URL(url) if (location.origin !== origin) return false if (message.text().startsWith(loadFailurePrefix)) { // The declared paths come FIRST: every 404 carries the exact same browser // message as the missing favicon, so a favicon-shaped early return would // swallow a refusal the spec declared on purpose. const declared = expectedRequestFailurePaths.some((expected) => expected instanceof RegExp ? expected.test(location.pathname) : expected === location.pathname, ) if (declared) return true } // The app intentionally has no /favicon.ico asset yet. if (message.text() === missingFaviconConsoleError) return location.pathname === '/favicon.ico' return false } export const test = base.extend({ expectedRequestFailurePaths: [[], { option: true }], e2eFailureGuard: [ async ({ baseURL, context, expectedRequestFailurePaths, page }, use) => { const failures: BrowserFailure[] = [] const origin = baseURL ? new URL(baseURL).origin : null const onConsole = (message: ConsoleMessage) => { if ( message.type() !== 'error' || isAllowedConsoleError(message, origin, expectedRequestFailurePaths) ) { return } failures.push({ kind: 'console.error', detail: consoleDetail(message) }) } const onPageError = (error: Error) => { failures.push({ kind: 'pageerror', detail: errorDetail(error) }) } const onResponse = (response: Response) => { if ( response.status() >= 500 && origin !== null && new URL(response.url()).origin === origin ) { failures.push({ kind: 'same-origin 5xx', detail: responseDetail(response) }) } } const onWebError = (webError: WebError) => { failures.push({ kind: 'weberror', detail: errorDetail(webError.error()) }) } page.on('console', onConsole) page.on('pageerror', onPageError) page.on('response', onResponse) context.on('weberror', onWebError) // S8 — the home story-video embed is a YouTube iframe; e2e must stay // hermetic (CI network must never gate a home-page test on the real // player loading — same spirit as the S2/S3 feed stubs). await context.route('https://www.youtube-nocookie.com/**', (route) => route.abort()) // S10 — the campaign-home pixel spec publishes a Meta Pixel ID into the // shared test DB, so concurrent specs may render the home with the base // code. Fulfill the external script locally (never a real request to // Meta): a blocked network would trip the console-error guard below. await context.route('https://connect.facebook.net/**', (route) => route.fulfill({ body: '', contentType: 'application/javascript' }), ) // S25 — the home radio is an own `