import assert from 'node:assert/strict'; import { test } from 'node:test'; import { keptInOrder, moveItem, missingMinimums, lineupValid, photoRole, roleLabel, resolvePhotoUrl, type LineupItem, } from './lineup'; const p = (id: string, role: LineupItem['role'], kept = true): LineupItem => ({ id, role, isCalibration: role === 'calibration', kept, }); test('lineup: keptInOrder returns kept photos in their current order', () => { const items = [p('cal', 'calibration'), p('ov', 'overlay', false), p('b1', 'batch')]; assert.deepEqual(keptInOrder(items).map((x) => x.id), ['cal', 'b1']); }); test('lineup: leftmost kept is the primary', () => { const items = [p('b1', 'batch'), p('cal', 'calibration'), p('ov', 'overlay')]; // After a drag the owner re-ordered; primary follows position, not role. assert.equal(keptInOrder(items)[0].id, 'b1'); }); test('lineup: primary auto-promotes when the leftmost kept is removed', () => { const items = [p('cal', 'calibration', false), p('ov', 'overlay'), p('b1', 'batch')]; assert.equal(keptInOrder(items)[0].id, 'ov'); }); test('lineup: moveItem reorders (drag-to-front) and is a no-op on same index', () => { const a = ['x', 'y', 'z']; assert.deepEqual(moveItem(a, 2, 0), ['z', 'x', 'y']); assert.deepEqual(moveItem(a, 0, 0), ['x', 'y', 'z']); // Out-of-range is a no-op, never throws. assert.deepEqual(moveItem(a, 0, 5), ['x', 'y', 'z']); // Returns a new array, does not mutate the input. const before = a.slice(); moveItem(a, 0, 2); assert.deepEqual(a, before); }); test('lineup: minimum requires a clean AND an overlay copy (REVIEW-5)', () => { assert.deepEqual(missingMinimums([p('cal', 'calibration'), p('ov', 'overlay')]), []); assert.deepEqual(missingMinimums([p('cal', 'calibration')]), ['overlay']); assert.deepEqual(missingMinimums([p('ov', 'overlay')]), ['calibration']); assert.deepEqual(missingMinimums([p('b1', 'batch')]), ['calibration', 'overlay']); assert.deepEqual(missingMinimums([]), ['calibration', 'overlay']); }); test('lineup: batch-only is not valid; cal+overlay+batch is', () => { assert.equal(lineupValid([p('b1', 'batch')]), false); assert.equal(lineupValid([p('cal', 'calibration'), p('ov', 'overlay'), p('b1', 'batch')]), true); }); test('lineup: missingMinimums only counts KEPT photos', () => { // Both roles present in the array, but the cal is not kept. assert.deepEqual(missingMinimums([p('cal', 'calibration', false), p('ov', 'overlay')]), ['calibration']); }); test('lineup: photoRole is the single role rule (cal > overlay > batch)', () => { assert.equal(photoRole('cal', 'cal', true), 'calibration'); assert.equal(photoRole('ov', 'cal', true), 'overlay'); assert.equal(photoRole('b1', 'cal', false), 'batch'); // Calibration wins even if it carries a processed copy. assert.equal(photoRole('cal', 'cal', true), 'calibration'); // No calibration selected: processed photos are overlays. assert.equal(photoRole('ov', '', true), 'overlay'); assert.equal(photoRole('b1', '', false), 'batch'); }); test('lineup: roleLabel is owner-facing', () => { assert.match(roleLabel('calibration'), /clean/); assert.match(roleLabel('overlay'), /overlay/); }); test('lineup: resolvePhotoUrl turns an in-memory blob into a blob: URL', () => { const blob = new Blob(['png-bytes'], { type: 'image/png' }); const fake = (b: Blob) => `blob:fake-${b.size}`; assert.equal(resolvePhotoUrl(blob, '/api/endpoints/photo', fake), 'blob:fake-9'); // Empty/absent blob falls back to the stored endpoint. assert.equal(resolvePhotoUrl(new Blob(), '/api/endpoints/photo', fake), '/api/endpoints/photo'); assert.equal(resolvePhotoUrl(null, '/api/endpoints/photo', fake), '/api/endpoints/photo'); });