Merge pull request #102 from beatz174-bit/codex/mock-barcode-scanner-dependencies-for-tests

Add unit tests for useBarcodeScanner hook
This commit is contained in:
beatz174-bit
2025-11-24 08:23:35 +10:00
committed by GitHub
@@ -0,0 +1,128 @@
import { renderHook, waitFor } from '@testing-library/react';
import { describe, it, beforeEach, afterEach, expect, vi, type MockInstance } from 'vitest';
import { useBarcodeScanner } from '../useBarcodeScanner';
const decodeFromVideoDevice = vi.fn();
vi.mock('@zxing/browser', () => ({
BrowserMultiFormatReader: vi.fn().mockImplementation(() => ({
decodeFromVideoDevice,
})),
}));
declare global {
interface Window {
BarcodeDetector?: new () => { detect: (source: ImageBitmapSource) => Promise<{ rawValue: string }[]> };
}
}
const createMediaStream = (stopSpy: MockInstance = vi.fn()) => {
const track = { stop: stopSpy } as MediaStreamTrack;
return {
getTracks: vi.fn(() => [track]),
getVideoTracks: vi.fn(() => [track]),
} as unknown as MediaStream;
};
const createVideoElement = () => {
let srcObject: MediaStream | null = null;
return {
play: vi.fn(() => Promise.resolve()),
get srcObject() {
return srcObject;
},
set srcObject(value: MediaStream | null) {
srcObject = value;
},
} as unknown as HTMLVideoElement;
};
beforeEach(() => {
decodeFromVideoDevice.mockReset();
(globalThis as unknown as { createImageBitmap: typeof createImageBitmap }).createImageBitmap = vi
.fn()
.mockResolvedValue({} as ImageBitmap);
Object.assign(navigator, {
mediaDevices: {
getUserMedia: vi.fn(),
},
});
});
afterEach(() => {
vi.clearAllMocks();
delete (window as Window & { BarcodeDetector?: unknown }).BarcodeDetector;
});
describe('useBarcodeScanner', () => {
it('uses native BarcodeDetector when available and sets the detected code', async () => {
const trackStop = vi.fn();
const stream = createMediaStream(trackStop);
(navigator.mediaDevices.getUserMedia as unknown as vi.Mock).mockResolvedValue(stream);
class MockBarcodeDetector {
detect = vi.fn(async () => [{ rawValue: 'NATIVE-123' }]);
}
(window as Window).BarcodeDetector = MockBarcodeDetector;
const videoElement = createVideoElement();
const { result, unmount } = renderHook(() => {
const hook = useBarcodeScanner();
hook.videoRef.current = videoElement;
return hook;
});
await waitFor(() => expect(result.current.result.code).toBe('NATIVE-123'));
expect(navigator.mediaDevices.getUserMedia).toHaveBeenCalledWith({ video: { facingMode: 'environment' } });
unmount();
expect(trackStop).toHaveBeenCalled();
});
it('falls back to BrowserMultiFormatReader and returns decoded text', async () => {
delete (window as Window).BarcodeDetector;
const stopControls = vi.fn();
decodeFromVideoDevice.mockImplementation(async (_deviceId, _video, callback) => {
callback({ getText: () => 'FALLBACK-789' } as unknown as { getText: () => string });
return { stop: stopControls };
});
const videoElement = createVideoElement();
const { result, unmount } = renderHook(() => {
const hook = useBarcodeScanner();
hook.videoRef.current = videoElement;
return hook;
});
await waitFor(() => {
expect(decodeFromVideoDevice).toHaveBeenCalled();
expect(result.current.result.code).toBe('FALLBACK-789');
});
unmount();
expect(stopControls).toHaveBeenCalled();
});
it('surfaces errors encountered while starting the scanner', async () => {
(navigator.mediaDevices.getUserMedia as unknown as vi.Mock).mockRejectedValue(new Error('Camera denied'));
class MockBarcodeDetector {
detect = vi.fn(async () => []);
}
(window as Window).BarcodeDetector = MockBarcodeDetector;
const videoElement = createVideoElement();
const { result } = renderHook(() => {
const hook = useBarcodeScanner();
hook.videoRef.current = videoElement;
return hook;
});
await waitFor(() => expect(result.current.result.error).toBe('Camera denied'));
});
});