164 lines
5.5 KiB
TypeScript
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([]);
|
|
});
|