import { expect, test } from "@playwright/test"; const iphoneUserAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 Version/18.5 Mobile/15E148 Safari/604.1"; async function dispatchTouchStart( page: import("@playwright/test").Page, clientX: number, ) { return await page.locator(".nvr-navigator").evaluate((element, x) => { const event = new Event("touchstart", { bubbles: true, cancelable: true }); Object.defineProperty(event, "touches", { value: [{ identifier: 7, clientX: x, clientY: 240 }], }); return { dispatched: element.dispatchEvent(event), prevented: event.defaultPrevented, }; }, clientX); } test("ships an installable standalone manifest and iOS metadata", async ({ page, request, }) => { await page.goto("/inbox"); await expect( page.locator('meta[name="apple-mobile-web-app-capable"]'), ).toHaveAttribute("content", "yes"); await expect( page.locator('meta[name="apple-mobile-web-app-status-bar-style"]'), ).toHaveAttribute("content", "black-translucent"); const touchIconPath = await page .locator('link[rel="apple-touch-icon"]') .evaluate( (link) => new URL(link.getAttribute("href") ?? "", document.baseURI).pathname, ); expect(touchIconPath).toBe("/apple-touch-icon.png"); const manifestResponse = await request.get("/manifest.webmanifest"); expect(manifestResponse.ok()).toBe(true); const manifest = await manifestResponse.json(); expect(manifest).toMatchObject({ id: "/", scope: "/", start_url: "/", display: "standalone", }); expect(manifest.icons).toEqual( expect.arrayContaining([ expect.objectContaining({ src: "/pwa-192.png", sizes: "192x192", type: "image/png", }), expect.objectContaining({ src: "/pwa-512.png", sizes: "512x512", type: "image/png", }), ]), ); expect( (await request.get("/apple-touch-icon.png")).headers()["content-type"], ).toContain("image/png"); const workerResponse = await request.get("/sw.js"); expect(workerResponse.ok()).toBe(true); expect(workerResponse.headers()["cache-control"]).toContain("no-cache"); const worker = await workerResponse.text(); expect(worker).toContain("self.skipWaiting()"); expect(worker).toContain("clientsClaim()"); }); test("does not interfere with Safari edge touches before Home Screen installation", async ({ page, }) => { await page.addInitScript((userAgent) => { Object.defineProperty(navigator, "userAgent", { configurable: true, value: userAgent, }); Object.defineProperty(navigator, "standalone", { configurable: true, value: false, }); }, iphoneUserAgent); await page.goto("/chat/maya"); await expect(page.locator("html")).toHaveAttribute( "data-pwa-display-mode", "browser", ); await expect(page.locator("html")).toHaveAttribute( "data-pwa-edge-guard", "inactive", ); expect((await dispatchTouchStart(page, 1)).prevented).toBe(false); }); test("reserves only the leading edge in an installed iOS PWA", async ({ page, }) => { await page.addInitScript((userAgent) => { Object.defineProperty(navigator, "userAgent", { configurable: true, value: userAgent, }); Object.defineProperty(navigator, "standalone", { configurable: true, value: true, }); }, iphoneUserAgent); await page.goto("/chat/maya"); const root = page.locator("html"); await expect(root).toHaveAttribute("data-pwa-platform", "ios"); await expect(root).toHaveAttribute("data-pwa-display-mode", "standalone"); await expect(root).toHaveAttribute("data-pwa-edge-guard", "active"); expect((await dispatchTouchStart(page, 80)).prevented).toBe(false); expect((await dispatchTouchStart(page, 1)).prevented).toBe(true); await expect(root).toHaveAttribute("data-pwa-edge-claims", "1"); }); test("registers and activates the offline service worker", async ({ page }) => { await page.goto("/inbox"); const workerUrl = await page.evaluate(async () => { const registration = await navigator.serviceWorker.ready; return registration.active?.scriptURL ?? ""; }); expect(workerUrl).toMatch(/\/sw\.js$/); await expect(page.locator("html")).not.toHaveAttribute( "data-pwa-update-checks", "0", ); }); test("precaches lazily split routes for offline navigation", async ({ page, }) => { await page.goto("/inbox"); await page.evaluate(async () => { await navigator.serviceWorker.ready; }); const cachedUrls = await page.evaluate(async () => { const urls: string[] = []; for (const name of await caches.keys()) { const cache = await caches.open(name); urls.push(...(await cache.keys()).map((request) => request.url)); } return urls; }); expect(cachedUrls.some((url) => /StoriesView-.*\.js$/.test(url))).toBe(true); expect(cachedUrls.some((url) => /ProfileView-.*\.js$/.test(url))).toBe(true); expect(cachedUrls.some((url) => /RuntimeLabView-.*\.js$/.test(url))).toBe( true, ); });