Files
mokosh/tests/build/icons-present.test.ts
Mark 34a9ce10d4 test(01-12): wave-0 — scaffold RED unit tests (tokens / fonts / icons / no-remote-fonts / manifest-i18n / locale-parity)
Wave 0 of the design-integration plan. Six new test files at tests/build/
and tests/i18n/ pin the contracts that later waves will GREEN:

- tokens-adopted.test.ts (4 cases): src/shared/tokens.css exists +
  parses; src/popup/style.css @imports it; popup/style.css has zero
  hex literals; welcome.css conditional check.
- fonts-present.test.ts: 7 required WOFF2 faces (Lora normal + Plex Sans
  ×4 + Plex Mono ×2) + LICENSE-Lora + LICENSE-IBM-Plex + README +
  optional Lora-Italic (A5 verify-at-execute).
- icons-present.test.ts (15 cases across 3 sizes): existence, size FLOOR
  per assets-spec.md, PNG signature, dimensions, color-type byte === 6
  (RGBA — RED until Wave 2 rsvg-convert overwrites the 16-bit-RGB
  placeholders).
- no-remote-fonts.test.ts: production dist/ contains zero
  fonts.googleapis.com / https://fonts / googleapis substrings (MV3 CSP
  self-host invariant T-01-12-01).
- manifest-i18n.test.ts (10 cases): manifest:name === '__MSG_extName__',
  :description === '__MSG_extDesc__', :default_locale === 'en',
  :action.default_title === '__MSG_tooltipOff__'; _locales/{en,ru}/
  messages.json carry D-07 + D-08 canonical strings.
- locale-parity.test.ts (4 cases): ru→en parity, en→ru symmetric,
  non-empty .message strings (RESEARCH Pitfall 4 mitigation).

Current polarity: 29 RED + 18 GREEN across the 6 new files (placeholders
already clear dim+size floors; no-remote-fonts vacuous-GREEN since
tokens.css doesn't yet exist with remote URLs). Existing 100/100 vitest
baseline preserved (verified SKIP_BUILD=1 npx vitest run).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 21:56:08 +02:00

117 lines
4.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// tests/build/icons-present.test.ts — Plan 01-12 Wave 0 RED unit test.
//
// Asserts that icons/icon{16,48,128}.png are rasterized from the Loom
// brand mark via rsvg-convert (Wave 2 Task 1) — replacing the Bug A
// placeholders (16-bit/color RGB) with 8-bit/color RGBA.
//
// Polarity at Wave 0 land:
// - Dimension + size FLOOR cases: GREEN today (placeholders are at
// correct dims + clear size floors). Stay GREEN after Wave 2.
// - Color-type RGBA case: RED today (placeholders are color-type 2 =
// RGB). Flips GREEN after Wave 2 (rsvg-convert default output is
// 8-bit/color RGBA, color-type 6).
//
// PNG header layout per ISO/IEC 15948 §11.2.2:
// - Bytes 0-7: signature 89 50 4E 47 0D 0A 1A 0A
// - Bytes 8-11: chunk length (big-endian uint32)
// - Bytes 12-15: chunk type 'IHDR' (49 48 44 52)
// - Bytes 16-19: width (big-endian uint32)
// - Bytes 20-23: height (big-endian uint32)
// - Byte 24: bit depth (8 or 16 for our cases)
// - Byte 25: color type — 2=RGB, 6=RGBA (load-bearing for RED→GREEN flip)
//
// References:
// - RESEARCH §3 (verified locally: rsvg-convert defaults to 8-bit RGBA)
// - assets-spec.md (Chrome imageUtil silent-rejection FLOORs)
// - Plan 01-13 harness A9 (icon-size floor at UAT-time)
import { describe, expect, it } from 'vitest';
import { existsSync, openSync, readSync, closeSync, statSync } from 'node:fs';
import { resolve as resolvePath } from 'node:path';
const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const COLOR_TYPE_RGBA = 6;
interface IconSpec {
readonly size: number;
readonly sizeFloorBytes: number;
}
/** Per assets-spec.md Chrome imageUtil silent-rejection thresholds. */
const ICON_SPECS: ReadonlyArray<IconSpec> = [
{ size: 16, sizeFloorBytes: 200 },
{ size: 48, sizeFloorBytes: 500 },
{ size: 128, sizeFloorBytes: 1024 },
];
/**
* Read the first N bytes of a file synchronously. Used to parse the PNG
* header without slurping the whole file. Returns a Buffer of length N
* (zero-padded if the file is shorter, which only happens on a corrupt
* non-PNG file we want to fail loudly on anyway).
*/
function readHead(filePath: string, n: number): Buffer {
const buf = Buffer.alloc(n);
const fd = openSync(filePath, 'r');
try {
readSync(fd, buf, 0, n, 0);
} finally {
closeSync(fd);
}
return buf;
}
/** Read big-endian uint32 at offset; ISO/IEC 15948 §7 mandates network byte order. */
function readU32BE(buf: Buffer, offset: number): number {
return buf.readUInt32BE(offset);
}
describe('Plan 01-12: icons rasterized from Loom mark (RGBA, 8-bit, clearing imageUtil FLOORs)', () => {
for (const spec of ICON_SPECS) {
const iconPath = resolvePath(process.cwd(), `icons/icon${spec.size}.png`);
describe(`icons/icon${spec.size}.png (${spec.size}×${spec.size})`, () => {
it('exists at icons/iconN.png', () => {
expect(existsSync(iconPath), `Expected ${iconPath}`).toBe(true);
});
it(`size >= ${spec.sizeFloorBytes} bytes (Chrome imageUtil floor)`, () => {
const size = statSync(iconPath).size;
expect(
size,
`icon${spec.size}.png is ${size} B; need >= ${spec.sizeFloorBytes} B to clear Chrome imageUtil floor`,
).toBeGreaterThanOrEqual(spec.sizeFloorBytes);
});
it('PNG signature (first 8 bytes match \\x89PNG\\r\\n\\x1a\\n)', () => {
const head = readHead(iconPath, 8);
expect(
head.equals(PNG_SIGNATURE),
`Expected PNG signature; got ${head.toString('hex')}`,
).toBe(true);
});
it(`dimensions === ${spec.size}×${spec.size} (IHDR offset 16-23, big-endian uint32)`, () => {
const head = readHead(iconPath, 32);
const width = readU32BE(head, 16);
const height = readU32BE(head, 20);
expect(width, `icon${spec.size}.png width`).toBe(spec.size);
expect(height, `icon${spec.size}.png height`).toBe(spec.size);
});
it('color-type byte (IHDR offset 25) === 6 (RGBA — RED until Wave 2 overwrites placeholder)', () => {
const head = readHead(iconPath, 32);
const colorType = head[25];
expect(
colorType,
colorType === COLOR_TYPE_RGBA
? 'unreachable'
: `icon${spec.size}.png color-type=${colorType}; expected 6 (RGBA). ` +
`Placeholder PNGs are color-type 2 (RGB). RED until Wave 2 Task 1 ` +
`rasterizes via rsvg-convert (default output is 8-bit/color RGBA per RESEARCH §3).`,
).toBe(COLOR_TYPE_RGBA);
});
});
}
});