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

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:
2026-08-04 22:17:43 -04:00
parent 071bda243b
commit 26faf33a95
15 changed files with 265 additions and 342 deletions
+65
View File
@@ -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, 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');
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,
);
}