import { test } from "node:test"; import assert from "node:assert/strict"; import { readFileSync, readdirSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { inflateSync } from "node:zlib"; import { ACCENTS, ICON_PALETTES, markSvg } from "./brand"; import { resolveAccent } from "./accent"; import { FAVICON_SIZES, ICON_FILES, ICON_METADATA, iconFile, siteIconPalette, } from "./brandIconFiles"; import { pngToIco, renderFaviconIco, renderIconFile, renderIconPng } from "./brandIcons"; const PNG_MAGIC = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]; // A PNG's first chunk is IHDR: length(4) "IHDR"(4) width(4) height(4), big-endian. function ihdr(png: Uint8Array): { width: number; height: number } { assert.deepEqual([...png.subarray(0, 8)], PNG_MAGIC, "PNG magic"); assert.equal(Buffer.from(png.subarray(12, 16)).toString("latin1"), "IHDR"); const v = new DataView(png.buffer, png.byteOffset, png.byteLength); return { width: v.getUint32(16), height: v.getUint32(20) }; } // The RGBA of pixel (0, 0). Every PNG filter predicts the first pixel of the // first scanline from zeros, so after inflating the IDAT stream its bytes are // the pixel as stored, whichever filter the encoder chose — no unfiltering. function topLeftPixel(png: Uint8Array): number[] { const b = Buffer.from(png); assert.equal(b[24], 8, "bit depth 8"); assert.equal(b[25], 6, "colour type 6 (RGBA)"); const idat: Buffer[] = []; for (let off = 8; off < b.length; ) { const len = b.readUInt32BE(off); if (b.toString("latin1", off + 4, off + 8) === "IDAT") idat.push(b.subarray(off + 8, off + 8 + len)); off += 12 + len; } const raw = inflateSync(Buffer.concat(idat)); return [...raw.subarray(1, 5)]; // byte 0 is the scanline's filter type } const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); test("ICON_FILES: the seven URLs the manifest, , OG image and PWAs name", () => { assert.deepEqual( ICON_FILES.map((f) => f.file), [ "icon.svg", "maskable.svg", "icon-32.png", "icon-192.png", "icon-512.png", "maskable-512.png", "apple-touch-icon.png", ], ); const sizes = Object.fromEntries( ICON_FILES.flatMap((f) => (f.format === "png" ? [[f.file, f.size]] : [])), ); assert.deepEqual(sizes, { "icon-32.png": 32, "icon-192.png": 192, "icon-512.png": 512, "maskable-512.png": 512, "apple-touch-icon.png": 180, }); assert.equal(iconFile("maskable-512.png")?.variant, "maskable"); assert.equal(iconFile("apple-touch-icon.png")?.variant, "apple"); assert.equal(iconFile("favicon.ico"), undefined); }); test("ICON_METADATA links only files the routes serve, SVG first", () => { const served = new Set(ICON_FILES.map((f) => `/icons/${f.file}`)); const links = [...ICON_METADATA.icon, ...ICON_METADATA.apple]; for (const l of links) assert.ok(served.has(l.url), l.url); assert.deepEqual(ICON_METADATA.icon[0], { url: "/icons/icon.svg", type: "image/svg+xml" }); for (const l of ICON_METADATA.icon.slice(1)) { const f = iconFile(l.url.slice("/icons/".length)); assert.ok(f && f.format === "png"); assert.equal(l.sizes, `${f.size}x${f.size}`); } }); test("siteIconPalette: the child ground, lit by the accent's on-dark value", () => { assert.deepEqual(siteIconPalette("brass"), { ground: ICON_PALETTES.child.ground, dim: ICON_PALETTES.child.dim, lit: ACCENTS.brass.onDark, }); // Absent or malformed reads as Signal. assert.equal(siteIconPalette(undefined).lit, ACCENTS.signal.onDark); assert.equal(siteIconPalette("not-a-colour").lit, ACCENTS.signal.onDark); // A custom hex is lit with its dark-ground fit, the value the header uses. assert.equal(siteIconPalette("#cc3366").lit, resolveAccent("#cc3366").dark); assert.equal(siteIconPalette("#cc3366").lit, "#d14574"); }); test("pngToIco: ICONDIR, one ICONDIRENTRY per image, then the payloads verbatim", () => { const a = Uint8Array.from([1, 2, 3]); const b = Uint8Array.from([4, 5, 6, 7, 8]); const ico = pngToIco([ { size: 16, png: a }, { size: 256, png: b }, ]); const v = new DataView(ico.buffer, ico.byteOffset, ico.byteLength); // 00 00 01 00: reserved, type 1 (icon); then the count. assert.deepEqual([...ico.subarray(0, 4)], [0, 0, 1, 0]); assert.equal(v.getUint16(4, true), 2); assert.equal(ico.byteLength, 6 + 16 * 2 + a.byteLength + b.byteLength); // Entry 1: 16×16, 1 plane, 32 bpp, 3 bytes at offset 38. assert.deepEqual([ico[6], ico[7], ico[8], ico[9]], [16, 16, 0, 0]); assert.equal(v.getUint16(10, true), 1); assert.equal(v.getUint16(12, true), 32); assert.equal(v.getUint32(14, true), 3); assert.equal(v.getUint32(18, true), 38); // Entry 2: 256 is written as 0; its payload follows the first. assert.deepEqual([ico[22], ico[23]], [0, 0]); assert.equal(v.getUint32(30, true), 5); assert.equal(v.getUint32(34, true), 41); assert.deepEqual([...ico.subarray(38, 41)], [...a]); assert.deepEqual([...ico.subarray(41)], [...b]); assert.throws(() => pngToIco([{ size: 0, png: a }]), /1\.\.256/); assert.throws(() => pngToIco([{ size: 257, png: a }]), /1\.\.256/); }); test("renderIconPng: a PNG of the requested size, per variant", async () => { for (const [variant, size] of [ ["any", 32], ["any", 192], ["maskable", 512], ["apple", 180], ] as const) { const png = await renderIconPng(ICON_PALETTES.archilyzer, { variant, size }); assert.deepEqual(ihdr(png), { width: size, height: size }, `${variant} ${size}`); } }); test("renderIconPng draws the variant: any has transparent corners, maskable and apple are full-bleed", async () => { const p = ICON_PALETTES.archilyzer; const ground = [0x15, 0x1b, 0x20, 255]; // #151b20, opaque const any = await renderIconPng(p, { variant: "any", size: 512 }); assert.equal(topLeftPixel(any)[3], 0, "any: the rounded corner is transparent"); assert.deepEqual(topLeftPixel(await renderIconPng(p, { variant: "maskable", size: 512 })), ground); assert.deepEqual(topLeftPixel(await renderIconPng(p, { variant: "apple", size: 180 })), ground); }); test("renderFaviconIco: 16/32/48 PNG entries whose IHDR matches the directory", async () => { const ico = await renderFaviconIco(siteIconPalette("violet")); const v = new DataView(ico.buffer, ico.byteOffset, ico.byteLength); assert.deepEqual([...ico.subarray(0, 4)], [0, 0, 1, 0]); assert.equal(v.getUint16(4, true), FAVICON_SIZES.length); FAVICON_SIZES.forEach((size, i) => { const e = 6 + 16 * i; assert.equal(ico[e], size); assert.equal(ico[e + 1], size); const len = v.getUint32(e + 8, true); const off = v.getUint32(e + 12, true); assert.deepEqual(ihdr(ico.subarray(off, off + len)), { width: size, height: size }); }); }); test("renderIconFile: SVGs are markSvg verbatim, PNGs are PNGs, anything else is null", async () => { const palette = siteIconPalette("green"); const svg = await renderIconFile("icon.svg", palette); assert.deepEqual(svg, { body: markSvg(palette), contentType: "image/svg+xml" }); const maskable = await renderIconFile("maskable.svg", palette); assert.equal(maskable?.body, markSvg(palette, { variant: "maskable" })); const apple = await renderIconFile("apple-touch-icon.png", palette); assert.equal(apple?.contentType, "image/png"); assert.ok(apple && typeof apple.body !== "string"); assert.deepEqual(ihdr(apple.body as Uint8Array), { width: 180, height: 180 }); assert.equal(await renderIconFile("icon-64.png", palette), null); assert.equal(await renderIconFile("../site.json", palette), null); }); // next/og belongs to the icon and favicon routes alone: a page, layout or // header that imported lib/brandIcons would put the renderer in every page's // server graph. The pure data is lib/brandIconFiles.ts, which must not import // next at all. test("only the icon and favicon route handlers import lib/brandIcons; brandIconFiles imports no next", () => { const importers: string[] = []; for (const app of ["export/app", "homepage/app", "editor/app"]) { for (const rel of readdirSync(path.join(REPO, app), { recursive: true }) as string[]) { if (!/\.tsx?$/.test(rel)) continue; const src = readFileSync(path.join(REPO, app, rel), "utf8"); if (src.includes('lib/brandIcons"')) importers.push(`${app}/${rel}`); } } assert.deepEqual(importers.sort(), [ "export/app/favicon.ico/route.ts", "export/app/icons/[file]/route.ts", "homepage/app/favicon.ico/route.ts", "homepage/app/icons/[file]/route.ts", ]); const pure = readFileSync(path.join(REPO, "common/lib/brandIconFiles.ts"), "utf8"); assert.doesNotMatch(pure, /from "next/); assert.doesNotMatch(pure, /from "\.\/brandIcons"/); });