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, 0–1+). */ 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 { 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 { 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, ); }