who_need_help/e2e/pwa/pwa.spec.ts

164 lines
5.5 KiB
TypeScript

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<void> {
await page.evaluate(async () => {
await navigator.serviceWorker.ready;
if (!navigator.serviceWorker.controller) {
await new Promise<void>((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<string, [number, number]> = {};
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([]);
});