feat: add console error monitoring for Playwright tests; enhance page fixture with error assertions

This commit is contained in:
2026-03-08 15:36:50 +00:00
parent a9a13a6b5b
commit 0be4852f74
4 changed files with 129 additions and 0 deletions

View File

@@ -1,5 +1,6 @@
import type { Page } from "@playwright/test" import type { Page } from "@playwright/test"
import { test as base, expect as pwExpect } 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" export { expect, type Page, API_BASE, clickSpaLink } from "../e2e/fixtures"
import { expect } from "../e2e/fixtures" import { expect } from "../e2e/fixtures"
@@ -11,6 +12,8 @@ export const test = base.extend<MobileFixtures>({
* Override page fixture: BrowserSync domcontentloaded workaround. * Override page fixture: BrowserSync domcontentloaded workaround.
*/ */
page: async ({ page }, use) => { page: async ({ page }, use) => {
const monitor = attachConsoleMonitor(page)
const origGoto = page.goto.bind(page) const origGoto = page.goto.bind(page)
const origReload = page.reload.bind(page) const origReload = page.reload.bind(page)
@@ -19,6 +22,7 @@ export const test = base.extend<MobileFixtures>({
page.reload = ((opts?: any) => origReload({ waitUntil: "domcontentloaded", ...opts })) as typeof page.reload page.reload = ((opts?: any) => origReload({ waitUntil: "domcontentloaded", ...opts })) as typeof page.reload
await use(page) await use(page)
monitor.assertNoErrors()
}, },
}) })

View File

@@ -1,5 +1,6 @@
import type { Page, Locator } from "@playwright/test" import type { Page, Locator } from "@playwright/test"
import { test as base, expect as pwExpect } 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" export { expect, type Page, API_BASE, clickSpaLink } from "../e2e/fixtures"
import { expect } from "../e2e/fixtures" import { expect } from "../e2e/fixtures"
@@ -11,6 +12,8 @@ export const test = base.extend<VisualFixtures>({
* Override page fixture: BrowserSync domcontentloaded workaround. * Override page fixture: BrowserSync domcontentloaded workaround.
*/ */
page: async ({ page }, use) => { page: async ({ page }, use) => {
const monitor = attachConsoleMonitor(page)
const origGoto = page.goto.bind(page) const origGoto = page.goto.bind(page)
const origReload = page.reload.bind(page) const origReload = page.reload.bind(page)
@@ -19,6 +22,7 @@ export const test = base.extend<VisualFixtures>({
page.reload = ((opts?: any) => origReload({ waitUntil: "domcontentloaded", ...opts })) as typeof page.reload page.reload = ((opts?: any) => origReload({ waitUntil: "domcontentloaded", ...opts })) as typeof page.reload
await use(page) await use(page)
monitor.assertNoErrors()
}, },
}) })

View File

@@ -1,5 +1,6 @@
import { test as base, expect, type Page } from "@playwright/test" import { test as base, expect, type Page } from "@playwright/test"
import { ensureTestUser, type TestUserCredentials } from "../api/helpers/test-user" import { ensureTestUser, type TestUserCredentials } from "../api/helpers/test-user"
import { attachConsoleMonitor } from "../fixtures/console-monitor"
const API_BASE = "/api" const API_BASE = "/api"
@@ -29,6 +30,8 @@ export const test = base.extend<E2eFixtures, E2eWorkerFixtures>({
* navigation methods to "domcontentloaded". * navigation methods to "domcontentloaded".
*/ */
page: async ({ page }, use) => { page: async ({ page }, use) => {
const monitor = attachConsoleMonitor(page)
const origGoto = page.goto.bind(page) const origGoto = page.goto.bind(page)
const origReload = page.reload.bind(page) const origReload = page.reload.bind(page)
const origGoBack = page.goBack.bind(page) const origGoBack = page.goBack.bind(page)
@@ -42,6 +45,7 @@ export const test = base.extend<E2eFixtures, E2eWorkerFixtures>({
origGoForward({ waitUntil: "domcontentloaded", ...opts })) as typeof page.goForward origGoForward({ waitUntil: "domcontentloaded", ...opts })) as typeof page.goForward
await use(page) await use(page)
monitor.assertNoErrors()
}, },
// Worker-scoped: create/reuse test user once per worker // Worker-scoped: create/reuse test user once per worker

117
tests/fixtures/console-monitor.ts vendored Normal file
View File

@@ -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!`
)
}
},
}
}