import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { renderHook, waitFor } from '@testing-library/react' import { useBarcodeScanner, shouldEmit } from '../src/hooks/useBarcodeScanner.js' function fakeStream(): MediaStream { const tracks: MediaStreamTrack[] = [{ stop: vi.fn() } as unknown as MediaStreamTrack] return { getTracks: () => tracks } as unknown as MediaStream } function stubMediaDevices(impl: () => Promise) { vi.stubGlobal( 'navigator', Object.assign(Object.create(Object.getPrototypeOf(navigator)), navigator, { mediaDevices: { getUserMedia: impl }, }) ) } beforeEach(() => { vi.spyOn(HTMLMediaElement.prototype, 'play').mockResolvedValue() }) afterEach(() => { vi.unstubAllGlobals() vi.restoreAllMocks() }) describe('shouldEmit (cooldown dedup)', () => { it('emits a new code, suppresses a rapid repeat, emits after cooldown', () => { expect(shouldEmit(null, '123', 0, 1500)).toBe(true) expect(shouldEmit({ code: '123', at: 100 }, '123', 500, 1500)).toBe(false) expect(shouldEmit({ code: '123', at: 100 }, '456', 500, 1500)).toBe(true) expect(shouldEmit({ code: '123', at: 100 }, '123', 1700, 1500)).toBe(true) }) }) describe('useBarcodeScanner', () => { it('detects a barcode via BarcodeDetector and calls onDetect', async () => { const detections = [[], [{ rawValue: '5021592210629', format: 'ean_13' }]] let call = 0 class FakeDetector { constructor(_opts: { formats: string[] }) { expect(_opts.formats).toEqual(['ean_13', 'upc_a', 'ean_8']) } async detect() { return detections[Math.min(call++, 1)]! } } ;(window as unknown as { BarcodeDetector?: unknown }).BarcodeDetector = FakeDetector stubMediaDevices(() => Promise.resolve(fakeStream())) const onDetect = vi.fn() const videoRef = { current: document.createElement('video') } as React.RefObject const { result } = renderHook(() => useBarcodeScanner(videoRef, onDetect, true)) await waitFor(() => expect(result.current.status).toBe('ready')) await waitFor(() => expect(onDetect).toHaveBeenCalledWith('5021592210629')) }) it('reports denied when camera permission is rejected', async () => { stubMediaDevices(() => Promise.reject(new DOMException('denied', 'NotAllowedError')) ) const videoRef = { current: document.createElement('video') } as React.RefObject const { result } = renderHook(() => useBarcodeScanner(videoRef, () => {}, true)) await waitFor(() => expect(result.current.status).toBe('denied')) }) it('stops the stream when enabled goes false', async () => { const stream = fakeStream() const stopSpy = vi.spyOn(stream.getTracks()[0]!, 'stop') stubMediaDevices(() => Promise.resolve(stream)) ;(window as unknown as { BarcodeDetector?: unknown }).BarcodeDetector = class { async detect() { return [] } } const videoRef = { current: document.createElement('video') } as React.RefObject const { result, rerender } = renderHook( ({ enabled }) => useBarcodeScanner(videoRef, () => {}, enabled), { initialProps: { enabled: true } } ) await waitFor(() => expect(result.current.status).toBe('ready')) rerender({ enabled: false }) expect(stopSpy).toHaveBeenCalled() expect(result.current.status).toBe('idle') }) })