✨ feat: add console error monitoring for Playwright tests; enhance page fixture with error assertions
This commit is contained in:
@@ -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()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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
117
tests/fixtures/console-monitor.ts
vendored
Normal 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!`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user