feat(t5): App-Icons, Manifest und Offline-Cache v5 (T5a A9)
- Honigzelle auf Tinte als Master-SVG; scripts/generate-icons.mjs erzeugt any/maskable 192/512, Monochrom 512, drei Shortcut-Icons, Apple-Icon und favicon.ico (16/32/48, ICO-Container von Hand) byte-gleich bei jedem Lauf. - manifest.ts exakt nach Spec 12.3, Farben gleich dem hellen --background. - Service Worker: SHELL_CACHE apicore-shell-v5, Icons im Shell-Cache; ein fehlendes Icon hält das Vorladen der Seiten nicht auf. - Proxy-Matcher: nur echte Icon-Dateien (icons/*.png) ohne Sicherheits-Header, Punkte maskiert. - BrandMark-Deckel mittig (linke Kante 18.8). Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
fee84c7a6b
commit
2395576c88
23 files changed
+632
-67
No files matched your search
@@ -3,6 +3,7 @@ import { readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import vm from "node:vm";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import manifest from "@/app/manifest";
|
||||
|
||||
// public/sw.js itself, run in a sandbox with an in-memory Cache Storage and a
|
||||
// fake network - the worker's wiring, beyond the pure rules in sw-routes.js.
|
||||
@@ -10,6 +11,8 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
const APP = "https://apicore.example";
|
||||
const API = "https://apicorebackend.example";
|
||||
const PUBLIC_DIR = path.resolve(process.cwd(), "public");
|
||||
// Spec 12.3: raised so installed apps fetch the new icons and tokens.
|
||||
const SHELL = "apicore-shell-v5";
|
||||
|
||||
interface MatchOptions {
|
||||
ignoreSearch?: boolean;
|
||||
@@ -154,7 +157,7 @@ function page(asset: string, marker: string): string {
|
||||
|
||||
async function workerWithBackend(network: ReturnType<typeof vi.fn>): Promise<Worker> {
|
||||
const caches = new FakeCacheStorage();
|
||||
const shell = await caches.open("apicore-shell-v4");
|
||||
const shell = await caches.open(SHELL);
|
||||
await shell.put("/api/runtime-config", json({ apiUrl: `${API}/api/v1` }));
|
||||
const worker = loadWorker(caches, network);
|
||||
// Let the worker read the backend origin from the cached config.
|
||||
@@ -289,7 +292,7 @@ describe("service worker", () => {
|
||||
describe("background refresh of a cached offline page", () => {
|
||||
async function workerWithOldOfflinePage(): Promise<Worker> {
|
||||
const caches = new FakeCacheStorage();
|
||||
const shell = await caches.open("apicore-shell-v4");
|
||||
const shell = await caches.open(SHELL);
|
||||
await shell.put("/offline/colony", html(page("/_next/static/chunks/old.js", "alt")));
|
||||
await shell.put("/_next/static/chunks/old.js", new Response("old()"));
|
||||
caches.log.length = 0;
|
||||
@@ -297,7 +300,7 @@ describe("service worker", () => {
|
||||
}
|
||||
|
||||
async function cachedOfflinePage(worker: Worker): Promise<string | undefined> {
|
||||
const shell = await worker.caches.open("apicore-shell-v4");
|
||||
const shell = await worker.caches.open(SHELL);
|
||||
return (await shell.match("/offline/colony"))?.text();
|
||||
}
|
||||
|
||||
@@ -317,8 +320,8 @@ describe("service worker", () => {
|
||||
// ... and the new one is kept with its assets, the assets first.
|
||||
expect(await cachedOfflinePage(worker)).toContain("neu");
|
||||
expect(worker.caches.log).toEqual([
|
||||
"apicore-shell-v4 /_next/static/chunks/new.js",
|
||||
"apicore-shell-v4 /offline/colony",
|
||||
`${SHELL} /_next/static/chunks/new.js`,
|
||||
`${SHELL} /offline/colony`,
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -335,4 +338,65 @@ describe("service worker", () => {
|
||||
expect(await cachedOfflinePage(worker)).toContain("alt");
|
||||
});
|
||||
});
|
||||
|
||||
describe("app icons in the shell (spec 12.3)", () => {
|
||||
function answerEverything() {
|
||||
network.mockImplementation(async (request: RequestInfo) => {
|
||||
const { pathname } = new URL(keyOf(request));
|
||||
if (pathname === "/api/runtime-config") return json({ apiUrl: `${API}/api/v1` });
|
||||
if (pathname.startsWith("/_next/static/")) return new Response("asset()");
|
||||
if (/\.(png|ico|svg)$/.test(pathname)) {
|
||||
return new Response("image", { headers: { "Content-Type": "image/png" } });
|
||||
}
|
||||
return html(page("/_next/static/chunks/app.js", pathname));
|
||||
});
|
||||
}
|
||||
|
||||
it("the service worker cache version is apicore-shell-v5", async () => {
|
||||
const caches = new FakeCacheStorage();
|
||||
await (await caches.open("apicore-shell-v4")).put("/dashboard", html("alt"));
|
||||
answerEverything();
|
||||
const worker = loadWorker(caches, network);
|
||||
|
||||
await dispatch(worker, "install", {});
|
||||
await dispatch(worker, "activate", {});
|
||||
|
||||
expect(await caches.keys()).toContain(SHELL);
|
||||
expect(await caches.keys()).not.toContain("apicore-shell-v4");
|
||||
});
|
||||
|
||||
it("install keeps every manifest icon in the shell cache", async () => {
|
||||
answerEverything();
|
||||
const worker = loadWorker(new FakeCacheStorage(), network);
|
||||
|
||||
await dispatch(worker, "install", {});
|
||||
|
||||
const data = manifest();
|
||||
const icons = [
|
||||
...(data.icons ?? []),
|
||||
...(data.shortcuts ?? []).flatMap((shortcut) => shortcut.icons ?? []),
|
||||
].map((icon) => icon.src);
|
||||
const shell = await worker.caches.open(SHELL);
|
||||
for (const src of icons) {
|
||||
expect(await shell.match(src), src).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
it("a missing icon does not stop the warm-up of the pages", async () => {
|
||||
answerEverything();
|
||||
const pages = network.getMockImplementation() as (request: RequestInfo) => Promise<Response>;
|
||||
network.mockImplementation(async (request: RequestInfo) =>
|
||||
new URL(keyOf(request)).pathname.startsWith("/icons/")
|
||||
? new Response("gone", { status: 404 })
|
||||
: pages(request),
|
||||
);
|
||||
const worker = loadWorker(new FakeCacheStorage(), network);
|
||||
|
||||
await dispatch(worker, "install", {});
|
||||
|
||||
const shell = await worker.caches.open(SHELL);
|
||||
expect(await shell.match("/dashboard")).toBeDefined();
|
||||
expect(await shell.match("/icons/icon-192.png")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user