mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9cd461cc08 | ||
|
|
9c8be25ff0 |
@@ -1,4 +1,4 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { test, expect } from "@app/tests/helpers/test-base";
|
||||
import { ensureCookieConsent } from "@app/tests/helpers/login";
|
||||
import { bypassOnboarding } from "@app/tests/helpers/api-stubs";
|
||||
import { openSettings } from "@app/tests/helpers/ui-helpers";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { test, expect } from "@app/tests/helpers/test-base";
|
||||
import { ensureCookieConsent } from "@app/tests/helpers/login";
|
||||
import { bypassOnboarding } from "@app/tests/helpers/api-stubs";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { test, expect } from "@app/tests/helpers/test-base";
|
||||
import { ensureCookieConsent } from "@app/tests/helpers/login";
|
||||
import { bypassOnboarding } from "@app/tests/helpers/api-stubs";
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { test as base, expect } from "@playwright/test";
|
||||
import { test as base, expect } from "@app/tests/helpers/test-base";
|
||||
import {
|
||||
bypassOnboarding,
|
||||
mockAppApis,
|
||||
seedCookieConsent,
|
||||
skipOnboarding,
|
||||
type MockAppApiOptions,
|
||||
} from "@app/tests/helpers/api-stubs";
|
||||
@@ -57,7 +56,8 @@ export const test = base.extend<StubFixtures>({
|
||||
seedJwt: [false, { option: true }],
|
||||
|
||||
page: async ({ page, stubOptions, autoGoto, seedJwt }, use) => {
|
||||
await seedCookieConsent(page);
|
||||
// `page` comes from test-base, which has already seeded cookie consent
|
||||
// and attached the console-error recorder before any navigation runs.
|
||||
if (seedJwt) {
|
||||
// Logged-in users hit the orchestrator path that surfaces the
|
||||
// analytics opt-in / MFA prompts — use the stronger bypass-all flag
|
||||
|
||||
@@ -1,14 +1,216 @@
|
||||
import { test as base, expect } from "@playwright/test";
|
||||
import { test as base, expect, type Page } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Custom test fixture that auto-dismisses the cookie consent banner
|
||||
* before every test. The banner (#cc-main) overlays the page and
|
||||
* intercepts pointer events, causing click timeouts across all tests.
|
||||
* Console message types that should fail the test if they appear.
|
||||
* `console.error()` -> type "error", `console.warn()` -> type "warning".
|
||||
*/
|
||||
const FAILING_CONSOLE_TYPES = new Set(["error", "warning"]);
|
||||
|
||||
/**
|
||||
* Patterns ignored globally on every page. Keep this list small and
|
||||
* well-justified — each entry suppresses a genuine console warning for
|
||||
* every test, which means we lose detection of regressions in that
|
||||
* surface. Only add things that:
|
||||
*
|
||||
* - fire on first page render of *every* test (so per-test
|
||||
* suppression would just be ceremony), AND
|
||||
* - are environmental noise (third-party CDN, dev-server quirk,
|
||||
* known init-order quirk) rather than something a test could
|
||||
* reasonably assert.
|
||||
*
|
||||
* Anything that fires only on specific flows belongs in an inline
|
||||
* `expectConsoleError` / `suppressConsoleErrors` at the call site.
|
||||
*/
|
||||
const GLOBAL_IGNORE_PATTERNS: RegExp[] = [
|
||||
// Stripe.js logs an HTTP warning when loaded over localhost. Third-party,
|
||||
// expected in dev, no production impact.
|
||||
/You may test your Stripe\.js integration over HTTP/,
|
||||
// i18next's HTTP backend fails to load namespace files under Vite dev's
|
||||
// `@fs/` URLs; the app falls back to embedded English copy and tests
|
||||
// still pass functional assertions.
|
||||
/i18next::backendConnector: loading namespace/,
|
||||
// scarfTracking.firePixel() is invoked from a router effect on the first
|
||||
// route render, before the useScarfTracking hook has called
|
||||
// setScarfConfig(). Harmless (the pixel is a no-op on first call) but
|
||||
// worth a follow-up to reorder init. See utils/scarfTracking.ts.
|
||||
/\[scarfTracking\] firePixel\(\) called before setScarfConfig/,
|
||||
|
||||
// ── Vite dev-server flakiness under parallel-worker load ────────────────
|
||||
// The next block suppresses the entire cascade that follows when Vite's
|
||||
// dev server briefly stops accepting connections (because several workers
|
||||
// hit it simultaneously). In CI we serve a pre-built dist via
|
||||
// `vite preview`, where none of this happens; locally the cascade is just
|
||||
// environmental noise. None of these patterns mask production-only bugs.
|
||||
|
||||
// 1) Browser-level network failure for an unreachable URL.
|
||||
/Failed to load resource: net::ERR_/,
|
||||
// 2) Vite's lazy chunk loader sees the network failure and throws.
|
||||
/Failed to fetch dynamically imported module/,
|
||||
// 3) PDF.js / pdfium WASM streaming fetch trips on the same outage.
|
||||
/WebAssembly compilation aborted: Network error/,
|
||||
/wasm streaming compile failed/,
|
||||
/failed to asynchronously prepare wasm/,
|
||||
/falling back to ArrayBuffer instantiation/,
|
||||
// 4) React-dom logs its own wrapper line when the lazy chunk error reaches
|
||||
// a Suspense / ErrorBoundary. Suppress only this exact wrapper — real
|
||||
// React errors that aren't chunk-load failures still surface elsewhere.
|
||||
/The above error occurred in one of your React components/,
|
||||
// 5) Our ErrorBoundary's componentDidCatch dumps ~15 supplementary
|
||||
// diagnostic lines. They are useful in prod but in tests they are
|
||||
// pure noise on top of whatever already failed. Match by source URL.
|
||||
/\(https?:\/\/[^)]*\/src\/core\/components\/shared\/ErrorBoundary\.tsx:/,
|
||||
];
|
||||
|
||||
/**
|
||||
* Per-page collector for console errors / warnings / uncaught page errors.
|
||||
*
|
||||
* The fixture installs one of these on every page. Messages that aren't
|
||||
* absorbed by an active `expectConsoleError` / `suppressConsoleErrors`
|
||||
* scope are reported in fixture teardown and fail the test.
|
||||
*/
|
||||
class ConsoleErrorRecorder {
|
||||
private readonly failed: string[] = [];
|
||||
private readonly scopes: Array<{ pattern: RegExp; matched: boolean }> = [];
|
||||
|
||||
record(text: string): void {
|
||||
for (const pattern of GLOBAL_IGNORE_PATTERNS) {
|
||||
if (pattern.test(text)) return; // documented global noise
|
||||
}
|
||||
for (const scope of this.scopes) {
|
||||
if (scope.pattern.test(text)) {
|
||||
scope.matched = true;
|
||||
return; // absorbed by an active scope, not a failure
|
||||
}
|
||||
}
|
||||
this.failed.push(text);
|
||||
}
|
||||
|
||||
async withScope<T>(
|
||||
pattern: RegExp,
|
||||
fn: () => Promise<T>,
|
||||
requireMatch: boolean,
|
||||
): Promise<T> {
|
||||
const scope = { pattern, matched: false };
|
||||
this.scopes.push(scope);
|
||||
try {
|
||||
const result = await fn();
|
||||
if (requireMatch && !scope.matched) {
|
||||
throw new Error(
|
||||
`expectConsoleError: no console error/warning matched ${pattern} ` +
|
||||
`during the scoped action`,
|
||||
);
|
||||
}
|
||||
return result;
|
||||
} finally {
|
||||
const idx = this.scopes.indexOf(scope);
|
||||
if (idx >= 0) this.scopes.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
|
||||
failures(): string[] {
|
||||
return this.failed;
|
||||
}
|
||||
}
|
||||
|
||||
const recordersByPage = new WeakMap<Page, ConsoleErrorRecorder>();
|
||||
|
||||
function attachConsoleErrorRecorder(page: Page): ConsoleErrorRecorder {
|
||||
const recorder = new ConsoleErrorRecorder();
|
||||
recordersByPage.set(page, recorder);
|
||||
|
||||
page.on("console", (msg) => {
|
||||
const type = msg.type();
|
||||
if (!FAILING_CONSOLE_TYPES.has(type)) return;
|
||||
const { url, lineNumber, columnNumber } = msg.location();
|
||||
const where = url ? ` (${url}:${lineNumber}:${columnNumber})` : "";
|
||||
recorder.record(`[console.${type}] ${msg.text()}${where}`);
|
||||
});
|
||||
|
||||
page.on("pageerror", (err) => {
|
||||
recorder.record(`[pageerror] ${err.message}`);
|
||||
});
|
||||
|
||||
return recorder;
|
||||
}
|
||||
|
||||
function getRecorder(page: Page, caller: string): ConsoleErrorRecorder {
|
||||
const recorder = recordersByPage.get(page);
|
||||
if (!recorder) {
|
||||
throw new Error(
|
||||
`${caller} requires the \`test\` exported from ` +
|
||||
`\`@app/tests/helpers/test-base\` (or stub-test-base). ` +
|
||||
`Are you importing \`test\` directly from "@playwright/test"?`,
|
||||
);
|
||||
}
|
||||
return recorder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run `fn` and *require* at least one console error / warning / page error
|
||||
* that matches `pattern` to occur during it. Matching messages are
|
||||
* absorbed (they don't fail the test); if none match, this throws.
|
||||
*
|
||||
* Use when a test deliberately exercises an error path and the error
|
||||
* surfaces in the console:
|
||||
*
|
||||
* await expectConsoleError(page, /Validation failed/, async () => {
|
||||
* await page.getByRole("button", { name: "Submit" }).click();
|
||||
* await expect(page.getByRole("alert")).toBeVisible();
|
||||
* });
|
||||
*
|
||||
* The scope only covers messages emitted while `fn` is awaiting, so
|
||||
* remember to `await` any UI assertion that the error has surfaced
|
||||
* *inside* the callback rather than after it returns.
|
||||
*/
|
||||
export async function expectConsoleError<T>(
|
||||
page: Page,
|
||||
pattern: RegExp,
|
||||
fn: () => Promise<T>,
|
||||
): Promise<T> {
|
||||
return getRecorder(page, "expectConsoleError").withScope(pattern, fn, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run `fn` and silently absorb any console errors / warnings / page
|
||||
* errors matching `pattern`, without asserting that one occurred. Use
|
||||
* sparingly — `expectConsoleError` is preferred because it also verifies
|
||||
* the error path actually fires.
|
||||
*
|
||||
* await suppressConsoleErrors(page, /MUI Grid v1 deprecated/, async () => {
|
||||
* await page.getByRole("button", { name: "Open settings" }).click();
|
||||
* });
|
||||
*/
|
||||
export async function suppressConsoleErrors<T>(
|
||||
page: Page,
|
||||
pattern: RegExp,
|
||||
fn: () => Promise<T>,
|
||||
): Promise<T> {
|
||||
return getRecorder(page, "suppressConsoleErrors").withScope(
|
||||
pattern,
|
||||
fn,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom test fixture shared across all Playwright suites. Two things
|
||||
* happen for every test that uses this base (directly or transitively
|
||||
* via `stub-test-base.ts`):
|
||||
*
|
||||
* 1. The cookie-consent cookie is seeded before any navigation so the
|
||||
* `#cc-main` banner never renders and never intercepts clicks.
|
||||
* 2. Console errors/warnings and uncaught page errors are captured.
|
||||
* If any unhandled message appears during the test, the fixture
|
||||
* throws during teardown and the test fails. Tests that legitimately
|
||||
* produce errors should wrap the offending step in
|
||||
* `expectConsoleError(page, /pattern/, async () => { ... })`.
|
||||
*
|
||||
* Usage: import { test, expect } from '@app/tests/helpers/test-base';
|
||||
*/
|
||||
export const test = base.extend({
|
||||
page: async ({ page }, use) => {
|
||||
const recorder = attachConsoleErrorRecorder(page);
|
||||
|
||||
// Set the cookie consent cookie before any navigation so the banner
|
||||
// never appears. The cookieconsent library (orestbida/cookieconsent)
|
||||
// reads this cookie on init and skips the banner if consent exists.
|
||||
@@ -29,6 +231,16 @@ export const test = base.extend({
|
||||
]);
|
||||
|
||||
await use(page);
|
||||
|
||||
const failures = recorder.failures();
|
||||
if (failures.length > 0) {
|
||||
throw new Error(
|
||||
`Test produced ${failures.length} unhandled console error(s)/warning(s):\n` +
|
||||
failures.map((m) => ` ${m}`).join("\n") +
|
||||
`\n\nIf any of these are expected, wrap the action in ` +
|
||||
`expectConsoleError(page, /pattern/, async () => { ... }).`,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { test, expect, type Page } from "@playwright/test";
|
||||
import { test, expect } from "@app/tests/helpers/test-base";
|
||||
import type { Page } from "@playwright/test";
|
||||
import {
|
||||
bypassOnboarding,
|
||||
mockAppApis,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { test, expect, type Page } from "@playwright/test";
|
||||
import { test, expect } from "@app/tests/helpers/test-base";
|
||||
import type { Page } from "@playwright/test";
|
||||
import path from "path";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
* The Vite dev server must be running (handled by playwright.config.ts webServer).
|
||||
*/
|
||||
|
||||
import { test, expect, type Page } from "@playwright/test";
|
||||
import { test, expect } from "@app/tests/helpers/test-base";
|
||||
import type { Page } from "@playwright/test";
|
||||
import path from "path";
|
||||
import { mockAppApis } from "@app/tests/helpers/api-stubs";
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
* The Vite dev server must be running (handled by playwright.config.ts webServer).
|
||||
*/
|
||||
|
||||
import { test, expect, type Page } from "@playwright/test";
|
||||
import { test, expect } from "@app/tests/helpers/test-base";
|
||||
import type { Page } from "@playwright/test";
|
||||
import path from "path";
|
||||
import { mockAppApis } from "@app/tests/helpers/api-stubs";
|
||||
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
* unlock-all-wrong-password - all transitively covered or low-value.
|
||||
*/
|
||||
|
||||
import { test, expect, type Page } from "@playwright/test";
|
||||
import { test, expect } from "@app/tests/helpers/test-base";
|
||||
import type { Page } from "@playwright/test";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import { mockAppApis } from "@app/tests/helpers/api-stubs";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { test, expect, type Page } from "@playwright/test";
|
||||
import { test, expect } from "@app/tests/helpers/test-base";
|
||||
import type { Page } from "@playwright/test";
|
||||
import { mockAppApis, seedCookieConsent } from "@app/tests/helpers/api-stubs";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { test, expect, type Page } from "@playwright/test";
|
||||
import { test, expect } from "@app/tests/helpers/test-base";
|
||||
import type { Page } from "@playwright/test";
|
||||
import {
|
||||
bypassOnboarding,
|
||||
mockAppApis,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { test, expect, type Page } from "@playwright/test";
|
||||
import { test, expect } from "@app/tests/helpers/test-base";
|
||||
import type { Page } from "@playwright/test";
|
||||
import {
|
||||
bypassOnboarding,
|
||||
mockAppApis,
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
* STIRLING_FLAVOR=saas ./gradlew :stirling-pdf:bootRun --args="--server.port=18083 --spring.profiles.include=dev"
|
||||
* STIRLING_SAAS_URL=http://localhost:18083 npx playwright test --project=stubbed saas-backend-smoke
|
||||
*/
|
||||
import { test, expect, request } from "@playwright/test";
|
||||
import { test, expect } from "@app/tests/helpers/test-base";
|
||||
import { request } from "@playwright/test";
|
||||
|
||||
const SAAS_URL = process.env.STIRLING_SAAS_URL ?? "http://localhost:18083";
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { test, expect, type Page } from "@playwright/test";
|
||||
import { test, expect } from "@app/tests/helpers/test-base";
|
||||
import type { Page } from "@playwright/test";
|
||||
import {
|
||||
bypassOnboarding,
|
||||
mockAppApis,
|
||||
|
||||
Reference in New Issue
Block a user