import { expect, test } from "@playwright/test"; const CACHE = "who-need-help-static-v3"; const STALE_CACHE = "who-need-help-static-v0"; const SHELL_PATHS = [ "/offline.html", "/manifest.webmanifest", "/assets/css/app.css", "/assets/js/app.js", "/images/logo.svg", "/images/pwa-192.png", "/images/pwa-512.png", "/images/pwa-maskable-512.png", ]; async function waitForController(page: import("@playwright/test").Page): Promise { await page.evaluate(async () => { await navigator.serviceWorker.ready; if (!navigator.serviceWorker.controller) { await new Promise((resolve) => { navigator.serviceWorker.addEventListener("controllerchange", () => resolve(), { once: true, }); }); } }); } test("public HTTPS PWA installs, updates its cache, and falls back safely offline", async ({ context, page, request, }) => { const consoleFailures: string[] = []; page.on("console", (message) => { if (["warning", "error"].includes(message.type())) { consoleFailures.push(`${message.type()}: ${message.text()}`); } }); page.on("pageerror", (error) => consoleFailures.push(`pageerror: ${error.message}`)); const manifestResponse = await request.get("/manifest.webmanifest"); expect(manifestResponse.status()).toBe(200); expect(manifestResponse.headers()["content-type"]).toContain("application/manifest+json"); const manifest = (await manifestResponse.json()) as { name: string; short_name: string; id: string; start_url: string; scope: string; display: string; icons: Array<{ src: string; sizes: string; type: string; purpose: string }>; }; expect(manifest).toMatchObject({ name: "Who Need Help", short_name: "Who Need Help", id: "/", start_url: "/", scope: "/", display: "standalone", }); expect(manifest.icons).toEqual( expect.arrayContaining([ expect.objectContaining({ sizes: "192x192", purpose: "any" }), expect.objectContaining({ sizes: "512x512", purpose: "any" }), expect.objectContaining({ sizes: "512x512", purpose: "maskable" }), expect.objectContaining({ sizes: "1024x1024", purpose: "maskable" }), expect.objectContaining({ sizes: "any", purpose: "monochrome" }), ]), ); await page.goto("/"); await waitForController(page); const iconDimensions = await page.evaluate(async () => { const dimensions: Record = {}; for (const path of [ "/images/pwa-192.png", "/images/pwa-512.png", "/images/pwa-maskable-512.png", "/images/pwa-maskable-1024.png", ]) { const response = await fetch(path); const image = await createImageBitmap(await response.blob()); dimensions[path] = [image.width, image.height]; image.close(); } return dimensions; }); expect(iconDimensions).toEqual({ "/images/pwa-192.png": [192, 192], "/images/pwa-512.png": [512, 512], "/images/pwa-maskable-512.png": [512, 512], "/images/pwa-maskable-1024.png": [1024, 1024], }); await expect .poll(() => page.evaluate(() => caches.keys())) .toContain(CACHE); const cachedShell = await page.evaluate(async (cacheName) => { const cache = await caches.open(cacheName); return (await cache.keys()).map((entry) => new URL(entry.url).pathname).sort(); }, CACHE); expect(cachedShell).toEqual(expect.arrayContaining(SHELL_PATHS)); await page.goto("/safety"); await page.goto("/users/log-in"); const cachedDocuments = await page.evaluate(async () => { const entries = await Promise.all( (await caches.keys()).map(async (cacheName) => { const cache = await caches.open(cacheName); return Promise.all( (await cache.keys()).map(async (entry) => ({ path: new URL(entry.url).pathname, destination: entry.destination, contentType: (await cache.match(entry))?.headers.get("content-type") ?? "", })), ); }), ); return entries.flat().filter((entry) => entry.contentType.includes("text/html")); }); expect(cachedDocuments).toEqual([ expect.objectContaining({ path: "/offline.html" }), ]); await page.evaluate((staleCache) => caches.open(staleCache), STALE_CACHE); expect(await page.evaluate(() => caches.keys())).toContain(STALE_CACHE); await page.evaluate(async () => { const registrations = await navigator.serviceWorker.getRegistrations(); await Promise.all(registrations.map((registration) => registration.unregister())); }); await page.close(); const updatedPage = await context.newPage(); await updatedPage.goto("/"); await waitForController(updatedPage); await expect .poll(() => updatedPage.evaluate(() => caches.keys())) .not.toContain(STALE_CACHE); expect(await updatedPage.evaluate(() => caches.keys())).toContain(CACHE); await context.setOffline(true); const offlineResponse = await updatedPage.goto("/requests"); expect(offlineResponse?.status()).toBe(200); await expect(updatedPage.getByRole("heading", { name: "You're offline" })).toBeVisible(); await expect(updatedPage.getByRole("button", { name: "Try again" })).toBeVisible(); await expect(updatedPage.locator("img[src='/images/logo.svg']")).toBeVisible(); expect(await updatedPage.evaluate(() => navigator.onLine)).toBe(false); await context.setOffline(false); await updatedPage.reload(); await expect(updatedPage.getByRole("heading", { name: "Log in", exact: true })).toBeVisible(); expect(consoleFailures).toEqual([]); });