← back to Homesonspec
apps/web/scripts/verify-pwa.mjs
66 lines
// End-to-end PWA proof in an isolated headless Chromium:
// 1. Load the app, wait for the service worker to activate.
// 2. Confirm the shell cache got populated.
// 3. Go offline, reload a navigation, and confirm the offline fallback renders.
import { createRequire } from "node:module";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
const here = dirname(fileURLToPath(import.meta.url));
const repoRoot = join(here, "..", "..", "..");
const require = createRequire(join(repoRoot, "package.json"));
// pnpm doesn't hoist playwright into apps/web — resolve it from the store by absolute path.
const { chromium } = require(join(repoRoot, "node_modules/.pnpm/playwright@1.61.1/node_modules/playwright"));
const BASE = process.env.BASE || "http://localhost:3199";
const ok = (b) => (b ? "✓" : "✗");
let pass = true;
const browser = await chromium.launch();
const ctx = await browser.newContext();
const page = await ctx.newPage();
await page.goto(BASE + "/", { waitUntil: "load" });
// 1. Wait for an activated service worker.
const swActive = await page.evaluate(async () => {
if (!("serviceWorker" in navigator)) return false;
const reg = await navigator.serviceWorker.ready.catch(() => null);
return !!(reg && (reg.active || reg.installing || reg.waiting));
});
console.log(`${ok(swActive)} service worker registered + activated`);
pass &&= swActive;
// Give SWR a beat to populate caches, then inspect them.
await page.waitForTimeout(1500);
const caches = await page.evaluate(async () => {
const keys = await self.caches.keys();
const shell = keys.find((k) => k.endsWith("-shell"));
if (!shell) return { keys, shellCount: 0 };
const c = await self.caches.open(shell);
const reqs = await c.keys();
return { keys, shellCount: reqs.length, shellUrls: reqs.map((r) => new URL(r.url).pathname) };
});
const cachesOk = caches.shellCount > 0;
console.log(`${ok(cachesOk)} shell cache populated (${caches.shellCount} entries: ${(caches.shellUrls || []).join(", ")})`);
console.log(` cache buckets: ${caches.keys.join(", ")}`);
pass &&= cachesOk;
// 3. Offline fallback: block the network and force a fresh navigation.
await ctx.setOffline(true);
let offlineText = "";
try {
await page.goto(BASE + "/search", { waitUntil: "load", timeout: 8000 });
offlineText = await page.evaluate(() => document.body.innerText);
} catch (e) {
offlineText = "NAV-THREW: " + e.message;
}
const offlineOk = /offline/i.test(offlineText);
console.log(`${ok(offlineOk)} offline navigation served fallback (matched "offline": ${offlineOk})`);
pass &&= offlineOk;
await ctx.setOffline(false);
await browser.close();
console.log(pass ? "\nPWA VERIFY: PASS" : "\nPWA VERIFY: FAIL");
process.exit(pass ? 0 : 1);