import { describe, it, expect } from 'vitest'; import { keySheet, maskStats } from '$lib/core/mask.js'; // M1-3 mask key geometry — unit tests (RED first, per strict TDD). // Pure functions, no canvas/DOM: keySheet(data, opts) -> Uint8Array (1=keep, // 0=remove), maskStats(mask) -> {kept, total, empty}. // // Ground truth is docs/CAPTURE.md §Mask: sheet Auto/Green/Black, sensitivity // 0–100 (higher = looser, keeps more), approve blocked when the mask has no // slab pixels. // --- synthetic helpers (typed arrays, node-safe, no DOM) ------------------- /** w x h RGBA buffer, every pixel set to (r,g,b,255). */ function solid(w, h, r, g, b) { const data = new Uint8ClampedArray(w * h * 4); for (let i = 0; i < w * h; i++) { data[i * 4 + 0] = r; data[i * 4 + 1] = g; data[i * 4 + 2] = b; data[i * 4 + 3] = 255; } return data; } /** Solid canvas with a centered filled rect painted over it. */ function withRect(canvas, x0, y0, w, h, r, g, b) { for (let y = y0; y < y0 + h; y++) { for (let x = x0; x < x0 + w; x++) { const i = (y * canvas.width + x) * 4; canvas.data[i + 0] = r; canvas.data[i + 1] = g; canvas.data[i + 2] = b; canvas.data[i + 3] = 255; } } return canvas; } /** 12 x 8 RGBA: green sheet field + 8 x 4 brown rectangle at (2,2). */ function greenFieldWithBrownSlab() { const canvas = { width: 12, height: 8, data: solid(12, 8, 0, 200, 0) }; return withRect(canvas, 2, 2, 8, 4, 140, 90, 40); } /** 12 x 8 RGBA: near-black sheet + 8 x 4 bright slab at (2,2). */ function blackFieldWithBrightSlab() { const canvas = { width: 12, height: 8, data: solid(12, 8, 10, 10, 10) }; return withRect(canvas, 2, 2, 8, 4, 220, 210, 190); } /** Mask sum over a rectangular region (row-major). */ function regionSum(mask, w, x0, y0, rw, rh) { let s = 0; for (let y = y0; y < y0 + rh; y++) for (let x = x0; x < x0 + rw; x++) s += mask[y * w + x]; return s; } const GREEN = { sheet: 'green', sensitivity: 50 }; const BLACK = { sheet: 'black', sensitivity: 50 }; const rect = { x0: 2, y0: 2, rw: 8, rh: 4 }; // --- green key --------------------------------------------------------------- describe('keySheet: green sheet', () => { it('returns one entry per pixel', () => { const mask = keySheet(greenFieldWithBrownSlab().data, GREEN); expect(mask).toBeInstanceOf(Uint8Array); expect(mask.length).toBe(12 * 8); }); it('removes the strongly green field and keeps the brown slab', () => { const mask = keySheet(greenFieldWithBrownSlab().data, GREEN); // Outside the rect the field is uniformly green -> fully removed. expect(regionSum(mask, 12, 0, 0, 12, 2)).toBe(0); // top strip expect(regionSum(mask, 12, 0, 6, 12, 2)).toBe(0); // bottom strip expect(regionSum(mask, 12, 0, 2, 2, 4)).toBe(0); // left strip expect(regionSum(mask, 12, 10, 2, 2, 4)).toBe(0); // right strip // The brown rect is fully kept. expect(regionSum(mask, 12, rect.x0, rect.y0, rect.rw, rect.rh)).toBe(32); }); }); // --- black key --------------------------------------------------------------- describe('keySheet: black sheet', () => { it('removes the near-black field and keeps the bright slab', () => { const mask = keySheet(blackFieldWithBrightSlab().data, BLACK); expect(regionSum(mask, 12, 0, 0, 12, 2)).toBe(0); expect(regionSum(mask, 12, 0, 6, 12, 2)).toBe(0); expect(regionSum(mask, 12, 0, 2, 2, 4)).toBe(0); expect(regionSum(mask, 12, 10, 2, 2, 4)).toBe(0); expect(regionSum(mask, 12, rect.x0, rect.y0, rect.rw, rect.rh)).toBe(32); }); }); // --- sensitivity ------------------------------------------------------------- describe('keySheet: sensitivity', () => { it('higher sensitivity keeps >= as many pixels (green, monotone)', () => { const data = greenFieldWithBrownSlab().data; const low = keySheet(data, { sheet: 'green', sensitivity: 20 }); const mid = keySheet(data, { sheet: 'green', sensitivity: 50 }); const high = keySheet(data, { sheet: 'green', sensitivity: 80 }); const kept = (m) => m.reduce((a, b) => a + b, 0); expect(kept(mid)).toBeGreaterThanOrEqual(kept(low)); expect(kept(high)).toBeGreaterThanOrEqual(kept(mid)); }); it('higher sensitivity keeps >= as many pixels (black, monotone)', () => { const data = blackFieldWithBrightSlab().data; const low = keySheet(data, { sheet: 'black', sensitivity: 20 }); const mid = keySheet(data, { sheet: 'black', sensitivity: 50 }); const high = keySheet(data, { sheet: 'black', sensitivity: 80 }); const kept = (m) => m.reduce((a, b) => a + b, 0); expect(kept(mid)).toBeGreaterThanOrEqual(kept(low)); expect(kept(high)).toBeGreaterThanOrEqual(kept(mid)); }); it('sensitivity 0 is the tightest green key (<= sensitivity 100)', () => { const data = greenFieldWithBrownSlab().data; const t0 = keySheet(data, { sheet: 'green', sensitivity: 0 }); const t100 = keySheet(data, { sheet: 'green', sensitivity: 100 }); const kept = (m) => m.reduce((a, b) => a + b, 0); expect(kept(t100)).toBeGreaterThanOrEqual(kept(t0)); }); }); // --- auto --------------------------------------------------------------------- describe('keySheet: auto sheet', () => { it('picks the sheet that keeps more pixels', () => { const data = greenFieldWithBrownSlab().data; const auto = keySheet(data, { sheet: 'auto', sensitivity: 50 }); const greenKept = keySheet(data, GREEN).reduce((a, b) => a + b, 0); const blackKept = keySheet(data, BLACK).reduce((a, b) => a + b, 0); expect(auto.reduce((a, b) => a + b, 0)).toBe( Math.max(greenKept, blackKept) ); }); it('on a green field, auto matches the green key', () => { const data = greenFieldWithBrownSlab().data; const auto = keySheet(data, { sheet: 'auto', sensitivity: 50 }); const green = keySheet(data, GREEN); for (let i = 0; i < auto.length; i++) expect(auto[i]).toBe(green[i]); }); }); // --- maskStats ----------------------------------------------------------------- describe('maskStats', () => { it('empty = true when no pixel is kept (approve is blocked)', () => { const mask = new Uint8Array([0, 0, 0, 0]); expect(maskStats(mask)).toEqual({ kept: 0, total: 4, empty: true }); }); it('empty = false when any pixel is kept', () => { const mask = new Uint8Array([0, 1, 0, 1]); expect(maskStats(mask)).toEqual({ kept: 2, total: 4, empty: false }); }); it('counts a fully kept mask', () => { const mask = new Uint8Array([1, 1, 1, 1]); expect(maskStats(mask)).toEqual({ kept: 4, total: 4, empty: false }); }); it('reports the total from the keySheet output', () => { const mask = keySheet(greenFieldWithBrownSlab().data, GREEN); expect(maskStats(mask).total).toBe(96); expect(maskStats(mask).empty).toBe(false); }); });