diff --git a/tests/e2e-mobile/fixtures.ts b/tests/e2e-mobile/fixtures.ts index 033e38e..d040e04 100644 --- a/tests/e2e-mobile/fixtures.ts +++ b/tests/e2e-mobile/fixtures.ts @@ -1,5 +1,6 @@ import type { Page } from "@playwright/test" import { test as base, expect as pwExpect } from "@playwright/test" +import { attachConsoleMonitor } from "../fixtures/console-monitor" export { expect, type Page, API_BASE, clickSpaLink } from "../e2e/fixtures" import { expect } from "../e2e/fixtures" @@ -11,6 +12,8 @@ export const test = base.extend({ * Override page fixture: BrowserSync domcontentloaded workaround. */ page: async ({ page }, use) => { + const monitor = attachConsoleMonitor(page) + const origGoto = page.goto.bind(page) const origReload = page.reload.bind(page) @@ -19,6 +22,7 @@ export const test = base.extend({ page.reload = ((opts?: any) => origReload({ waitUntil: "domcontentloaded", ...opts })) as typeof page.reload await use(page) + monitor.assertNoErrors() }, }) diff --git a/tests/e2e-visual/fixtures.ts b/tests/e2e-visual/fixtures.ts index 3c7fb1a..81a2c6f 100644 --- a/tests/e2e-visual/fixtures.ts +++ b/tests/e2e-visual/fixtures.ts @@ -1,5 +1,6 @@ import type { Page, Locator } from "@playwright/test" import { test as base, expect as pwExpect } from "@playwright/test" +import { attachConsoleMonitor } from "../fixtures/console-monitor" export { expect, type Page, API_BASE, clickSpaLink } from "../e2e/fixtures" import { expect } from "../e2e/fixtures" @@ -11,6 +12,8 @@ export const test = base.extend({ * Override page fixture: BrowserSync domcontentloaded workaround. */ page: async ({ page }, use) => { + const monitor = attachConsoleMonitor(page) + const origGoto = page.goto.bind(page) const origReload = page.reload.bind(page) @@ -19,6 +22,7 @@ export const test = base.extend({ page.reload = ((opts?: any) => origReload({ waitUntil: "domcontentloaded", ...opts })) as typeof page.reload await use(page) + monitor.assertNoErrors() }, }) diff --git a/tests/e2e/fixtures.ts b/tests/e2e/fixtures.ts index 213678f..3d229e0 100644 --- a/tests/e2e/fixtures.ts +++ b/tests/e2e/fixtures.ts @@ -1,5 +1,6 @@ import { test as base, expect, type Page } from "@playwright/test" import { ensureTestUser, type TestUserCredentials } from "../api/helpers/test-user" +import { attachConsoleMonitor } from "../fixtures/console-monitor" const API_BASE = "/api" @@ -29,6 +30,8 @@ export const test = base.extend({ * navigation methods to "domcontentloaded". */ page: async ({ page }, use) => { + const monitor = attachConsoleMonitor(page) + const origGoto = page.goto.bind(page) const origReload = page.reload.bind(page) const origGoBack = page.goBack.bind(page) @@ -42,6 +45,7 @@ export const test = base.extend({ origGoForward({ waitUntil: "domcontentloaded", ...opts })) as typeof page.goForward await use(page) + monitor.assertNoErrors() }, // Worker-scoped: create/reuse test user once per worker diff --git a/tests/fixtures/console-monitor.ts b/tests/fixtures/console-monitor.ts new file mode 100644 index 0000000..8a0b0c6 --- /dev/null +++ b/tests/fixtures/console-monitor.ts @@ -0,0 +1,117 @@ +/** + * Console/Network-Error-Monitor für Playwright-Tests. + * + * Überwacht Browser-Pages auf: + * - `pageerror` – Uncaught Exceptions + * - `console.error` – Alle console.error()-Aufrufe + * - `requestfailed` – Fehlgeschlagene Netzwerk-Requests (nicht absichtlich abgebrochene) + * + * Verwendung in Fixtures: + * ```ts + * const monitor = attachConsoleMonitor(page) + * // … setup … + * await use(page) + * monitor.assertNoErrors() + * ``` + * + * Fehler werden NICHT ignoriert, sondern in der App gefixt. + * IGNORED_PATTERNS nur für Infrastructure-Rauschen, das nicht in der App liegt. + */ + +import type { Page } from "@playwright/test" + +// ── Whitelist (nur Infrastructure-Rauschen, keine App-Fehler) ──────── + +const IGNORED_PATTERNS: RegExp[] = [ + /browser-sync/i, + /cc\.webmakers\.de/i, + /sentry/i, + /Failed to load resource: net::ERR_/i, + /Failed to load resource: the server responded with a status of/i, + /TypeError: Failed to fetch/i, +] + +// ── Types ──────────────────────────────────────────────────────────── + +interface ConsoleError { + type: "pageerror" | "console.error" | "request-failed" + message: string +} + +export interface ConsoleMonitor { + getErrors(): ConsoleError[] + assertNoErrors(): void +} + +// ── Implementation ─────────────────────────────────────────────────── + +function isIgnored(text: string): boolean { + return IGNORED_PATTERNS.some((pattern) => pattern.test(text)) +} + +export function attachConsoleMonitor(page: Page): ConsoleMonitor { + const errors: ConsoleError[] = [] + + // Inject script to capture detailed unhandled rejection info + page.addInitScript(() => { + window.addEventListener("unhandledrejection", (event) => { + const reason = event.reason + if (reason && typeof reason === "object" && !(reason instanceof Error)) { + try { + const detail = JSON.stringify( + reason, + (_key, val) => { + if (val instanceof Response) return `[Response ${val.status} ${val.url}]` + if (val instanceof Request) return `[Request ${val.method} ${val.url}]` + return val + }, + 2 + ) + console.error(`[unhandled-rejection-detail] ${detail}`) + } catch { + console.error(`[unhandled-rejection-detail] ${String(reason)}`) + } + } + }) + }) + + page.on("pageerror", (error) => { + const msg = error.stack || error.message || String(error) + if (!isIgnored(msg)) { + errors.push({ type: "pageerror", message: msg }) + } + }) + + page.on("console", (msg) => { + if (msg.type() === "error") { + const text = msg.text() + if (!isIgnored(text)) { + errors.push({ type: "console.error", message: text }) + } + } + }) + + page.on("requestfailed", (request) => { + const failure = request.failure()?.errorText || "unknown" + if (failure === "net::ERR_ABORTED" || failure === "net::ERR_FAILED") return + const url = request.url() + if (isIgnored(url)) return + errors.push({ + type: "request-failed", + message: `${request.method()} ${url} – ${failure}`, + }) + }) + + return { + getErrors: () => [...errors], + assertNoErrors() { + if (errors.length > 0) { + const summary = errors.map((e) => ` [${e.type}] ${e.message}`).join("\n") + throw new Error( + `Browser-Fehler während des Tests erkannt (${errors.length}):\n${summary}\n\n` + + `→ Fehler in der App fixen, NICHT in IGNORED_PATTERNS aufnehmen!` + ) + } + }, + } +}