import { describe, it, expect } from 'vitest' import { scanReducer, INITIAL_SCAN_STATE, type ScanState } from '../src/scan/reducer.js' import type { Candidate, Item, ReleasePreview } from '../src/types.js' const candidate: Candidate = { id: 1001, artist: 'The Cinematic Orchestra', title: 'Motion', year: 1999, formats: ['CD'], labels: ['Ninja Tune'], country: 'UK', catno: 'ZENCD012', thumbUrl: 'https://img/x.jpg', } const preview: ReleasePreview = { release: { ...candidate, genres: [], tracklist: [], coverUrl: null, barcodes: ['5021592210629'] }, duplicate: false, ripMatch: 'not_ripped', matchCandidates: [], } const item: Item = { id: 1, discogsReleaseId: 1001, title: 'Motion', artist: 'The Cinematic Orchestra', year: 1999, formats: ['CD'], genres: [], labels: ['Ninja Tune'], tracklist: [], catno: 'ZENCD012', country: 'UK', artworkUrl: null, barcodes: ['5021592210629'], dateAdded: '2026-08-29', ripOverride: null, ripStatus: 'not_ripped', } function stateOf(phase: ScanState['phase']): ScanState { let state = INITIAL_SCAN_STATE const steps: Parameters[1][] = [ { type: 'DETECT', code: '5021592210629' }, { type: 'CANDIDATES', code: '5021592210629', candidates: [candidate] }, { type: 'SELECT', candidate }, { type: 'PREVIEW', preview }, { type: 'ADD_START' }, { type: 'ADDED', item }, ] const order: ScanState['phase'][] = ['scan', 'looking', 'candidates', 'confirm', 'confirm', 'confirm', 'added'] const idx = order.indexOf(phase) if (idx < 0) return state for (let i = 1; i <= idx; i++) state = scanReducer(state, steps[i - 1]!) return state } describe('scanReducer', () => { it('DETECT from scan → looking', () => { const next = scanReducer(INITIAL_SCAN_STATE, { type: 'DETECT', code: '123' }) expect(next).toEqual({ phase: 'looking', code: '123' }) }) it('DETECT is ignored unless scanning (prevents duplicate lookups)', () => { const looking = stateOf('looking') expect(scanReducer(looking, { type: 'DETECT', code: '999' })).toBe(looking) }) it('CANDIDATES from looking → candidates', () => { const next = scanReducer(stateOf('looking'), { type: 'CANDIDATES', code: '5021592210629', candidates: [candidate], }) expect(next.phase).toBe('candidates') }) it('NOT_FOUND from looking → error not_found', () => { const next = scanReducer(stateOf('looking'), { type: 'NOT_FOUND', code: '123' }) expect(next).toEqual({ phase: 'error', kind: 'not_found', code: '123' }) }) it('ERROR maps kinds', () => { const next = scanReducer(stateOf('looking'), { type: 'ERROR', kind: 'no_discogs_token', code: '123' }) expect(next).toEqual({ phase: 'error', kind: 'no_discogs_token', code: '123' }) }) it('SELECT from candidates → confirm with candidate, no preview yet', () => { const next = scanReducer(stateOf('candidates'), { type: 'SELECT', candidate }) expect(next.phase).toBe('confirm') if (next.phase === 'confirm') { expect(next.candidate).toBe(candidate) expect(next.preview).toBeNull() expect(next.adding).toBe(false) expect(next.matchAlbumId).toBeNull() } }) it('PREVIEW fills the confirm phase', () => { const next = scanReducer(stateOf('confirm'), { type: 'PREVIEW', preview }) if (next.phase === 'confirm') expect(next.preview).toBe(preview) else throw new Error('expected confirm') }) it('SET_MATCH / CLEAR_MATCH only in confirm', () => { const confirm = stateOf('confirm') const set = scanReducer(confirm, { type: 'SET_MATCH', albumId: 7 }) if (set.phase === 'confirm') expect(set.matchAlbumId).toBe(7) const cleared = scanReducer(set, { type: 'CLEAR_MATCH' }) if (cleared.phase === 'confirm') expect(cleared.matchAlbumId).toBeNull() expect(scanReducer(stateOf('scan'), { type: 'SET_MATCH', albumId: 7 })).toBe(stateOf('scan')) }) it('ADD_START → ADDED', () => { const confirm = scanReducer(stateOf('confirm'), { type: 'PREVIEW', preview }) const starting = scanReducer(confirm, { type: 'ADD_START' }) if (starting.phase === 'confirm') expect(starting.adding).toBe(true) const added = scanReducer(starting, { type: 'ADDED', item }) expect(added).toEqual({ phase: 'added', item }) }) it('ADD_ERROR stores the message without leaving confirm', () => { const confirm = scanReducer(stateOf('confirm'), { type: 'PREVIEW', preview }) const starting = scanReducer(confirm, { type: 'ADD_START' }) const next = scanReducer(starting, { type: 'ADD_ERROR', message: 'duplicate' }) if (next.phase === 'confirm') { expect(next.adding).toBe(false) expect(next.addError).toBe('duplicate') } else { throw new Error('expected confirm') } }) it('RESET returns to scan from anywhere', () => { for (const phase of ['looking', 'candidates', 'confirm', 'added', 'error'] as const) { expect(scanReducer(stateOf(phase), { type: 'RESET' })).toEqual({ phase: 'scan' }) } }) })