···122122 Android manual "Add to Home Screen" both work from the manifest alone; the
123123 proactive Android install banner (which needs a service worker) is forgone on
124124 purpose. Loading speed is unchanged (CDN + ~94 KB bundle) with no new failure
125125- modes.
125125+ modes. The regression guard `tests/pwa.test.ts` uses `node:*` imports, so
126126+ `"node"` was added to the shared `tsconfig.json` `types` allowlist — a
127127+ conscious trade-off: Node globals become visible to `src/` typechecking, but
128128+ `noEmit` + Vite bundling mean it can't affect runtime; narrow later by giving
129129+ `tests` its own node-typed project if that boundary is wanted.
+11
tests/pwa.test.ts
···66const root = fileURLToPath(new URL('..', import.meta.url));
77const p = (rel: string) => root + rel;
8899+const SIG = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
1010+911/** Read width/height from a PNG's IHDR chunk (bytes 16–24, big-endian). */
1012function pngSize(path: string): { width: number; height: number } {
1113 const buf = readFileSync(path);
1414+ if (!buf.subarray(0, 8).equals(SIG)) throw new Error(`${path} is not a PNG`);
1215 return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
1616+}
1717+1818+/** PNG IHDR color type (byte 25): 2 = RGB (opaque), 6 = RGBA. */
1919+function pngColorType(path: string): number {
2020+ return readFileSync(path).readUInt8(25);
1321}
14221523describe('PWA icons', () => {
···2533 const { width, height } = pngSize(p(rel));
2634 expect(width).toBe(size);
2735 expect(height).toBe(size);
3636+ // Icons must be opaque (flattened onto #0e1116), not RGBA.
3737+ expect(pngColorType(p(rel)), `${rel} has an alpha channel`).toBe(2);
2838 });
2939 }
3040});
···6474 it('declares mobile-web-app capability and iOS app title', () => {
6575 expect(html).toContain('name="mobile-web-app-capable"');
6676 expect(html).toContain('name="apple-mobile-web-app-title"');
7777+ expect(html).toContain('name="apple-mobile-web-app-status-bar-style"');
6778 });
6879});