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>
117 lines
4.5 KiB
TypeScript
117 lines
4.5 KiB
TypeScript
// 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);
|
||
});
|
||
});
|
||
}
|
||
});
|