| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175 |
- /**
- * The scanner's decoding never throws for a frame or photo it can't read, and
- * falls back to jsQR when the platform decoder is missing, can't be built or
- * fails (#1574 review).
- */
- import { describe, it, expect, vi, afterEach } from 'vitest';
- const jsQR = vi.hoisted(() => vi.fn());
- vi.mock('jsqr', () => ({ default: jsQR }));
- import { createFrameDecoder, createQrDetector, decodeCanvas, decodeImageBlob } from '../../utils/qrDecode';
- function readableContext() {
- return {
- drawImage: vi.fn(),
- getImageData: vi.fn(() => ({ data: new Uint8ClampedArray(4) })),
- };
- }
- function canvasWith(getContext: () => unknown): HTMLCanvasElement {
- const canvas = document.createElement('canvas');
- canvas.getContext = vi.fn(getContext) as unknown as HTMLCanvasElement['getContext'];
- return canvas;
- }
- const video = { videoWidth: 640, videoHeight: 480 } as unknown as HTMLVideoElement;
- function stubDetector(detect: () => Promise<{ rawValue: string }[]>) {
- const detectSpy = vi.fn(detect);
- vi.stubGlobal(
- 'BarcodeDetector',
- class {
- detect = detectSpy;
- },
- );
- return detectSpy;
- }
- function stubUnbuildableDetector() {
- vi.stubGlobal(
- 'BarcodeDetector',
- class {
- constructor() {
- throw new TypeError("Failed to construct 'BarcodeDetector': qr_code is not supported");
- }
- },
- );
- }
- afterEach(() => {
- vi.unstubAllGlobals();
- vi.restoreAllMocks();
- jsQR.mockReset();
- });
- describe('createQrDetector', () => {
- it('is null where the platform has no BarcodeDetector', () => {
- vi.stubGlobal('BarcodeDetector', undefined);
- expect(createQrDetector()).toBeNull();
- });
- it('is null where the platform decoder cannot read QR codes', () => {
- stubUnbuildableDetector();
- expect(createQrDetector()).toBeNull();
- });
- it('asks the platform decoder for QR codes', () => {
- const made: unknown[] = [];
- vi.stubGlobal(
- 'BarcodeDetector',
- class {
- constructor(opts: unknown) {
- made.push(opts);
- }
- },
- );
- expect(createQrDetector()).not.toBeNull();
- expect(made).toEqual([{ formats: ['qr_code'] }]);
- });
- });
- describe('decodeCanvas', () => {
- it("returns jsQR's reading of the frame", () => {
- jsQR.mockReturnValue({ data: 'spool=3' });
- expect(decodeCanvas(canvasWith(readableContext), video, 4, 4, 'dontInvert')).toBe('spool=3');
- });
- it('is undefined, not a throw, when the canvas cannot be read', () => {
- expect(decodeCanvas(canvasWith(() => { throw new Error('not implemented'); }), video, 4, 4, 'dontInvert')).toBeUndefined();
- expect(decodeCanvas(canvasWith(() => null), video, 4, 4, 'dontInvert')).toBeUndefined();
- const ctx = readableContext();
- ctx.drawImage.mockImplementation(() => {
- throw new DOMException('no frame yet', 'InvalidStateError');
- });
- expect(decodeCanvas(canvasWith(() => ctx), video, 4, 4, 'dontInvert')).toBeUndefined();
- });
- it('skips a frame with no size', () => {
- const canvas = canvasWith(readableContext);
- expect(decodeCanvas(canvas, video, 0, 0, 'dontInvert')).toBeUndefined();
- expect(canvas.getContext).not.toHaveBeenCalled();
- });
- });
- describe('createFrameDecoder', () => {
- it("uses the platform decoder's reading", async () => {
- stubDetector(async () => [{ rawValue: 'spool=1' }]);
- const decode = createFrameDecoder();
- expect(await decode(video, canvasWith(readableContext))).toBe('spool=1');
- expect(jsQR).not.toHaveBeenCalled();
- });
- it('drops a platform decoder that rejects, for jsQR on that frame and the next', async () => {
- const detect = stubDetector(async () => {
- throw new DOMException('decode failed', 'OperationError');
- });
- jsQR.mockReturnValue({ data: 'spool=2' });
- const decode = createFrameDecoder();
- const canvas = canvasWith(readableContext);
- expect(await decode(video, canvas)).toBe('spool=2');
- expect(await decode(video, canvas)).toBe('spool=2');
- expect(detect).toHaveBeenCalledTimes(1);
- });
- it('uses jsQR when the platform decoder cannot be built', async () => {
- stubUnbuildableDetector();
- jsQR.mockReturnValue({ data: 'spool=4' });
- const decode = createFrameDecoder();
- expect(await decode(video, canvasWith(readableContext))).toBe('spool=4');
- });
- it('resolves rather than rejects when nothing can read the frame', async () => {
- stubDetector(async () => {
- throw new Error('boom');
- });
- const decode = createFrameDecoder();
- await expect(decode(video, canvasWith(() => { throw new Error('no canvas'); }))).resolves.toBeUndefined();
- });
- });
- describe('decodeImageBlob', () => {
- const photo = new Blob(['qr'], { type: 'image/jpeg' });
- function stubBitmap() {
- const bitmap = { width: 3000, height: 2000, close: vi.fn() };
- vi.stubGlobal('createImageBitmap', vi.fn().mockResolvedValue(bitmap));
- vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockImplementation(
- () => readableContext() as unknown as RenderingContext,
- );
- return bitmap;
- }
- it('falls back to jsQR when the platform decoder cannot be built', async () => {
- const bitmap = stubBitmap();
- stubUnbuildableDetector();
- jsQR.mockReturnValue({ data: 'spool=5' });
- expect(await decodeImageBlob(photo)).toBe('spool=5');
- // Downscaled to 1600px on the long side before jsQR reads it.
- expect(jsQR).toHaveBeenCalledWith(expect.any(Uint8ClampedArray), 1600, 1067, { inversionAttempts: 'attemptBoth' });
- expect(bitmap.close).toHaveBeenCalled();
- });
- it('falls back to jsQR when the platform decoder rejects the photo', async () => {
- stubBitmap();
- stubDetector(async () => {
- throw new DOMException('decode failed', 'OperationError');
- });
- jsQR.mockReturnValue({ data: 'spool=6' });
- expect(await decodeImageBlob(photo)).toBe('spool=6');
- });
- });
|