/** * Collects console errors and uncaught exceptions from a Playwright page. * Filters out expected noise so tests can assert "no real errors". */ import type { Page, ConsoleMessage } from "@playwright/test"; /** Patterns to ignore — expected noise from EncryptID, HMR, analytics, SW */ const IGNORE_PATTERNS = [ /encryptid/i, /session/i, /service.?worker/i, /umami/i, /HMR/i, /hot.?update/i, /favicon/i, /\[vite\]/i, /net::ERR_/, // network errors in background fetches /ResizeObserver loop/i, // benign browser warning /Failed to register.*SW/i, // service worker blocked by config /attachShadow/i, // benign: duplicate shadow root from HMR/re-registration /already hosts a shadow tree/i, /Failed to resolve module specifier/i, // known importmap gaps (e.g. three-forcegraph) /folk-graph-viewer.*Failed to load/i, // rNetwork 3d-force-graph load failure (known) /Failed to load 3d-force-graph/i, ]; function isNoise(text: string): boolean { return IGNORE_PATTERNS.some((re) => re.test(text)); } export class ConsoleCollector { readonly errors: string[] = []; readonly pageErrors: string[] = []; constructor(page: Page) { page.on("console", (msg: ConsoleMessage) => { if (msg.type() === "error") { const text = msg.text(); if (!isNoise(text)) this.errors.push(text); } }); page.on("pageerror", (err) => { const text = err.message || String(err); if (!isNoise(text)) this.pageErrors.push(text); }); } /** Returns all collected real errors (console.error + uncaught exceptions). */ get allErrors(): string[] { return [...this.errors, ...this.pageErrors]; } /** Asserts no real errors were logged. */ assertNoErrors() { if (this.allErrors.length > 0) { throw new Error( `Unexpected console errors:\n${this.allErrors.map((e) => ` - ${e}`).join("\n")}` ); } } }