import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; import { runInNewContext } from "node:vm"; import { createElement, Fragment } from "react"; import { renderToStaticMarkup } from "react-dom/server"; import { NextRequest } from "next/server"; import { createMetadataElements } from "next/dist/lib/metadata/metadata-elements"; import { unstable_doesMiddlewareMatch as doesProxyMatch, } from "next/experimental/testing/server"; import nextConfig from "../next.config"; import manifest from "../src/app/manifest"; import { pwaConfig, pwaViewport as viewport } from "../src/lib/pwa/config"; import { config as proxyConfig, proxy } from "../src/proxy"; import { createContext } from "../src/server/context"; import { enrichSEO, toNextMetadata } from "../src/server/seo"; function pngDimensions(source: string): [number, number] { assert.match(source, /^\/static\/pwa\/[\w-]+\.png$/); const bytes = readFileSync(new URL("../public" + source, import.meta.url)); assert.deepEqual(bytes.subarray(0, 8), Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])); assert.equal(bytes.toString("ascii", 12, 16), "IHDR"); return [bytes.readUInt32BE(16), bytes.readUInt32BE(20)]; } test("native manifest installs the Chinese brand at the root with fullscreen and standalone support", () => { const data = manifest(); assert.equal(data.name, "花魁短剧"); assert.equal(data.short_name, "花魁短剧"); assert.equal(data.lang, "zh-CN"); assert.equal(data.id, "/"); assert.equal(data.start_url, "/"); assert.equal(data.scope, "/"); assert.equal(data.display, "fullscreen"); assert.deepEqual(data.display_override, ["fullscreen", "standalone"]); assert.equal(data.orientation, "any"); assert.equal(data.theme_color, pwaConfig.themeColor); assert.equal(data.background_color, pwaConfig.themeColor); assert.equal(pwaConfig.manifest, "/manifest.webmanifest"); assert.equal(pwaConfig.worker, "/sw.js"); }); test("installation icons are real 192/512 PNGs with an independent maskable asset and 180 Apple icon", () => { const icons = manifest().icons || []; for (const size of [192, 512]) { const icon = icons.find((item) => item.sizes === `${size}x${size}` && item.purpose === "any"); assert.ok(icon, `${size}px icon`); assert.equal(icon.type, "image/png"); assert.deepEqual(pngDimensions(icon.src), [size, size]); } const maskable = icons.find((item) => item.purpose === "maskable"); const regular = icons.find((item) => item.sizes === "512x512" && item.purpose === "any"); assert.ok(maskable && regular); assert.equal(maskable.sizes, "512x512"); assert.equal(maskable.type, "image/png"); assert.deepEqual(pngDimensions(maskable.src), [512, 512]); assert.notEqual(maskable.src, regular.src); assert.notDeepEqual( readFileSync(new URL("../public" + maskable.src, import.meta.url)), readFileSync(new URL("../public" + regular.src, import.meta.url)), ); assert.deepEqual(pngDimensions(pwaConfig.icons.apple), [180, 180]); }); test("global viewport and Apple metadata cover the installed screen on public, private and error pages", () => { assert.equal(viewport.width, "device-width"); assert.equal(viewport.initialScale, 1); assert.equal(viewport.viewportFit, "cover"); assert.equal(viewport.themeColor, pwaConfig.themeColor); const site = "https://canonical.drama.test"; const appConfig = { foreverUrlAddr: site, seoInfo: { webSiteName: "花魁短剧" } }; for (const [route, status] of [["/", 200], ["/drama/story/", 200], ["/account/settings/", 200], ["/missing/", 404]] as const) { const data = enrichSEO(createContext(new Request(site + route)), { appConfig }, { status }); const metadata = toNextMetadata(data); assert.equal(metadata.applicationName, "花魁短剧", route); assert.equal(metadata.manifest, "/manifest.webmanifest", route); assert.deepEqual(metadata.appleWebApp, { capable: true, title: "花魁短剧", statusBarStyle: "black-translucent", }, route); assert.equal(metadata.other?.["apple-mobile-web-app-capable"], "yes", route); // Exercise Next's actual output: appleWebApp emits the generic capability, // while Safari still requires the separate Apple-prefixed capability. const elements = createMetadataElements({ appleWebApp: metadata.appleWebApp, other: metadata.other, } as Parameters[0]); const head = renderToStaticMarkup(createElement(Fragment, null, ...elements)); for (const name of ["mobile-web-app-capable", "apple-mobile-web-app-capable"]) { assert.equal((head.match(new RegExp(`name="${name}"`, "g")) || []).length, 1, route + " " + name); assert.match(head, new RegExp(` { const site = "https://canonical.drama.test"; const appConfig = { foreverUrlAddr: site, seoInfo: { title: "花魁短剧", webSiteName: "花魁短剧" } }; const page = enrichSEO(createContext(new Request("http://internal:3000/dramas/all/0/1/page/2/?dc=discard")), { appConfig, pagination: { Page: 2, Visible: true, HasPrev: true, PrevURL: "/list/", HasNext: true, NextURL: "/list/all/0/1/page/3/" }, list: [{ id: 41, slug: "story", title: "故事" }, { id: 42, slug: "another", title: "另一部故事" }], }); const metadata = toNextMetadata(page); assert.equal(metadata.alternates?.canonical, site + "/dramas/all/0/1/page/2/"); assert.deepEqual(metadata.pagination, { previous: site + "/dramas/", next: site + "/dramas/all/0/1/page/3/" }); assert.equal(metadata.robots, "index,follow,max-image-preview:large,max-snippet:-1,max-video-preview:-1"); assert.equal(metadata.other?.googlebot, page.robots); assert.equal(metadata.verification?.google, "Y_ldHPiw-yBVgVw_jZ4UG5WJw-xjO31mBMfhflqMGw8"); assert.deepEqual(metadata.verification?.yandex, ["56acf08532be3060", "acaf652efa6cd920"]); assert.equal(metadata.verification?.other?.["msvalidate.01"], "57A5999E9B5167EE9039265EBCA2AE3A"); assert.ok(JSON.parse(page.jsonLD)["@graph"].some((node: { "@type": string }) => node["@type"] === "ItemList")); for (const [route, extra, status, robots] of [ ["/feedback/", {}, 200, "noindex,nofollow"], ["/navigation/", { isNavPage: true }, 200, "noindex,follow"], ["/missing/", {}, 404, "noindex,follow"], ["/dramas/", {}, 503, "noindex,follow"], ] as const) { const ctx = createContext(new Request(site + route)); const data = enrichSEO(ctx, { appConfig, ...extra }, { status }); const result = toNextMetadata(data); assert.equal(result.robots, robots, route); assert.equal(result.alternates, undefined, route); assert.equal(data.jsonLD, undefined, route); assert.equal(ctx.responseHeaders.get("x-robots-tag"), robots.replace(/,/g, ", "), route); if (route === "/navigation/") { assert.equal(result.verification?.google, "Y_ldHPiw-yBVgVw_jZ4UG5WJw-xjO31mBMfhflqMGw8"); assert.deepEqual(result.verification?.yandex, ["f5f965a1c5d31a69", "acaf652efa6cd920"]); } } }); test("Next's actual matcher excludes installation assets while retaining normal pages, APIs and signed media", () => { for (const url of ["/manifest.webmanifest", "/manifest.webmanifest?v=1", "/sw.js", "/sw.js?v=1", "/static/pwa/icon-192.png", "/_next/static/chunk.js"]) assert.equal(doesProxyMatch({ config: proxyConfig, nextConfig, url }), false, url); for (const url of ["/", "/dramas/", "/account/login/", "/feedback/", "/api/auth/guest", "/h5/m3u8/story.m3u8?ticket=a%2Bb%3D", "/enkey?ticket=1", "/manifest.webmanifest/child", "/sw.js/child"]) assert.equal(doesProxyMatch({ config: proxyConfig, nextConfig, url }), true, url); }); test("manifest and worker runtime bypass never create guest sessions, cookies or SSR handoffs, including navigation-only mode", async () => { const originalFetch = globalThis.fetch; const originalNavigation = process.env.NAVIGATION_ONLY; let requests = 0; globalThis.fetch = async () => { requests++; throw new Error("Installation assets must not contact the upstream"); }; try { for (const navigation of ["false", "true"]) { process.env.NAVIGATION_ONLY = navigation; for (const path of ["/manifest.webmanifest?dc=pwa-test", "/sw.js?v=pwa-test"]) for (const method of ["GET", "HEAD"]) { const request = new NextRequest("https://site.test" + path, { method }); const response = await proxy(request); assert.equal(response.status, 200, navigation + path + method); assert.equal(response.headers.get("x-middleware-next"), "1"); assert.deepEqual(response.headers.getSetCookie(), []); assert.equal(response.headers.has("location"), false); assert.equal(response.headers.has("x-middleware-rewrite"), false); assert.equal(response.headers.has("x-middleware-override-headers"), false); for (const name of response.headers.keys()) assert.equal(name.includes("x-hk-"), false, "Unexpected handoff: " + name); assert.equal(request.headers.get("cookie"), null); } } assert.equal(requests, 0); } finally { globalThis.fetch = originalFetch; if (originalNavigation === undefined) delete process.env.NAVIGATION_ONLY; else process.env.NAVIGATION_ONLY = originalNavigation; } }); test("the generated worker activates without fetch interception, network calls or Cache Storage access", async () => { type Event = { waitUntil: (promise: Promise) => void }; const listeners = new Map void>(); let waiting = 0; let claimed = 0; const worker = { addEventListener(name: string, listener: (event: Event) => void) { assert.equal(listeners.has(name), false, "Duplicate worker listener"); listeners.set(name, listener); }, skipWaiting: async () => { waiting++; }, clients: { claim: async () => { claimed++; } }, }; const forbidden = (name: string) => () => { assert.fail("The installation worker must not access " + name); }; const sandbox = { self: worker }; for (const target of [worker, sandbox]) { Object.defineProperty(target, "caches", { get: forbidden("Cache Storage") }); Object.defineProperty(target, "fetch", { get: forbidden("fetch") }); } runInNewContext(readFileSync(new URL("../public/sw.js", import.meta.url), "utf8"), sandbox, { timeout: 1000 }); assert.deepEqual([...listeners.keys()].sort(), ["activate", "install"]); for (const name of ["install", "activate"]) { const promises: Promise[] = []; listeners.get(name)!({ waitUntil: (promise) => { promises.push(Promise.resolve(promise)); } }); assert.equal(promises.length, 1, name + " must wait for its lifecycle operation"); await Promise.all(promises); } assert.equal(waiting, 1); assert.equal(claimed, 1); }); test("the worker is served as JavaScript without persistent HTTP caching and with root scope", async () => { const rules = await nextConfig.headers!(); const worker = rules.find((rule) => rule.source === "/sw.js"); assert.ok(worker); const headers = new Headers(worker.headers.map(({ key, value }): [string, string] => [key, value])); assert.match(headers.get("content-type") || "", /^(?:application|text)\/javascript\b/i); assert.match(headers.get("cache-control") || "", /\bno-store\b/); assert.match(headers.get("cache-control") || "", /\bno-cache\b/); assert.equal(headers.get("service-worker-allowed"), "/"); });