import { test, expect } from "@playwright/test"; import { BASE_GROUNDS, ICON_PALETTES } from "../../common/lib/brand"; // PWA surface for the export (Archive) sites: an installable web app manifest, // a registered service worker, and the /offline management page. The transcript // shard caching itself is validated by the production build (artifacts emit) + // the SW code; the e2e harness mocks /transcripts/** so a full offline // round-trip isn't exercised here. test("serves a valid web app manifest with per-site fields", async ({ request, }) => { const res = await request.get("/manifest.webmanifest"); expect(res.ok()).toBeTruthy(); const m = await res.json(); expect(m.name).toBeTruthy(); expect(m.start_url).toBe("/"); expect(m.display).toBe("standalone"); expect(Array.isArray(m.icons)).toBe(true); // 192 + 512 + a maskable icon (installability). const sizes = m.icons.map((i: { sizes: string }) => i.sizes); expect(sizes).toContain("192x192"); expect(sizes).toContain("512x512"); expect( m.icons.some((i: { purpose?: string }) => i.purpose === "maskable"), ).toBe(true); // The scalable mark, for launchers that take one. expect(m.icons).toContainEqual({ src: "/icons/icon.svg", sizes: "any", type: "image/svg+xml", purpose: "any", }); // The chrome is the dark base's ground and the splash the icon's own tile — // never the accent, which a reader may change and the manifest cannot follow. expect(m.theme_color).toBe(BASE_GROUNDS.dark); expect(m.background_color).toBe(ICON_PALETTES.child.ground); }); const PNG_MAGIC = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]; test("serves the service worker and the icon set", async ({ request }) => { const sw = await request.get("/sw.js"); expect(sw.ok()).toBeTruthy(); // Rendered by app/icons/[file]/route.ts, not committed: each is a real PNG // of its size (IHDR width/height at bytes 16 and 20). for (const [icon, size] of [ ["/icons/icon-32.png", 32], ["/icons/icon-192.png", 192], ["/icons/icon-512.png", 512], ["/icons/maskable-512.png", 512], ["/icons/apple-touch-icon.png", 180], ] as const) { const r = await request.get(icon); expect(r.ok(), icon).toBeTruthy(); expect(r.headers()["content-type"], icon).toBe("image/png"); const body = await r.body(); expect([...body.subarray(0, 8)], icon).toEqual(PNG_MAGIC); expect([body.readUInt32BE(16), body.readUInt32BE(20)], icon).toEqual([size, size]); } for (const icon of ["/icons/icon.svg", "/icons/maskable.svg"]) { const r = await request.get(icon); expect(r.ok(), icon).toBeTruthy(); expect(r.headers()["content-type"], icon).toBe("image/svg+xml"); expect(await r.text(), icon).toMatch(/^ { await page.goto("/offline/"); await expect( page.getByRole("heading", { name: "Offline", level: 1 }), ).toBeVisible(); await expect(page.getByLabel("connection status")).toBeVisible(); // testsite exposes at least one channel → a Download control is present. await expect( page.getByRole("button", { name: /download .* for offline/ }).first(), ).toBeVisible(); });