import { expect, type Locator, type Page, type Request, type Response } from "@playwright/test"; const CRITICAL_RESOURCE_TYPES = new Set(["document", "stylesheet", "script", "image", "font"]); const ABORTED_REQUEST_PATTERNS = ["ERR_ABORTED", "NS_BINDING_ABORTED"]; const BENIGN_PAGE_ERRORS = ["ResizeObserver loop limit exceeded"]; function isSameOrigin(url: string, baseOrigin: string) { try { return new URL(url).origin === baseOrigin; } catch { return false; } } function isCriticalResource(url: string, resourceType: string) { if (CRITICAL_RESOURCE_TYPES.has(resourceType)) { return true; } try { const pathname = new URL(url).pathname; return /manifest(\.webmanifest|\.json)$/i.test(pathname); } catch { return false; } } export function attachPageHealthGuards(page: Page, baseURL: string) { const baseOrigin = new URL(baseURL).origin; const pageErrors: string[] = []; const failedResources: string[] = []; const onPageError = (error: Error) => { if (BENIGN_PAGE_ERRORS.some((pattern) => error.message.includes(pattern))) { return; } pageErrors.push(error.message); }; const onRequestFailed = (request: Request) => { const errorText = request.failure()?.errorText || "Request failed"; if (ABORTED_REQUEST_PATTERNS.some((pattern) => errorText.includes(pattern))) { return; } if (!isSameOrigin(request.url(), baseOrigin)) { return; } const resourceType = request.resourceType(); if (!isCriticalResource(request.url(), resourceType)) { return; } failedResources.push(`${resourceType} ${request.url()} (${errorText})`); }; const onResponse = (response: Response) => { if (response.status() < 400) { return; } const request = response.request(); if (!isSameOrigin(response.url(), baseOrigin)) { return; } const resourceType = request.resourceType(); if (!isCriticalResource(response.url(), resourceType)) { return; } failedResources.push(`${response.status()} ${resourceType} ${response.url()}`); }; page.on("pageerror", onPageError); page.on("requestfailed", onRequestFailed); page.on("response", onResponse); return { async expectHealthy() { expect(pageErrors, "Unexpected uncaught browser errors were recorded.").toEqual([]); expect(failedResources, "Critical same-origin resources failed to load.").toEqual([]); }, dispose() { page.off("pageerror", onPageError); page.off("requestfailed", onRequestFailed); page.off("response", onResponse); }, }; } export async function expectBodyHasContent(page: Page, minimumLength = 20) { await expect .poll( async () => { const text = await page .locator("body") .innerText() .catch(() => ""); return text.replace(/\s+/g, " ").trim().length; }, { timeout: 15_000 } ) .toBeGreaterThan(minimumLength); } export async function settlePage(page: Page) { await page.waitForLoadState("domcontentloaded"); await page.waitForLoadState("networkidle", { timeout: 10_000 }).catch(() => {}); } export async function expectOneVisible(locators: Locator[], timeout = 15_000) { await expect .poll( async () => { for (const locator of locators) { if (await locator.isVisible().catch(() => false)) { return true; } } return false; }, { timeout } ) .toBe(true); } export function requiredEnv(name: string) { const value = process.env[name]; if (!value) { throw new Error(`${name} is required for the live smoke gate.`); } return value; }