File
Blob: worker/tests/fixtures/radio.tsx
| 1 | import { useRef, useState } from 'react'; |
| 2 | import { initialState } from '@/client/radio/session-state.ts'; |
| 3 | import type { SessionState, SignalBuffer } from '@/client/radio/session-state.ts'; |
| 4 | import type { RadioActions } from '@/client/radio/use-radio.ts'; |
| 5 | |
| 6 | const track = { title: 'Morning Radio', artist: 'Example Artist', durationMs: 240000 }; |
| 7 | const room = { |
| 8 | online: true, |
| 9 | viewers: 1, |
| 10 | controller: null, |
| 11 | generation: 'fixture', |
| 12 | track, |
| 13 | nowPlaying: { revision: 1, trackIndex: 0, trackCount: 4, track }, |
| 14 | }; |
| 15 | |
| 16 | declare global { |
| 17 | interface Window { |
| 18 | exhibitFixture: { patch: (patch: Partial<SessionState>) => void; calls: () => string[] }; |
| 19 | } |
| 20 | } |
| 21 | |
| 22 | // Replaces only the React/session boundary in browser tests; the real App renders. |
| 23 | export function useFixtureRadio() { |
| 24 | const [state, setState] = useState<SessionState>(() => ({ |
| 25 | ...initialState(), |
| 26 | auth: 'ready', |
| 27 | room, |
| 28 | nowPlaying: room.nowPlaying, |
| 29 | message: 'Board online. Start listening.', |
| 30 | })); |
| 31 | const calls = useRef<string[]>([]); |
| 32 | const signal = useRef<SignalBuffer>({ at: 0, count: 0 }); |
| 33 | const audioRef = useRef<HTMLAudioElement>(null); |
| 34 | const patch = (next: Partial<SessionState>) => setState((previous) => ({ ...previous, ...next })); |
| 35 | window.exhibitFixture = { patch, calls: () => calls.current }; |
| 36 | const actions: RadioActions = { |
| 37 | login: async () => {}, |
| 38 | connect: async () => { |
| 39 | calls.current.push('connect'); |
| 40 | patch({ phase: 'connecting', message: 'Opening data channels…' }); |
| 41 | }, |
| 42 | disconnect: async () => { |
| 43 | calls.current.push('disconnect'); |
| 44 | patch({ |
| 45 | phase: 'idle', |
| 46 | viewerId: undefined, |
| 47 | telemetry: undefined, |
| 48 | paused: undefined, |
| 49 | led: undefined, |
| 50 | busy: false, |
| 51 | audioBlocked: false, |
| 52 | room, |
| 53 | ack: { state: 'idle', text: '', at: 0 }, |
| 54 | message: 'Disconnected. Shared playback is unchanged.', |
| 55 | }); |
| 56 | }, |
| 57 | toggleAudio: async () => { |
| 58 | calls.current.push('audio'); |
| 59 | patch({ audioBlocked: false, muted: !state.muted }); |
| 60 | }, |
| 61 | setVolume: (volume) => patch({ volume }), |
| 62 | toggleControl: async () => { |
| 63 | calls.current.push('control'); |
| 64 | patch({ busy: true }); |
| 65 | }, |
| 66 | send: (command) => { |
| 67 | calls.current.push(JSON.stringify(command)); |
| 68 | patch({ ack: { state: 'pending', text: 'Waiting for board confirmation…', at: 10000 } }); |
| 69 | }, |
| 70 | }; |
| 71 | return { state, signal: signal.current, actions, audioRef }; |
| 72 | } |