Sprint D: hardening e2e, /search redirect, scroll restore fix.
CI / skip-ci-check (pull_request) Successful in 29s
CI / docker-ci (pull_request) Successful in 32s
CI / python-lint (pull_request) Successful in 32s
CI / secret-scan (pull_request) Successful in 37s
CI / viewer-unit (pull_request) Successful in 1m53s
CI / admin-unit (pull_request) Successful in 2m39s
CI / e2e (pull_request) Failing after 3m19s
CI / skip-ci-check (pull_request) Successful in 29s
CI / docker-ci (pull_request) Successful in 32s
CI / python-lint (pull_request) Successful in 32s
CI / secret-scan (pull_request) Successful in 37s
CI / viewer-unit (pull_request) Successful in 1m53s
CI / admin-unit (pull_request) Successful in 2m39s
CI / e2e (pull_request) Failing after 3m19s
Admin review smoke tests, CWV budgets, unified gallery at /, and explicit body unlock when closing the photo modal so scroll position restores.
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
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: 5_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<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');
|
||||
await page.waitForTimeout(settleMs);
|
||||
return page.evaluate(() => {
|
||||
const w = window as Window & { __punimtagCwv?: WebVitalsSample };
|
||||
return w.__punimtagCwv ?? { lcp: 0, cls: 0 };
|
||||
});
|
||||
}
|
||||
|
||||
export function expectWithinCwvBudget(sample: WebVitalsSample): void {
|
||||
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,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user