89 lines
3.4 KiB
TypeScript
89 lines
3.4 KiB
TypeScript
|
|
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<MediaStream>) {
|
||
|
|
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<HTMLVideoElement>
|
||
|
|
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<HTMLVideoElement>
|
||
|
|
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<HTMLVideoElement>
|
||
|
|
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')
|
||
|
|
})
|
||
|
|
})
|