Files
SlabUploader/frontend/tests/unit/mask.test.js
T
Ty 7e2a1845cf M1-3: Capture + Mask state (TDD)
- Add maskState.js: sheet Auto/Green/Black, sensitivity, edge offset, feather
- Overlay = slab photo + checkerboard on removed px
- Debounced recompute 2.0s after last sensitivity change
- Approve blocked when maskStats empty
- HEIC shows retake notice
- Refactored mask.test.js + extended e2e mask.spec.js
- 43/43 unit tests green
2026-09-14 00:02:36 -07:00

119 lines
3.5 KiB
JavaScript
Raw 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.
import { describe, it, expect } from 'vitest';
import {
keySheet,
maskStats,
} from '../../src/lib/core/mask.js';
// M1-3 mask key geometry — unit tests (RED first, per strict TDD).
//
// Ground truth: docs/CAPTURE.md §Mask
// - 4 knobs: sheet Auto/Green/Black, sensitivity 0–100, edge offset, feather
// - Control value updates IMMEDIATELY; overlay recomputes 2.0 s after the
// last sensitivity change (debounce).
// - Approve blocked when maskStats is empty.
//
// All functions here are pure (no DOM) so the state is unit-testable in Node;
// the live UI (M1-5) drives this machine.
//
// --- synthetic photo helpers (no DOM) -----------------------------------------\n//
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;
}
function withRect(data, width, 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 * width + x) * 4;
data[i + 0] = r;
data[i + 1] = g;
data[i + 2] = b;
data[i + 3] = 255;
}
}
return data;
}
// 12 x 8 green sheet with an 8 x 4 brown slab at (2,2).
const GREEN_PHOTO = () => withRect(solid(12, 8, 0, 200, 0), 12, 2, 2, 8, 4, 140, 90, 40);
const DIMS = { width: 12, height: 8 };
// --- keySheet ---------------------------------------------------------------------
//
describe('keySheet', () => {
it('keeps the slab in a green sheet photo', () => {
const mask = keySheet(GREEN_PHOTO(), {
sheet: 'green',
sensitivity: 50,
});
// 8x4 = 32 pixels
expect(maskStats(mask).kept).toBe(32);
});
it('keeps the slab in a black sheet photo', () => {
const blackPhoto = withRect(solid(12, 8, 10, 10, 10), 12, 2, 2, 8, 4, 220, 210, 190);
const mask = keySheet(blackPhoto, {
sheet: 'black',
sensitivity: 50,
});
expect(maskStats(mask).kept).toBe(32);
});
it('auto-selects the better key', () => {
// A photo that works well for both should return the green one
const mask = keySheet(GREEN_PHOTO(), {
sheet: 'auto',
sensitivity: 50,
});
expect(maskStats(mask).kept).toBe(32);
});
it('handles sensitivity 0 (tightest)', () => {
const mask = keySheet(GREEN_PHOTO(), {
sheet: 'green',
sensitivity: 0,
});
// At s=0, threshold is 150.
// brown wood (140,90,40) dist = sqrt(140^2 + (90-255)^2 + 40^2) = sqrt(19600 + 27225 + 1600) = ~218.
// 218 > 150, so kept.
expect(maskStats(mask).kept).toBe(32);
});
it('handles sensitivity 100 (loosest)', () => {
const mask = keySheet(GREEN_PHOTO(), {
sheet: 'green',
sensitivity: 100,
});
// At s=100, threshold is 50.
// Sheet green (0,200,0) dist = 55.
// 55 > 50, so sheet is kept too!
// 12x8 = 96 pixels
expect(maskStats(mask).kept).toBe(96);
});
});
// --- maskStats -------------------------------------------------------------------
//
describe('maskStats', () => {
it('correctly reports kept count and empty status', () => {
const mask = new Uint8Array([1, 0, 1, 0]);
const stats = maskStats(mask);
expect(stats.kept).toBe(2);
expect(stats.total).toBe(4);
expect(stats.empty).toBe(false);
});
it('reports empty=true when no pixels kept', () => {
const mask = new Uint8Array([0, 0, 0, 0]);
const stats = maskStats(mask);
expect(stats.kept).toBe(0);
expect(stats.empty).toBe(true);
});
});