frigate/web/e2e/fixtures/error-collector.ts
Josh Hawkins d113be5e19
Some checks are pending
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
Improve frontend test framework (#22824)
* add error allowlist file for error collector

* add error collector for console + page + request errors

* wire error collector into frigateApp fixture

* add self-tests for error collector fixture

* gate strict error mode on E2E_STRICT_ERRORS=1

* triage pre-existing errors and seed allowlist

* add mockEmpty/mockError/mockDelay helpers for state-driven tests

* add self-tests for mock override helpers

* add mobile affordance helpers to BasePage

* add lint script for banned spec patterns and @mobile rule

* apply prettier fixes to new e2e files

* rewrite export.spec.ts

* clean up

* move export spec rewrite and bugfix to separate branch
2026-04-09 14:42:36 -06:00

123 lines
3.6 KiB
TypeScript

/**
* Collects console errors, page errors, and failed network requests
* during a Playwright test, with regex-based allowlist filtering.
*
* Usage:
* const collector = installErrorCollector(page, [...GLOBAL_ALLOWLIST]);
* // ... run test ...
* collector.assertClean(); // throws if any non-allowlisted error
*
* The collector is wired into the `frigateApp` fixture so every test
* gets it for free. Tests that intentionally trigger an error pass
* additional regexes via the `expectedErrors` fixture parameter.
*/
import type { Page, Request, Response, ConsoleMessage } from "@playwright/test";
export type CollectedError = {
kind: "console" | "pageerror" | "request";
message: string;
url?: string;
stack?: string;
};
export type ErrorCollector = {
errors: CollectedError[];
assertClean(): void;
};
function isAllowlisted(message: string, allowlist: RegExp[]): boolean {
return allowlist.some((pattern) => pattern.test(message));
}
function firstStackFrame(stack: string | undefined): string | undefined {
if (!stack) return undefined;
const lines = stack
.split("\n")
.map((l) => l.trim())
.filter(Boolean);
// Skip the error message line (line 0); return the first "at ..." frame
return lines.find((l) => l.startsWith("at "));
}
function isSameOrigin(url: string, baseURL: string | undefined): boolean {
if (!baseURL) return true;
try {
return new URL(url).origin === new URL(baseURL).origin;
} catch {
return false;
}
}
export function installErrorCollector(
page: Page,
allowlist: RegExp[],
): ErrorCollector {
const errors: CollectedError[] = [];
const baseURL = (
page.context() as unknown as { _options?: { baseURL?: string } }
)._options?.baseURL;
const onConsole = (msg: ConsoleMessage) => {
if (msg.type() !== "error") return;
const text = msg.text();
if (isAllowlisted(text, allowlist)) return;
errors.push({
kind: "console",
message: text,
url: msg.location().url,
});
};
const onPageError = (err: Error) => {
const text = err.message;
if (isAllowlisted(text, allowlist)) return;
errors.push({
kind: "pageerror",
message: text,
stack: firstStackFrame(err.stack),
});
};
const onResponse = (response: Response) => {
const status = response.status();
if (status < 500) return;
const url = response.url();
if (!isSameOrigin(url, baseURL)) return;
const text = `${status} ${response.statusText()} ${url}`;
if (isAllowlisted(text, allowlist)) return;
errors.push({ kind: "request", message: text, url });
};
const onRequestFailed = (request: Request) => {
const url = request.url();
if (!isSameOrigin(url, baseURL)) return;
const failure = request.failure();
const text = `failed: ${failure?.errorText ?? "unknown"} ${url}`;
if (isAllowlisted(text, allowlist)) return;
errors.push({ kind: "request", message: text, url });
};
page.on("console", onConsole);
page.on("pageerror", onPageError);
page.on("response", onResponse);
page.on("requestfailed", onRequestFailed);
return {
errors,
assertClean() {
if (errors.length === 0) return;
const formatted = errors
.map((e, i) => {
const stack = e.stack ? `\n ${e.stack}` : "";
const url = e.url && e.url !== e.message ? ` (${e.url})` : "";
return ` ${i + 1}. [${e.kind}] ${e.message}${url}${stack}`;
})
.join("\n");
throw new Error(
`Page emitted ${errors.length} unexpected error${errors.length === 1 ? "" : "s"}:\n${formatted}`,
);
},
};
}