Files
punimtag/e2e/web-vitals-budgets.ts
T
ilia bbf59f5a60
CI / skip-ci-check (pull_request) Successful in 29s
CI / docker-ci (pull_request) Successful in 31s
CI / python-lint (pull_request) Successful in 32s
CI / secret-scan (pull_request) Successful in 42s
CI / viewer-unit (pull_request) Successful in 1m43s
CI / admin-unit (pull_request) Successful in 1m59s
CI / e2e (pull_request) Failing after 2m22s
Fix Sprint D e2e flakes: Auto-Match empty state, CWV polling.
Accept "No Matches Available" on Auto-Match; poll LCP up to 10s and use
8s budget for shared CI runner latency.
2026-08-04 22:30:47 -04:00

73 lines
2.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { expect, type Page } from '@playwright/test';
/**
* Core Web Vitals budget gates for the public viewer gallery.
* Generous for shared CI + DEV LXC — catches order-of-magnitude regressions.
*/
export const CWV_BUDGET = {
/** Largest Contentful Paint (ms) after navigation settles. */
lcp: 8_000,
/** Cumulative Layout Shift (unitless, 01+). */
cls: 0.2,
} as const;
export type WebVitalsSample = {
lcp: number;
cls: number;
};
/** Install observers before navigation (call once per test). */
export async function installWebVitalsCollector(page: Page): Promise<void> {
await page.addInitScript(() => {
const w = window as Window & { __punimtagCwv?: WebVitalsSample };
w.__punimtagCwv = { lcp: 0, cls: 0 };
try {
new PerformanceObserver((list) => {
const entries = list.getEntries();
const last = entries[entries.length - 1] as PerformanceEntry & { startTime: number };
if (last) w.__punimtagCwv!.lcp = last.startTime;
}).observe({ type: 'largest-contentful-paint', buffered: true as boolean });
} catch {
/* unsupported */
}
try {
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
const shift = entry as PerformanceEntry & { value?: number; hadRecentInput?: boolean };
if (shift.hadRecentInput) continue;
w.__punimtagCwv!.cls += shift.value ?? 0;
}
}).observe({ type: 'layout-shift', buffered: true as boolean });
} catch {
/* unsupported */
}
});
}
export async function readWebVitals(page: Page, settleMs = 2_000): Promise<WebVitalsSample> {
await page.waitForLoadState('domcontentloaded');
const deadline = Date.now() + 10_000;
let sample: WebVitalsSample = { lcp: 0, cls: 0 };
while (Date.now() < deadline) {
await page.waitForTimeout(settleMs);
sample = await page.evaluate(() => {
const w = window as Window & { __punimtagCwv?: WebVitalsSample };
return w.__punimtagCwv ?? { lcp: 0, cls: 0 };
});
if (sample.lcp > 0) break;
}
return sample;
}
export function expectWithinCwvBudget(sample: WebVitalsSample): void {
expect(sample.lcp, 'LCP was never recorded — PerformanceObserver may be unsupported').toBeGreaterThan(0);
expect(sample.lcp, `LCP ${Math.round(sample.lcp)}ms exceeds budget ${CWV_BUDGET.lcp}ms`).toBeLessThanOrEqual(
CWV_BUDGET.lcp,
);
expect(sample.cls, `CLS ${sample.cls.toFixed(3)} exceeds budget ${CWV_BUDGET.cls}`).toBeLessThanOrEqual(
CWV_BUDGET.cls,
);
}