import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { test } from 'node:test'; import type { BatchPhotoResult } from './capture.types'; import { buildPhotosReadyLineup, type PhotosReadyLineupItem, } from './photosReadyLineup'; const here = dirname(fileURLToPath(import.meta.url)); function png(label: string): Blob { return new Blob([label], { type: 'image/png' }); } function jpeg(label: string): Blob { return new Blob([label], { type: 'image/jpeg' }); } function roles(items: PhotosReadyLineupItem[]): string[] { return items.map((it) => it.kind === 'png' ? `png:${it.role}` : `error:${it.batchIndex}`, ); } // --- pure helper: lineup order + error slots (#64) --- test('test(photos): lineup is clean cal → overlay → other batch PNGs in capture order (#64)', () => { const clean = png('clean-cal'); const overlay = png('overlay-cal'); const sib0 = png('sib-0'); const sib2 = png('sib-2'); const batchApply: BatchPhotoResult[] = [ { status: 'ok', inventoryPng: sib0 }, null, // calibration at index 1 { status: 'ok', inventoryPng: sib2 }, ]; const lineup = buildPhotosReadyLineup({ inventoryPng: clean, overlayPng: overlay, batchApply, }); assert.deepEqual(roles(lineup), [ 'png:calibration', 'png:overlay', 'png:batch', 'png:batch', ]); assert.equal(lineup[0].kind, 'png'); if (lineup[0].kind === 'png') { assert.equal(lineup[0].blob, clean); assert.equal(lineup[0].role, 'calibration'); } assert.equal(lineup[1].kind, 'png'); if (lineup[1].kind === 'png') { assert.equal(lineup[1].blob, overlay); assert.equal(lineup[1].role, 'overlay'); } assert.equal(lineup[2].kind, 'png'); if (lineup[2].kind === 'png') { assert.equal(lineup[2].blob, sib0); assert.equal(lineup[2].batchIndex, 0); } assert.equal(lineup[3].kind, 'png'); if (lineup[3].kind === 'png') { assert.equal(lineup[3].blob, sib2); assert.equal(lineup[3].batchIndex, 2); } }); test('test(photos): single-photo batch lineup is clean + overlay only (#64)', () => { const clean = png('only-clean'); const overlay = png('only-overlay'); const lineup = buildPhotosReadyLineup({ inventoryPng: clean, overlayPng: overlay, batchApply: [null], }); assert.deepEqual(roles(lineup), ['png:calibration', 'png:overlay']); assert.ok(lineup.every((it) => it.kind === 'png')); }); test('test(photos): batchApply errors become clear error slots (empty mask / floor) (#64)', () => { const clean = png('clean'); const overlay = png('overlay'); const ok = png('ok-sib'); const batchApply: BatchPhotoResult[] = [ null, { status: 'error', message: 'empty mask: no slab pixels detected' }, { status: 'ok', inventoryPng: ok }, { status: 'error', message: 'PNG size floor: shorter side is below 800 px' }, ]; const lineup = buildPhotosReadyLineup({ inventoryPng: clean, overlayPng: overlay, batchApply, }); assert.deepEqual(roles(lineup), [ 'png:calibration', 'png:overlay', 'error:1', 'png:batch', 'error:3', ]); const err1 = lineup[2]; assert.equal(err1.kind, 'error'); if (err1.kind === 'error') { assert.match(err1.message, /empty mask/i); assert.equal(err1.batchIndex, 1); } const err3 = lineup[4]; assert.equal(err3.kind, 'error'); if (err3.kind === 'error') { assert.match(err3.message, /floor|800/i); } // Successful sibling still appears after cal+overlay, in capture order among siblings const okItem = lineup[3]; assert.equal(okItem.kind, 'png'); if (okItem.kind === 'png') { assert.equal(okItem.blob, ok); assert.equal(okItem.batchIndex, 2); } }); test('test(photos): lineup never uses source JPEG thumbs as inventory PNGs (#64)', () => { const clean = png('clean'); const overlay = png('overlay'); const thumb = jpeg('source-thumb'); // Dishonest proxy would pass source thumbs; helper must only emit image/png blobs for png slots const lineup = buildPhotosReadyLineup({ inventoryPng: clean, overlayPng: overlay, batchApply: [ null, { status: 'ok', inventoryPng: png('sib') }, ], }); for (const it of lineup) { if (it.kind === 'png') { assert.equal(it.blob.type, 'image/png', 'lineup PNG slots must be inventory PNGs'); assert.notEqual(it.blob, thumb); } } }); test('test(photos): skip clean cal when sourceBelowFloor; keep overlay + siblings (#64)', () => { const lineup = buildPhotosReadyLineup({ inventoryPng: png('below'), overlayPng: png('overlay'), batchApply: [null, { status: 'ok', inventoryPng: png('sib') }], sourceBelowFloor: true, }); assert.deepEqual(roles(lineup), ['png:overlay', 'png:batch']); }); test('test(photos): empty/missing results yield empty lineup (#64)', () => { assert.deepEqual(buildPhotosReadyLineup({}), []); assert.deepEqual( buildPhotosReadyLineup({ inventoryPng: null, overlayPng: null, batchApply: undefined }), [], ); }); // --- light UI / session wiring (#64) --- test('test(photos): MeasurePanel shows Photos ready lineup + M1 numbers + batch errors (#64)', () => { const panel = readFileSync(join(here, 'MeasurePanel.svelte'), 'utf8'); assert.match(panel, /Photos ready|photos.?ready/i, 'end screen title after Calculate'); assert.match(panel, /buildPhotosReadyLineup/, 'must use pure lineup helper'); assert.match(panel, /Widths/, 'M1 widths still visible'); assert.match(panel, /Sqft/, 'M1 sqft still visible'); assert.match(panel, /Bdft/, 'M1 bdft still visible'); assert.match(panel, /Scale/, 'M1 scale still visible'); // Dishonest proxies assert.ok( !/lineup[\s\S]{0,200}state\.source\.blob/.test(panel), 'must not lineup source thumbs', ); assert.match( panel, /error|batchApply|message/i, 'per-photo batchApply errors must surface in the panel', ); assert.match(panel, /Back/, 'Back must still work from Photos ready'); }); test('test(photos): App still routes measure results through Measure (or PhotosReady) with Back (#64)', () => { const app = readFileSync(join(here, '..', 'App.svelte'), 'utf8'); // Stay on Measure with results, or dedicated panel — either is fine assert.ok( /MeasurePanel/.test(app) || /PhotosReady/.test(app), 'App must render Photos-ready end screen', ); // Must not invent a Mask loop for the lineup assert.ok( !/\{\s*#each\s+[^}]*photos[^}]*\}[\s\S]*MaskPanel/i.test(app), 'App must not #each batch into Mask for Photos ready', ); });