test(photos): Photos-ready lineup order + error slots (#64)
Failing TDD for M2 Photos-ready end screen: pure buildPhotosReadyLineup orders clean calibration inventory PNG → dimension-overlay copy → other batch inventory PNGs in capture order (skipping cal null), surfaces batchApply empty-mask/floor errors, and MeasurePanel must show Photos ready with Milestone 1 numbers (not source thumbs).
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
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',
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user