- 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]>
67 lines
2.8 KiB
TypeScript
67 lines
2.8 KiB
TypeScript
// @vitest-environment node
|
|
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
import { BRAND_CELL_PATHS } from "@/components/brand-mark";
|
|
import { AREAS } from "@/lib/areas";
|
|
import { ICON_FILES, SHORTCUT_ICONS, buildIco, generateIcons, readMasterPaths } from "./generate-icons.mjs";
|
|
|
|
const ROOT = process.cwd();
|
|
|
|
describe("generate-icons (spec 12.2)", () => {
|
|
let outDir: string;
|
|
beforeEach(() => {
|
|
outDir = mkdtempSync(path.join(tmpdir(), "icons-"));
|
|
});
|
|
afterEach(() => rmSync(outDir, { recursive: true, force: true }));
|
|
|
|
it("the master SVG has the BrandMark geometry", () => {
|
|
expect(readMasterPaths(readFileSync(path.join(ROOT, "public/brand/apicore-mark.svg"), "utf8"))).toEqual(
|
|
BRAND_CELL_PATHS,
|
|
);
|
|
});
|
|
|
|
it("the lid sits centred over the cell, only lifted", () => {
|
|
const xs = (d: string) => [...d.matchAll(/L([\d.]+)\s/g)].map((match) => Number(match[1]));
|
|
const lidXs = xs(BRAND_CELL_PATHS.lid);
|
|
expect(Math.min(...lidXs) + Math.max(...lidXs)).toBeCloseTo(64, 5);
|
|
});
|
|
|
|
it("shortcut icons use the icon of their area", () => {
|
|
const pascal = (name: string) => name.replace(/(^|-)(\w)/g, (_, __, char: string) => char.toUpperCase());
|
|
for (const shortcut of SHORTCUT_ICONS) {
|
|
expect(AREAS[shortcut.area as keyof typeof AREAS].icon.displayName, shortcut.file).toBe(pascal(shortcut.lucide));
|
|
}
|
|
});
|
|
|
|
it("running the icon script twice leaves the files byte-identical", async () => {
|
|
const first = await generateIcons({ root: ROOT, outRoot: outDir });
|
|
const snapshot = new Map(first.map((file) => [file, readFileSync(path.join(outDir, file))]));
|
|
const second = await generateIcons({ root: ROOT, outRoot: outDir });
|
|
|
|
expect(second).toEqual(first);
|
|
expect([...first].sort()).toEqual([...ICON_FILES].sort());
|
|
for (const file of second) {
|
|
expect(readFileSync(path.join(outDir, file)).equals(snapshot.get(file) as Buffer), file).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("buildIco writes a header, one entry per PNG and the PNGs behind it", () => {
|
|
const png = (size: number) => Buffer.concat([Buffer.from([0x89, 0x50, 0x4e, 0x47]), Buffer.alloc(size)]);
|
|
const ico = buildIco([
|
|
{ size: 16, png: png(10) },
|
|
{ size: 256, png: png(20) },
|
|
]);
|
|
|
|
expect([ico.readUInt16LE(0), ico.readUInt16LE(2), ico.readUInt16LE(4)]).toEqual([0, 1, 2]);
|
|
// 256 px is written as 0 in the one-byte width and height fields.
|
|
expect([ico.readUInt8(6), ico.readUInt8(22)]).toEqual([16, 0]);
|
|
expect(ico.readUInt16LE(6 + 6)).toBe(32);
|
|
const secondOffset = ico.readUInt32LE(22 + 12);
|
|
expect(ico.readUInt32LE(6 + 12)).toBe(6 + 2 * 16);
|
|
expect(secondOffset).toBe(6 + 2 * 16 + 14);
|
|
expect(ico.length).toBe(secondOffset + 24);
|
|
});
|
|
});
|