import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { renderHook, act, waitFor } from '@testing-library/react' import { useCamera } from './useCamera' function fakeStream() { const tracks = [ { stop: vi.fn(), kind: 'video', getSettings: () => ({ deviceId: 'cam1' }) }, { stop: vi.fn(), kind: 'video', getSettings: () => ({ deviceId: 'cam1' }) }, ] return { _tracks: tracks, getTracks: () => tracks, getVideoTracks: () => tracks, } as unknown as MediaStream & { _tracks: { stop: ReturnType }[] } } describe('useCamera — track lifecycle', () => { let stream: ReturnType beforeEach(() => { stream = fakeStream() Object.defineProperty(navigator, 'mediaDevices', { configurable: true, value: { getUserMedia: vi.fn().mockResolvedValue(stream), enumerateDevices: vi.fn().mockResolvedValue([]) }, }) }) afterEach(() => vi.restoreAllMocks()) it('start() acquires a stream and stop() stops ALL tracks', async () => { const { result } = renderHook(() => useCamera()) await act(async () => { await result.current.start() }) await waitFor(() => expect(result.current.status).toBe('live')) act(() => result.current.stop()) for (const tr of stream._tracks) expect(tr.stop).toHaveBeenCalled() expect(result.current.status).toBe('idle') }) it('maps NotAllowedError to denied', async () => { const gum = navigator.mediaDevices.getUserMedia as ReturnType gum.mockRejectedValueOnce(Object.assign(new Error('no'), { name: 'NotAllowedError' })) const { result } = renderHook(() => useCamera()) await act(async () => { await result.current.start() }) await waitFor(() => expect(result.current.status).toBe('denied')) }) it('reports nocamera when getUserMedia is unavailable', async () => { Object.defineProperty(navigator, 'mediaDevices', { configurable: true, value: undefined }) const { result } = renderHook(() => useCamera()) await act(async () => { await result.current.start() }) expect(result.current.status).toBe('nocamera') }) })