import { afterEach, describe, expect, it, vi } from 'vitest'; import { cancelDashcamCapture, readDashcam, requestDashcamCapture, setDashcamOpen, setDashcamPointer, takeDashcamCapture, } from '@/journey/dashcam-feed'; import { DASHCAM_LENS, createDashcamLens, driftTarget } from '@/scene/world/dashcam-lens'; import { dashcamClock, dashcamTitle, frameBottomLeft, frameTopLeft, postcardCredit, postcardFilename, routePercent, type DashcamTelemetry, } from '@/lib/dashcam-postcard'; import { attachMusicPulse, createPulseTap, readSwitchboard, setMusicPulseAudible } from '@/audio/music-pulse'; import { playShutterClick } from '@/audio/switchboard-click'; afterEach(() => { setDashcamOpen(false, false); setMusicPulseAudible(false); attachMusicPulse(null); }); describe('dashcam feed', () => { it('opens with drift only when asked, and resets the pointer on every change', () => { setDashcamOpen(true, true); setDashcamPointer(0.5, -0.25, 100); expect(readDashcam()).toMatchObject({ open: true, drift: true, px: 0.5, py: -0.25, movedAt: 100 }); setDashcamOpen(false, true); expect(readDashcam()).toMatchObject({ open: false, drift: false, px: 0, py: 0 }); setDashcamOpen(true, false); expect(readDashcam().drift).toBe(false); }); it('clamps the pointer to [-1, 1]', () => { setDashcamOpen(true, true); setDashcamPointer(4, -9, 0); expect(readDashcam()).toMatchObject({ px: 1, py: -1 }); }); it('hands a capture to the scene exactly once', () => { const resolve = vi.fn(); requestDashcamCapture(resolve); expect(takeDashcamCapture()).toBe(resolve); expect(takeDashcamCapture()).toBeNull(); }); it('drops a queued capture on cancel and on close', () => { requestDashcamCapture(vi.fn()); cancelDashcamCapture(); expect(takeDashcamCapture()).toBeNull(); setDashcamOpen(true, false); requestDashcamCapture(vi.fn()); setDashcamOpen(false, false); expect(takeDashcamCapture()).toBeNull(); }); }); describe('dashcam lens', () => { const feed = (overrides: Partial> = {}) => ({ open: true, drift: true, px: 1, py: 1, movedAt: 0, ...overrides, }); it('targets the pointer while open with drift, centre otherwise', () => { expect(driftTarget(feed({ px: 0.4, py: -0.6 }), 10)).toEqual({ x: 0.4, y: -0.6 }); expect(driftTarget(feed({ px: 7, py: -7 }), 10)).toEqual({ x: 1, y: -1 }); expect(driftTarget(feed({ open: false }), 10)).toEqual({ x: 0, y: 0 }); expect(driftTarget(feed({ drift: false }), 10)).toEqual({ x: 0, y: 0 }); }); it('returns to centre after 3 s without pointer movement', () => { expect(driftTarget(feed(), DASHCAM_LENS.idleMs)).toEqual({ x: 1, y: 1 }); expect(driftTarget(feed(), DASHCAM_LENS.idleMs + 1)).toEqual({ x: 0, y: 0 }); }); it('eases the drift and never exceeds +-1.6 x and +-0.5 target height', () => { const lens = createDashcamLens(); const first = { ...lens.update(feed(), 1 / 60, 0) }; expect(first.x).toBeGreaterThan(0); expect(first.x).toBeLessThan(DASHCAM_LENS.driftX * 0.1); let state = first; for (let i = 0; i < 600; i += 1) state = lens.update(feed(), 1 / 60, 0); expect(state.x).toBeCloseTo(1.6, 3); expect(state.targetY).toBeCloseTo(-0.5, 3); for (let i = 0; i < 600; i += 1) state = lens.update(feed({ px: -1, py: -1 }), 1 / 60, 0); expect(state.x).toBeGreaterThanOrEqual(-1.6); expect(state.targetY).toBeLessThanOrEqual(0.5); }); it('closes the letterbox to 0.16 over 400 ms and opens it again', () => { const lens = createDashcamLens(); expect(lens.update(feed(), 0.2, 0).letterbox).toBeCloseTo(0.08, 5); expect(lens.update(feed(), 0.2, 0).letterbox).toBeCloseTo(0.16, 5); expect(lens.update(feed({ open: false }), 0.2, 0).letterbox).toBeCloseTo(0.08, 5); expect(lens.update(feed({ open: false }), 0.2, 0).letterbox).toBe(0); }); it('snaps the bars and holds centre without drift (touch, low tier)', () => { const lens = createDashcamLens(); const open = lens.update(feed({ drift: false }), 1 / 60, 0); expect(Math.abs(open.x) + Math.abs(open.targetY)).toBe(0); expect(open.letterbox).toBeCloseTo(0.16, 10); expect(lens.update(feed({ open: false, drift: false }), 1 / 60, 0).letterbox).toBe(0); }); }); describe('dashcam telemetry', () => { const telemetry: DashcamTelemetry = { sectionId: 'infrastructure', sectionLabel: 'Infrastructure', chapter: 'pre-dawn', exit: '04', routePercent: 45, clock: '03:51', name: 'Luca Pîslaru', city: 'Bucharest', }; it('formats the document title', () => { expect(dashcamTitle(telemetry)).toBe('DASHCAM · 45% · INFRASTRUCTURE · 03:51'); }); it('formats the frame plates and the credit', () => { expect(frameTopLeft(telemetry)).toBe('DASHCAM · EXIT 04'); expect(frameBottomLeft(telemetry)).toBe('ROUTE 45% · INFRASTRUCTURE'); expect(postcardCredit(telemetry.name, telemetry.city)).toBe('NIGHT SHIFT · LUCA PÎSLARU · BUCHAREST'); }); it('rounds and clamps route progress on the tab title\'s 5% step', () => { expect(routePercent(0.474)).toBe(45); expect(routePercent(0.476)).toBe(50); expect(routePercent(1.4)).toBe(100); expect(routePercent(-1)).toBe(0); expect(routePercent(Number.NaN)).toBe(0); }); it('names the postcard after the section and the clock', () => { expect(postcardFilename('infrastructure', '03:51')).toBe('night-shift-infrastructure-0351.png'); expect(postcardFilename('selected-builds', '23:05')).toBe('night-shift-selected-builds-2305.png'); expect(postcardFilename('', '7:5')).toBe('night-shift-route-0075.png'); }); it('uses the live Bucharest clock outside the maintenance window', () => { const now = new Date('2026-01-15T01:51:00Z'); expect(dashcamClock('profile', null, now)).toBe('03:51'); expect(dashcamClock('infrastructure', null, now)).toBe('03:51'); }); }); describe('switchboard click', () => { it('stays silent and creates nothing without an audible soundtrack', () => { expect(readSwitchboard()).toBeNull(); expect(playShutterClick()).toBe(false); }); it('plays into the mix bus at the switchboard gain when audible', () => { const gains: { gain: { setValueAtTime: ReturnType; linearRampToValueAtTime: ReturnType; exponentialRampToValueAtTime: ReturnType }; connect: ReturnType; disconnect: ReturnType }[] = []; const context = { state: 'running', sampleRate: 1000, currentTime: 2, createGain: () => { const node = { gain: { value: 1, setValueAtTime: vi.fn(), linearRampToValueAtTime: vi.fn(), exponentialRampToValueAtTime: vi.fn() }, connect: vi.fn(), disconnect: vi.fn(), context: undefined as unknown, }; node.context = context; gains.push(node); return node; }, createAnalyser: () => { throw new Error('no analyser'); }, createBuffer: (_c: number, length: number) => ({ getChannelData: () => new Float32Array(length) }), createBufferSource: () => ({ connect: vi.fn(), disconnect: vi.fn(), start: vi.fn(), stop: vi.fn(), buffer: null, onended: null }), }; const destination = {} as AudioNode; createPulseTap(context as unknown as BaseAudioContext, destination); expect(readSwitchboard()).toBeNull(); setMusicPulseAudible(true); const bus = readSwitchboard(); expect(bus).toBe(gains[0]); expect(playShutterClick()).toBe(true); const envelope = gains[1]!; expect(envelope.connect).toHaveBeenCalledWith(bus); expect(envelope.gain.linearRampToValueAtTime).toHaveBeenCalledWith(0.18, expect.any(Number)); attachMusicPulse(null); expect(readSwitchboard()).toBeNull(); }); });