import type { WebcamBackend, WebcamCaptureOptions } from '@flighthq/types/contract'; import { createWebWebcamBackend, getWebcamBackend, recordWebcamVideo, requestWebcamPermission, selectWebcamImage, setWebcamBackend, takeWebcamPhoto, explainWebcamBackend, installWebcamHostBackend, observeWebcamHostResult, resetWebcamBackendForTest, } from './webcam'; function fakeBackend(): WebcamBackend & { lastOptions: WebcamCaptureOptions | null } { return { lastOptions: null, async capture(options) { this.lastOptions = { ...options }; return { dataUrl: 'data:image/png;base64,xx', width: 0, height: 0, format: 'image/png' }; }, async captureVideo(options) { this.lastOptions = { ...options }; return { dataUrl: 'data:video/mp4;base64,xx', duration: 0, format: 'video/mp4' }; }, async requestPermission() { return true; }, }; } afterEach(() => setWebcamBackend(null)); describe('createWebWebcamBackend', () => { it('returns a backend whose capture yields a Promise without throwing synchronously', () => { const backend = createWebWebcamBackend(); expect(backend.capture({}) instanceof Promise).toBe(true); }); }); describe('explainWebcamBackend', () => { afterEach(() => resetWebcamBackendForTest()); it('reports host-not-enabled when no backend is installed', () => { resetWebcamBackendForTest(); const explanation = explainWebcamBackend(); expect(explanation.layer).toBe('host-not-enabled'); expect(explanation.conflict).toBe(false); expect(explanation.viability).toBe('unobserved'); }); it('reports custom layer when a custom backend is set', () => { setWebcamBackend(fakeBackend()); expect(explainWebcamBackend().layer).toBe('custom'); }); it('reports host layer when a host backend is installed', () => { installWebcamHostBackend(fakeBackend()); expect(explainWebcamBackend().layer).toBe('host'); }); it('reports conflict when two different host backends are installed', () => { installWebcamHostBackend(fakeBackend()); installWebcamHostBackend(fakeBackend()); expect(explainWebcamBackend().conflict).toBe(true); }); }); describe('getWebcamBackend', () => { it('falls back to a web backend', () => { expect(getWebcamBackend()).not.toBeNull(); }); it('returns the registered backend', () => { const backend = fakeBackend(); setWebcamBackend(backend); expect(getWebcamBackend()).toBe(backend); }); }); describe('installWebcamHostBackend', () => { afterEach(() => resetWebcamBackendForTest()); it('installs a host backend that getWebcamBackend returns', () => { const backend = fakeBackend(); installWebcamHostBackend(backend); expect(getWebcamBackend()).toBe(backend); }); it('is first-host-wins: a second different backend sets conflict', () => { const first = fakeBackend(); const second = fakeBackend(); installWebcamHostBackend(first); installWebcamHostBackend(second); expect(getWebcamBackend()).toBe(first); expect(explainWebcamBackend().conflict).toBe(true); }); }); describe('observeWebcamHostResult', () => { afterEach(() => resetWebcamBackendForTest()); it('records a successful observation', () => { installWebcamHostBackend(fakeBackend()); observeWebcamHostResult('capture', true); const explanation = explainWebcamBackend(); expect(explanation.operation).toBe('capture'); expect(explanation.viability).toBe('available'); }); it('records a failed observation', () => { installWebcamHostBackend(fakeBackend()); observeWebcamHostResult('capture', false); expect(explainWebcamBackend().viability).toBe('runtime-api-unavailable'); }); }); describe('recordWebcamVideo', () => { it('captures video with the camera source', async () => { const backend = fakeBackend(); setWebcamBackend(backend); const video = await recordWebcamVideo({ maxDurationMs: 5000 }); expect(video).not.toBeNull(); expect(backend.lastOptions).toEqual({ maxDurationMs: 5000, source: 'camera' }); }); it('returns a Promise from the web backend without throwing', () => { const backend = createWebWebcamBackend(); expect(backend.captureVideo({}) instanceof Promise).toBe(true); }); }); describe('requestWebcamPermission', () => { it('delegates to the active backend', async () => { setWebcamBackend(fakeBackend()); expect(await requestWebcamPermission()).toBe(true); }); it('returns a boolean from the web backend without throwing', async () => { expect(typeof (await requestWebcamPermission())).toBe('boolean'); }); }); describe('resetWebcamBackendForTest', () => { it('clears all backend slots', () => { setWebcamBackend(fakeBackend()); installWebcamHostBackend(fakeBackend()); observeWebcamHostResult('capture', true); resetWebcamBackendForTest(); expect(explainWebcamBackend().layer).toBe('host-not-enabled'); expect(explainWebcamBackend().conflict).toBe(false); expect(explainWebcamBackend().viability).toBe('unobserved'); }); }); describe('selectWebcamImage', () => { it('captures with the photos source', async () => { const backend = fakeBackend(); setWebcamBackend(backend); const photo = await selectWebcamImage({ quality: 0.5 }); expect(photo).not.toBeNull(); expect(backend.lastOptions).toEqual({ quality: 0.5, source: 'photos' }); }); }); describe('setWebcamBackend', () => { it('clears back to the web fallback when passed null', () => { setWebcamBackend(fakeBackend()); setWebcamBackend(null); expect(getWebcamBackend()).not.toBeNull(); }); }); describe('takeWebcamPhoto', () => { it('captures with the camera source', async () => { const backend = fakeBackend(); setWebcamBackend(backend); const photo = await takeWebcamPhoto({ allowEditing: true }); expect(photo).not.toBeNull(); expect(backend.lastOptions).toEqual({ allowEditing: true, source: 'camera' }); }); });