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>
This commit is contained in:
2026-05-19 21:56:08 +02:00
parent 3fe018beb9
commit 34a9ce10d4
6 changed files with 609 additions and 0 deletions

View File

@@ -0,0 +1,116 @@
// 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);
});
});
}
});