scandit-sdk
Version:
Scandit Barcode Scanner SDK for the Web
809 lines (780 loc) • 22 kB
text/typescript
/* 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<void> {
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();
(<any>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);
(<any>s).engineWorkerOnMessage({
data: ["status", "example-not-ready"]
});
t.false(s.isReady());
t.false(callbackSpy1.called);
t.false(callbackSpy2.called);
(<any>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();
(<any>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((<any>s).workerScanRequestId, 0);
t.is((<any>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((<any>s).workerScanRequestId, 0);
t.is((<any>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((<any>s).workerScanRequestId, 4);
t.is((<any>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((<any>s).workerScanRequestId, 0);
t.is((<any>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((<any>s).workerScanRequestId, 0);
t.is((<any>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<HTMLImageElement> {
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<ScanResult> = 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<ScanResult> = s.processImage(imageData2); // 3 * 2 * 4
const imageData3: Uint8ClampedArray = Uint8ClampedArray.from({ length: 24 }, () => {
return Math.floor(Math.random() * 255);
});
const processImage3: Promise<ScanResult> = s.processImage(imageData3); // 3 * 2 * 4
t.is((<any>s).workerScanRequestId, 3);
t.is((<any>s).workerScanQueueLength, 3);
(<any>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((<any>s).workerScanQueueLength, 2);
(<any>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((<any>s).workerScanQueueLength, 1);
(<any>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((<any>s).workerScanQueueLength, 0);
(<any>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((<any>s).workerParseRequestId, 0);
t.false(s.isBusyProcessing());
t.false(postMessageStub.called);
const parseString1: Promise<ParserResult> = s.parseString(Parser.DataFormat.DLID, "abcd");
t.is((<any>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<ParserResult> = s.parseString(Parser.DataFormat.GS1_AI, "efgh");
t.is((<any>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<ParserResult> = s.parseString(Parser.DataFormat.HIBC, "ijkl", {
exampleOption: true
});
t.is((<any>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();
(<any>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"
}
];
(<any>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)
});
(<any>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);
(<any>s).engineWorkerOnMessage({
data: [licenseFeaturesType]
});
t.false(callbackSpy1.called);
t.false(callbackSpy2.called);
(<any>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();
(<any>s).engineWorkerOnMessage({
data: [
licenseFeaturesType,
{
hiddenScanditLogoAllowed: true
}
]
});
t.false(callbackSpy1.called);
t.false(callbackSpy2.called);
t.false(callbackSpy3.called);
});