/* tslint:disable:no-implicit-dependencies no-any */ /* tslint:disable:insecure-random */ /** * Scanner tests */ import test from "ava"; import * as sinon from "sinon"; import { Barcode, BrowserHelper, configure, ImageSettings, Parser, ParserField, ParserResult, Scanner, ScanResult, ScanSettings } from ".."; declare const global: any; const postMessageStub: sinon.SinonStub = sinon.stub(); const terminateStub: sinon.SinonStub = sinon.stub(); const stubs: sinon.SinonStub[] = [postMessageStub, terminateStub]; global.Worker = sinon.stub().returns({ postMessage: postMessageStub, terminate: terminateStub }); URL.createObjectURL = sinon.stub(); function resetStubs(): void { stubs.forEach(mock => { mock.resetHistory(); }); } function checkBrowserCompatibility(): any { return { fullSupport: true, scannerSupport: true, missingFeatures: [] }; } async function prepareBrowserAndLibrary(): Promise { BrowserHelper.checkBrowserCompatibility = checkBrowserCompatibility; await configure("#".repeat(64)); } test.serial("constructor", async t => { let s: Scanner; let error: Error = t.throws(() => { s = new Scanner(); }); t.is(error.name, "UnsupportedBrowserError"); BrowserHelper.checkBrowserCompatibility = checkBrowserCompatibility; error = t.throws(() => { s = new Scanner(); }); t.is(error.name, "LibraryNotConfiguredError"); await configure("#".repeat(64)); resetStubs(); s = new Scanner(); t.false(s.isReady()); t.false(s.isBusyProcessing()); t.is(postMessageStub.callCount, 3); t.deepEqual(postMessageStub.getCall(0).args, [ { deviceId: BrowserHelper.getDeviceId(), deviceModelName: undefined, uaBrowserName: "WebKit", libraryLocation: "https://example.com/", path: "/", type: "load-library" } ]); t.deepEqual(postMessageStub.getCall(1).args, [ { licenseKey: "#".repeat(64), type: "license-key" } ]); t.deepEqual(postMessageStub.getCall(2).args, [ { settings: new ScanSettings().toJSONString(), type: "settings" } ]); resetStubs(); const ss: ScanSettings = new ScanSettings({ enabledSymbologies: Barcode.Symbology.QR, codeDuplicateFilter: 10, maxNumberOfCodesPerFrame: 10, searchArea: { x: 0.5, y: 0.5, width: 0.5, height: 0.1 } }); s = new Scanner({ scanSettings: ss }); t.is(postMessageStub.callCount, 3); t.deepEqual(postMessageStub.getCall(2).args, [ { settings: ss.toJSONString(), type: "settings" } ]); resetStubs(); const is: ImageSettings = { width: 640, height: 480, format: ImageSettings.Format.RGBA_8U }; s = new Scanner({ imageSettings: is }); t.is(postMessageStub.callCount, 4); t.deepEqual(postMessageStub.getCall(3).args, [ { imageSettings: is, type: "image-settings" } ]); }); test.serial("destroy", async t => { await prepareBrowserAndLibrary(); resetStubs(); let s: Scanner = new Scanner(); s.destroy(); t.true(terminateStub.called); resetStubs(); s = new Scanner(); (s).engineWorker = null; s.destroy(); t.false(terminateStub.called); }); test.serial("applyScanSettings", async t => { await prepareBrowserAndLibrary(); resetStubs(); const ss: ScanSettings = new ScanSettings({ enabledSymbologies: Barcode.Symbology.QR, codeDuplicateFilter: 10, maxNumberOfCodesPerFrame: 10, searchArea: { x: 0.5, y: 0.5, width: 0.5, height: 0.1 } }); const s: Scanner = new Scanner(); t.is(postMessageStub.callCount, 3); t.deepEqual(postMessageStub.getCall(2).args, [ { settings: new ScanSettings().toJSONString(), type: "settings" } ]); s.applyScanSettings(ss); t.is(postMessageStub.callCount, 4); t.deepEqual(postMessageStub.getCall(3).args, [ { settings: ss.toJSONString(), type: "settings" } ]); }); test.serial("applyImageSettings", async t => { await prepareBrowserAndLibrary(); resetStubs(); const is: ImageSettings = { width: 640, height: 480, format: ImageSettings.Format.RGBA_8U }; const s: Scanner = new Scanner(); t.is(postMessageStub.callCount, 3); t.deepEqual(postMessageStub.getCall(2).args, [ { settings: new ScanSettings().toJSONString(), type: "settings" } ]); s.applyImageSettings(is); t.is(postMessageStub.callCount, 4); t.deepEqual(postMessageStub.getCall(3).args, [ { imageSettings: is, type: "image-settings" } ]); }); test.serial("clearSession", async t => { await prepareBrowserAndLibrary(); resetStubs(); const s: Scanner = new Scanner(); t.is(postMessageStub.callCount, 3); s.clearSession(); t.is(postMessageStub.callCount, 4); t.deepEqual(postMessageStub.getCall(3).args, [ { type: "clear-session" } ]); }); test.serial("addListener", async t => { await prepareBrowserAndLibrary(); resetStubs(); const s: Scanner = new Scanner(); const onSpy: sinon.SinonSpy<["ready", () => void], Scanner> = sinon.spy(s, "on"); const callbackSpy: sinon.SinonSpy = sinon.spy(); s.addListener("ready", callbackSpy); t.true(onSpy.calledOnceWithExactly("ready", callbackSpy)); }); // Deprecated method test.serial("onReady", async t => { await prepareBrowserAndLibrary(); resetStubs(); const s: Scanner = new Scanner(); const onSpy: sinon.SinonSpy<["ready", () => void], Scanner> = sinon.spy(s, "on"); const callbackSpy: sinon.SinonSpy = sinon.spy(); s.onReady(callbackSpy); t.true(onSpy.calledOnceWithExactly("ready", callbackSpy)); }); test.serial("isReady & on ready", async t => { await prepareBrowserAndLibrary(); resetStubs(); let s: Scanner = new Scanner(); const callbackSpy1: sinon.SinonSpy = sinon.spy(); const callbackSpy2: sinon.SinonSpy = sinon.spy(); t.false(s.isReady()); s.on("ready", callbackSpy1); s.on("ready", callbackSpy2); t.false(callbackSpy1.called); t.false(callbackSpy2.called); (s).engineWorkerOnMessage({ data: ["status", "example-not-ready"] }); t.false(s.isReady()); t.false(callbackSpy1.called); t.false(callbackSpy2.called); (s).engineWorkerOnMessage({ data: ["status", "ready"] }); t.true(s.isReady()); t.true(callbackSpy1.called); t.true(callbackSpy2.called); t.true(callbackSpy2.calledAfter(callbackSpy1)); s = new Scanner(); (s).engineWorkerOnMessage({ data: ["status", "ready"] }); const callbackSpy3: sinon.SinonSpy = sinon.spy(); t.true(s.isReady()); s.on("ready", callbackSpy3); t.true(callbackSpy3.called); }); // tslint:disable-next-line:max-func-body-length test.serial("processImage calls", async t => { await prepareBrowserAndLibrary(); const s: Scanner = new Scanner(); resetStubs(); let error: Error = await t.throwsAsync(s.processImage(new Uint8ClampedArray(0))); t.is(error.name, "NoImageSettings"); t.is((s).workerScanRequestId, 0); t.is((s).workerScanQueueLength, 0); t.false(s.isBusyProcessing()); t.false(postMessageStub.called); s.applyImageSettings({ width: 3, height: 2, format: ImageSettings.Format.RGBA_8U }); resetStubs(); error = await t.throwsAsync(s.processImage(new Uint8ClampedArray(0))); t.is(error.name, "ImageSettingsDataMismatch"); t.is((s).workerScanRequestId, 0); t.is((s).workerScanQueueLength, 0); t.false(s.isBusyProcessing()); t.false(postMessageStub.called); resetStubs(); let imageData: Uint8ClampedArray = Uint8ClampedArray.from({ length: 24 }, () => { return Math.floor(Math.random() * 255); }); // tslint:disable-next-line: no-floating-promises s.processImage(imageData); // 3 * 2 * 4 t.true(s.isBusyProcessing()); t.is(postMessageStub.callCount, 1); t.deepEqual(postMessageStub.getCall(0).args, [ { type: "work", requestId: 1, data: imageData, highQualitySingleFrameMode: false }, [imageData.buffer] ]); s.applyImageSettings({ width: 3, height: 2, format: ImageSettings.Format.RGB_8U }); resetStubs(); imageData = Uint8ClampedArray.from({ length: 18 }, () => { return Math.floor(Math.random() * 255); }); // tslint:disable-next-line: no-floating-promises s.processImage(imageData); // 3 * 2 * 3 t.is(postMessageStub.callCount, 1); t.deepEqual(postMessageStub.getCall(0).args, [ { type: "work", requestId: 2, data: imageData, highQualitySingleFrameMode: false }, [imageData.buffer] ]); s.applyImageSettings({ width: 3, height: 2, format: ImageSettings.Format.GRAY_8U }); resetStubs(); imageData = Uint8ClampedArray.from({ length: 6 }, () => { return Math.floor(Math.random() * 255); }); // tslint:disable-next-line: no-floating-promises s.processImage(imageData); // 3 * 2 * 1 t.is(postMessageStub.callCount, 1); t.deepEqual(postMessageStub.getCall(0).args, [ { type: "work", requestId: 3, data: imageData, highQualitySingleFrameMode: false }, [imageData.buffer] ]); s.applyImageSettings({ width: 3, height: 2, format: 999 // Fake format }); resetStubs(); imageData = Uint8ClampedArray.from({ length: 6 }, () => { return Math.floor(Math.random() * 255); }); // tslint:disable-next-line: no-floating-promises s.processImage(imageData); // 3 * 2 * 1 t.is((s).workerScanRequestId, 4); t.is((s).workerScanQueueLength, 4); t.is(postMessageStub.callCount, 1); t.deepEqual(postMessageStub.getCall(0).args, [ { type: "work", requestId: 4, data: imageData, highQualitySingleFrameMode: false }, [imageData.buffer] ]); }); test.serial("processImage highQualitySingleFrameMode calls", async t => { await prepareBrowserAndLibrary(); const s: Scanner = new Scanner(); resetStubs(); let error: Error = await t.throwsAsync(s.processImage(new Uint8ClampedArray(0), true)); t.is(error.name, "NoImageSettings"); t.is((s).workerScanRequestId, 0); t.is((s).workerScanQueueLength, 0); t.false(s.isBusyProcessing()); t.false(postMessageStub.called); s.applyImageSettings({ width: 3, height: 2, format: ImageSettings.Format.RGBA_8U }); resetStubs(); error = await t.throwsAsync(s.processImage(new Uint8ClampedArray(0), true)); t.is(error.name, "ImageSettingsDataMismatch"); t.is((s).workerScanRequestId, 0); t.is((s).workerScanQueueLength, 0); t.false(s.isBusyProcessing()); t.false(postMessageStub.called); resetStubs(); const imageData: Uint8ClampedArray = Uint8ClampedArray.from({ length: 24 }, () => { return Math.floor(Math.random() * 255); }); // tslint:disable-next-line: no-floating-promises s.processImage(imageData, true); // 3 * 2 * 4 t.true(s.isBusyProcessing()); t.is(postMessageStub.callCount, 1); t.deepEqual(postMessageStub.getCall(0).args, [ { type: "work", requestId: 1, data: imageData, highQualitySingleFrameMode: true }, [imageData.buffer] ]); }); test.serial("processImage calls with ImageData", async t => { async function getImageFromCanvas(canvasElement: HTMLCanvasElement): Promise { return new Promise(resolve => { const image: HTMLImageElement = new Image(); image.onload = () => { resolve(image); }; image.src = canvasElement.toDataURL(); }); } await prepareBrowserAndLibrary(); const s: Scanner = new Scanner(); const canvas: HTMLCanvasElement = document.createElement("canvas"); canvas.width = 3; canvas.height = 2; let imageData: HTMLImageElement = await getImageFromCanvas(canvas); s.applyImageSettings({ width: 3, height: 2, format: ImageSettings.Format.RGBA_8U }); resetStubs(); // tslint:disable-next-line: no-floating-promises s.processImage(imageData); // 3 * 2 * 4 t.true(s.isBusyProcessing()); t.is(postMessageStub.callCount, 1); t.deepEqual(postMessageStub.getCall(0).args, [ { type: "work", requestId: 1, data: new Uint8ClampedArray(3 * 2 * 4), highQualitySingleFrameMode: false }, [new Uint8ClampedArray(3 * 2 * 4).buffer] ]); s.applyImageSettings({ width: 5, height: 4, format: ImageSettings.Format.RGBA_8U }); resetStubs(); canvas.width = 5; canvas.height = 4; imageData = await getImageFromCanvas(canvas); // tslint:disable-next-line: no-floating-promises s.processImage(imageData); // 5 * 4 * 4 t.is(postMessageStub.callCount, 1); t.deepEqual(postMessageStub.getCall(0).args, [ { type: "work", requestId: 2, data: new Uint8ClampedArray(5 * 4 * 4), highQualitySingleFrameMode: false }, [new Uint8ClampedArray(5 * 4 * 4).buffer] ]); }); // tslint:disable-next-line:max-func-body-length test.serial("processImage scan results", async t => { await prepareBrowserAndLibrary(); resetStubs(); const s: Scanner = new Scanner(); const imageSettings: ImageSettings = { width: 3, height: 2, format: ImageSettings.Format.RGBA_8U }; s.applyImageSettings(imageSettings); const imageData1: Uint8ClampedArray = Uint8ClampedArray.from({ length: 24 }, () => { return Math.floor(Math.random() * 255); }); const processImage1: Promise = s.processImage(imageData1); // 3 * 2 * 4 t.true(s.isBusyProcessing()); const imageData2: Uint8ClampedArray = Uint8ClampedArray.from({ length: 24 }, () => { return Math.floor(Math.random() * 255); }); const processImage2: Promise = s.processImage(imageData2); // 3 * 2 * 4 const imageData3: Uint8ClampedArray = Uint8ClampedArray.from({ length: 24 }, () => { return Math.floor(Math.random() * 255); }); const processImage3: Promise = s.processImage(imageData3); // 3 * 2 * 4 t.is((s).workerScanRequestId, 3); t.is((s).workerScanQueueLength, 3); (s).engineWorkerOnMessage({ data: [ "work-error", { requestId: 2, error: { errorCode: 123, errorMessage: "example_error" } } ] }); const error: Error = await t.throwsAsync(processImage2); t.deepEqual(error.message, "example_error (123)"); t.is((s).workerScanQueueLength, 2); (s).engineWorkerOnMessage({ data: [ "work-result", { requestId: 1, result: { scanResult: [], matrixScanResult: { barcodesAppeared: [], barcodesUpdated: [], barcodesLost: [], barcodesPredicted: [] } } } ] }); let scanResult: ScanResult = await processImage1; t.deepEqual(scanResult, new ScanResult([], imageData1, imageSettings)); t.is((s).workerScanQueueLength, 1); (s).engineWorkerOnMessage({ data: [ "work-result", { requestId: 3, result: { scanResult: [ { symbology: Barcode.Symbology.QR, rawData: [97, 98, 99, 100], location: [[1, 2], [3, 4], [5, 6], [7, 8]], compositeFlag: Barcode.CompositeFlag.NONE, isGs1DataCarrier: false, encodingArray: [], isRecognized: true } ], matrixScanResult: { barcodesAppeared: [], barcodesUpdated: [], barcodesLost: [], barcodesPredicted: [] } } } ] }); scanResult = await processImage3; t.deepEqual( scanResult, new ScanResult( [ { compositeFlag: Barcode.CompositeFlag.NONE, data: "abcd", encodingArray: [], isGs1DataCarrier: false, location: { bottomLeft: { x: 7, y: 8 }, bottomRight: { x: 5, y: 6 }, topLeft: { x: 1, y: 2 }, topRight: { x: 3, y: 4 } }, rawData: new Uint8Array([97, 98, 99, 100]), symbology: Barcode.Symbology.QR } ], imageData3, imageSettings ) ); t.is((s).workerScanQueueLength, 0); (s).engineWorkerOnMessage({ data: [] }); }); test.serial("getImageSettings", async t => { await prepareBrowserAndLibrary(); resetStubs(); const ss: ImageSettings = { width: 640, height: 480, format: ImageSettings.Format.RGBA_8U }; const s: Scanner = new Scanner(); t.is(s.getImageSettings(), undefined); s.applyImageSettings(ss); t.deepEqual(s.getImageSettings(), ss); }); test.serial("getScanSettings", async t => { await prepareBrowserAndLibrary(); resetStubs(); const ss: ScanSettings = new ScanSettings({ enabledSymbologies: Barcode.Symbology.QR, codeDuplicateFilter: 10, maxNumberOfCodesPerFrame: 10, searchArea: { x: 0.5, y: 0.5, width: 0.5, height: 0.1 } }); const s: Scanner = new Scanner(); t.deepEqual(s.getScanSettings(), new ScanSettings()); s.applyScanSettings(ss); t.deepEqual(s.getScanSettings(), ss); }); // tslint:disable-next-line:max-func-body-length test.serial("createParserForFormat & parseString", async t => { const parseStringType: string = "parse-string"; await prepareBrowserAndLibrary(); const s: Scanner = new Scanner(); resetStubs(); const parser: Parser = s.createParserForFormat(Parser.DataFormat.DLID); t.not(parser, null); t.is((s).workerParseRequestId, 0); t.false(s.isBusyProcessing()); t.false(postMessageStub.called); const parseString1: Promise = s.parseString(Parser.DataFormat.DLID, "abcd"); t.is((s).workerParseRequestId, 1); t.false(s.isBusyProcessing()); t.is(postMessageStub.callCount, 1); t.deepEqual(postMessageStub.getCall(0).args, [ { type: parseStringType, requestId: 1, dataFormat: Parser.DataFormat.DLID, dataString: "abcd", options: "{}" } ]); resetStubs(); const parseString2: Promise = s.parseString(Parser.DataFormat.GS1_AI, "efgh"); t.is((s).workerParseRequestId, 2); t.false(s.isBusyProcessing()); t.is(postMessageStub.callCount, 1); t.deepEqual(postMessageStub.getCall(0).args, [ { type: parseStringType, requestId: 2, dataFormat: Parser.DataFormat.GS1_AI, dataString: "efgh", options: "{}" } ]); resetStubs(); const parseString3: Promise = s.parseString(Parser.DataFormat.HIBC, "ijkl", { exampleOption: true }); t.is((s).workerParseRequestId, 3); t.false(s.isBusyProcessing()); t.is(postMessageStub.callCount, 1); t.deepEqual(postMessageStub.getCall(0).args, [ { type: parseStringType, requestId: 3, dataFormat: Parser.DataFormat.HIBC, dataString: "ijkl", options: '{"exampleOption":true}' } ]); resetStubs(); (s).engineWorkerOnMessage({ data: [ "parse-string-error", { requestId: 2, error: { errorCode: 123, errorMessage: "example_error" } } ] }); const error: Error = await t.throwsAsync(parseString2); t.deepEqual(error.message, "example_error (123)"); const resultData: ParserField[] = [ { name: "field1", parsed: 1, rawString: "123" }, { name: "field2", parsed: "abcd", rawString: "efgh" }, { name: "field3", parsed: { subField1: 1, subField2: 2 }, rawString: "sf1sf2" } ]; (s).engineWorkerOnMessage({ data: [ "parse-string-result", { requestId: 1, result: JSON.stringify(resultData) } ] }); let parserResult: ParserResult = await parseString1; const fieldsByName: { [fieldName: string]: ParserField } = {}; resultData.forEach(parserField => { fieldsByName[parserField.name] = parserField; }); t.deepEqual(parserResult, { fields: resultData, fieldsByName, jsonString: JSON.stringify(resultData) }); (s).engineWorkerOnMessage({ data: [ "parse-string-result", { requestId: 3, result: JSON.stringify([]) } ] }); parserResult = await parseString3; t.deepEqual(parserResult, { fields: [], fieldsByName: {}, jsonString: JSON.stringify([]) }); }); test.serial("onLicenseFeaturesReady", async t => { const licenseFeaturesType: string = "license-features"; await prepareBrowserAndLibrary(); resetStubs(); const s: Scanner = new Scanner(); const callbackSpy1: sinon.SinonSpy = sinon.spy(); const callbackSpy2: sinon.SinonSpy = sinon.spy(); const callbackSpy3: sinon.SinonSpy = sinon.spy(); s.onLicenseFeaturesReady(callbackSpy1); s.onLicenseFeaturesReady(callbackSpy2); t.false(callbackSpy1.called); t.false(callbackSpy2.called); (s).engineWorkerOnMessage({ data: [licenseFeaturesType] }); t.false(callbackSpy1.called); t.false(callbackSpy2.called); (s).engineWorkerOnMessage({ data: [ licenseFeaturesType, { hiddenScanditLogoAllowed: true } ] }); t.true(callbackSpy1.called); t.true(callbackSpy2.called); t.true(callbackSpy2.calledAfter(callbackSpy1)); t.deepEqual(callbackSpy1.getCall(0).args, [ { hiddenScanditLogoAllowed: true } ]); t.deepEqual(callbackSpy1.getCall(0).args, callbackSpy2.getCall(0).args); s.onLicenseFeaturesReady(callbackSpy3); t.true(callbackSpy3.called); t.deepEqual(callbackSpy1.getCall(0).args, callbackSpy3.getCall(0).args); callbackSpy1.resetHistory(); callbackSpy2.resetHistory(); callbackSpy3.resetHistory(); (s).engineWorkerOnMessage({ data: [ licenseFeaturesType, { hiddenScanditLogoAllowed: true } ] }); t.false(callbackSpy1.called); t.false(callbackSpy2.called); t.false(callbackSpy3.called); });