scandit-sdk
Version:
Scandit Barcode Scanner SDK for the Web
519 lines • 23.1 kB
JavaScript
/* tslint:disable:no-implicit-dependencies no-any */
/**
* BarcodePickerCameraManager tests
*/
import test from "ava";
import * as sinon from "sinon";
import { Camera, CameraAccess } from "..";
import { BarcodePickerCameraManager, MeteringMode } from "./barcodePickerCameraManager";
import { BarcodePickerGui } from "./barcodePickerGui";
Object.defineProperty(screen, "width", {
writable: true
});
Object.defineProperty(screen, "height", {
writable: true
});
screen.width = 100;
screen.height = 100;
const triggerFatalErrorSpy = sinon.spy();
// Speed up times
BarcodePickerCameraManager.cameraAccessTimeoutMs /= 10;
BarcodePickerCameraManager.cameraMetadataCheckTimeoutMs /= 10;
BarcodePickerCameraManager.cameraMetadataCheckIntervalMs /= 10;
BarcodePickerCameraManager.getCapabilitiesTimeoutMs /= 10;
BarcodePickerCameraManager.autofocusIntervalMs /= 10;
BarcodePickerCameraManager.manualToAutofocusResumeTimeoutMs /= 10;
BarcodePickerCameraManager.manualFocusWaitTimeoutMs /= 10;
async function wait(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
function fakeGetCameras(cameraAmount, cameraTypes, cameraLabels) {
if (CameraAccess.getCameras.restore != null) {
CameraAccess.getCameras.restore();
}
sinon.stub(CameraAccess, "getCameras").resolves(
// tslint:disable-next-line:prefer-array-literal
Array.from(Array(cameraAmount), (_, index) => {
const cameraType = cameraTypes == null || cameraTypes[index] == null ? Camera.Type.BACK : cameraTypes[index];
const label = cameraLabels == null || cameraLabels[index] == null
? `Fake Camera Device (${cameraType})`
: cameraLabels[index];
return {
deviceId: "unknown",
groupId: "1",
kind: "videoinput",
label,
cameraType
};
}));
}
function fakeAccessCameraStream(facingMode, mediaTrackCapabilities) {
if (CameraAccess.accessCameraStream.restore != null) {
CameraAccess.accessCameraStream.restore();
}
sinon.stub(CameraAccess, "accessCameraStream").callsFake(() => {
const mediaStreamTrack = {
stop: sinon.spy(),
addEventListener: sinon.spy(),
getSettings: () => {
return {
width: 640,
height: 480,
deviceId: "1",
facingMode
};
},
label: ""
};
if (mediaTrackCapabilities != null) {
mediaStreamTrack.getCapabilities = () => {
return mediaTrackCapabilities;
};
}
return Promise.resolve({
getTracks: () => {
return [mediaStreamTrack];
},
getVideoTracks: () => {
return [mediaStreamTrack];
}
});
});
}
function fakeAccessCameraStreamFailure(error) {
if (CameraAccess.accessCameraStream.restore != null) {
CameraAccess.accessCameraStream.restore();
}
sinon.stub(CameraAccess, "accessCameraStream").rejects(error);
}
function fakeMediaStream(cameraManager, mediaTrackCapabilities) {
const mediaStreamTrack = {
constraints: {},
stop: sinon.spy(),
// tslint:disable-next-line:no-accessor-field-mismatch
getConstraints() {
return this.constraints;
},
applyConstraints: sinon.stub().callsFake((mediaTrackConstraints) => {
mediaStreamTrack.constraints = mediaTrackConstraints;
return Promise.resolve();
})
};
if (mediaTrackCapabilities != null) {
mediaStreamTrack.getCapabilities = () => {
return mediaTrackCapabilities;
};
}
const mediaStream = {
getVideoTracks: () => {
return [mediaStreamTrack];
}
};
cameraManager.mediaStream = mediaStream;
cameraManager.storeStreamCapabilities();
return mediaStream;
}
test("isCameraSwitcherEnabled & setCameraSwitcherEnabled", async (t) => {
const barcodePickerGui = sinon.createStubInstance(BarcodePickerGui);
const cameraManager = new BarcodePickerCameraManager(triggerFatalErrorSpy, barcodePickerGui);
cameraManager.setInteractionOptions(false, false, false, false);
t.false(cameraManager.isCameraSwitcherEnabled());
t.is(barcodePickerGui.setCameraSwitcherVisible.callCount, 0);
fakeGetCameras(1);
await cameraManager.setCameraSwitcherEnabled(true);
t.is(barcodePickerGui.setCameraSwitcherVisible.callCount, 0);
fakeGetCameras(2);
await cameraManager.setCameraSwitcherEnabled(true);
t.is(barcodePickerGui.setCameraSwitcherVisible.callCount, 1);
t.deepEqual(barcodePickerGui.setCameraSwitcherVisible.lastCall.args, [true]);
t.true(cameraManager.isCameraSwitcherEnabled());
await cameraManager.setCameraSwitcherEnabled(false);
t.is(barcodePickerGui.setCameraSwitcherVisible.callCount, 2);
t.deepEqual(barcodePickerGui.setCameraSwitcherVisible.lastCall.args, [false]);
t.false(cameraManager.isCameraSwitcherEnabled());
});
test("isTorchToggleEnabled & setTorchToggleEnabled", t => {
const barcodePickerGui = sinon.createStubInstance(BarcodePickerGui);
const cameraManager = new BarcodePickerCameraManager(triggerFatalErrorSpy, barcodePickerGui);
cameraManager.setInteractionOptions(false, false, false, false);
t.false(cameraManager.isTorchToggleEnabled());
t.is(barcodePickerGui.setTorchTogglerVisible.callCount, 0);
cameraManager.setTorchToggleEnabled(true);
t.is(barcodePickerGui.setTorchTogglerVisible.callCount, 0);
fakeMediaStream(cameraManager, {
torch: true
});
cameraManager.setTorchToggleEnabled(true);
t.is(barcodePickerGui.setTorchTogglerVisible.callCount, 1);
t.deepEqual(barcodePickerGui.setTorchTogglerVisible.lastCall.args, [true]);
t.true(cameraManager.isTorchToggleEnabled());
cameraManager.setTorchToggleEnabled(false);
t.is(barcodePickerGui.setTorchTogglerVisible.callCount, 2);
t.deepEqual(barcodePickerGui.setTorchTogglerVisible.lastCall.args, [false]);
t.false(cameraManager.isTorchToggleEnabled());
});
test("isTapToFocusEnabled & setTapToFocusEnabled & isPinchToZoomEnabled & setPinchToZoomEnabled", t => {
const barcodePickerGui = sinon.createStubInstance(BarcodePickerGui);
const videoElementAddEventListener = sinon.spy();
const videoElementRemoveEventListener = sinon.spy();
barcodePickerGui.videoElement = {
addEventListener: videoElementAddEventListener,
removeEventListener: videoElementRemoveEventListener
};
const cameraManager = new BarcodePickerCameraManager(triggerFatalErrorSpy, barcodePickerGui);
cameraManager.setInteractionOptions(false, false, false, false);
t.false(cameraManager.isTapToFocusEnabled());
t.false(cameraManager.isPinchToZoomEnabled());
t.is(videoElementAddEventListener.callCount, 0);
cameraManager.setTapToFocusEnabled(true);
cameraManager.setPinchToZoomEnabled(true);
t.is(videoElementAddEventListener.callCount, 0);
fakeMediaStream(cameraManager);
cameraManager.setTapToFocusEnabled(true);
t.is(videoElementAddEventListener.callCount, 2);
t.true(videoElementAddEventListener.calledWith("mousedown"));
t.true(videoElementAddEventListener.calledWith("touchend"));
cameraManager.setPinchToZoomEnabled(true);
t.is(videoElementAddEventListener.callCount, 4);
t.true(videoElementAddEventListener.calledWith("touchstart"));
t.true(videoElementAddEventListener.calledWith("touchmove"));
t.true(cameraManager.isTapToFocusEnabled());
t.true(cameraManager.isPinchToZoomEnabled());
t.is(videoElementRemoveEventListener.callCount, 0);
cameraManager.setTapToFocusEnabled(false);
t.is(videoElementRemoveEventListener.callCount, 2);
t.true(videoElementRemoveEventListener.calledWith("mousedown"));
t.true(videoElementRemoveEventListener.calledWith("touchend"));
cameraManager.setPinchToZoomEnabled(false);
t.is(videoElementRemoveEventListener.callCount, 4);
t.true(videoElementRemoveEventListener.calledWith("touchstart"));
t.true(videoElementRemoveEventListener.calledWith("touchmove"));
t.false(cameraManager.isTapToFocusEnabled());
t.false(cameraManager.isPinchToZoomEnabled());
});
test("setTorchEnabled & toggleTorch", async (t) => {
const barcodePickerGui = sinon.createStubInstance(BarcodePickerGui);
const cameraManager = new BarcodePickerCameraManager(triggerFatalErrorSpy, barcodePickerGui);
await cameraManager.setTorchEnabled(true);
const mediaTrackCapabilities = {
torch: true
};
const applyConstraintsStub = (fakeMediaStream(cameraManager, mediaTrackCapabilities).getVideoTracks()[0].applyConstraints);
t.true(applyConstraintsStub.notCalled);
await cameraManager.setTorchEnabled(true);
t.true(applyConstraintsStub.calledOnce);
t.true(applyConstraintsStub.calledWith({ advanced: [{ torch: true }] }));
await cameraManager.setTorchEnabled(false);
t.true(applyConstraintsStub.calledTwice);
t.true(applyConstraintsStub.calledWith({ advanced: [{ torch: false }] }));
applyConstraintsStub.resetHistory();
await cameraManager.toggleTorch();
t.true(applyConstraintsStub.calledOnce);
t.true(applyConstraintsStub.calledWith({ advanced: [{ torch: true }] }));
await cameraManager.toggleTorch();
t.true(applyConstraintsStub.calledTwice);
t.true(applyConstraintsStub.calledWith({ advanced: [{ torch: false }] }));
});
test("setZoom", async (t) => {
const barcodePickerGui = sinon.createStubInstance(BarcodePickerGui);
const cameraManager = new BarcodePickerCameraManager(triggerFatalErrorSpy, barcodePickerGui);
await cameraManager.setZoom(2);
const mediaTrackCapabilities = {
zoom: {
max: 9,
min: 1,
step: 0.1
}
};
const applyConstraintsStub = (fakeMediaStream(cameraManager, mediaTrackCapabilities).getVideoTracks()[0].applyConstraints);
t.true(applyConstraintsStub.notCalled);
await cameraManager.setZoom(0);
t.deepEqual(applyConstraintsStub.lastCall.args, [{ advanced: [{ zoom: 1 }] }]);
await cameraManager.setZoom(1);
t.deepEqual(applyConstraintsStub.lastCall.args, [{ advanced: [{ zoom: 9 }] }]);
await cameraManager.setZoom(0.5);
t.deepEqual(applyConstraintsStub.lastCall.args, [{ advanced: [{ zoom: 5 }] }]);
await cameraManager.setZoom(10);
t.deepEqual(applyConstraintsStub.lastCall.args, [{ advanced: [{ zoom: 9 }] }]);
await cameraManager.setZoom(0.25, 5);
t.deepEqual(applyConstraintsStub.lastCall.args, [{ advanced: [{ zoom: 7 }] }]);
});
test("triggerZoomStart & triggerZoomMove", async (t) => {
const barcodePickerGui = sinon.createStubInstance(BarcodePickerGui);
const cameraManager = new BarcodePickerCameraManager(triggerFatalErrorSpy, barcodePickerGui);
const touchStartEvent = {
preventDefault: sinon.spy(),
type: "touchstart"
};
const touchStart0xEvent = {
...touchStartEvent,
touches: [
{
screenX: 0,
screenY: 0
},
{
screenX: 0,
screenY: 0
}
]
};
const touchStart25xEvent = {
...touchStartEvent,
touches: [
{
screenX: 0,
screenY: 0
},
{
screenX: 25,
screenY: 0
}
]
};
const touchStart50xEvent = {
...touchStartEvent,
touches: [
{
screenX: 0,
screenY: 0
},
{
screenX: 50,
screenY: 0
}
]
};
cameraManager.triggerZoomStart({
...touchStartEvent,
touches: [1]
});
cameraManager.triggerZoomMove({
...touchStartEvent,
touches: [1]
});
cameraManager.triggerZoomStart(touchStart25xEvent);
const mediaTrackCapabilities = {
torch: true,
zoom: {
max: 9,
min: 1,
step: 0.1
}
};
const applyConstraintsStub = (fakeMediaStream(cameraManager, mediaTrackCapabilities).getVideoTracks()[0].applyConstraints);
t.true(applyConstraintsStub.notCalled);
cameraManager.triggerZoomStart(touchStart0xEvent);
await cameraManager.setTorchEnabled(true);
cameraManager.triggerZoomStart(touchStart0xEvent);
cameraManager.triggerZoomMove(touchStart0xEvent);
t.deepEqual(applyConstraintsStub.lastCall.args, [{ advanced: [{ zoom: 1 }] }]);
cameraManager.triggerZoomMove(touchStart25xEvent);
t.deepEqual(applyConstraintsStub.lastCall.args, [{ advanced: [{ zoom: 5 }] }]);
cameraManager.triggerZoomMove(touchStart50xEvent);
t.deepEqual(applyConstraintsStub.lastCall.args, [{ advanced: [{ zoom: 9 }] }]);
cameraManager.triggerZoomStart(touchStart25xEvent);
cameraManager.triggerZoomMove(touchStart0xEvent);
t.deepEqual(applyConstraintsStub.lastCall.args, [{ advanced: [{ zoom: 5 }] }]);
});
// tslint:disable-next-line:max-func-body-length
test("manual / auto focus", async (t) => {
const barcodePickerGui = sinon.createStubInstance(BarcodePickerGui);
const cameraManager = new BarcodePickerCameraManager(triggerFatalErrorSpy, barcodePickerGui);
cameraManager.triggerManualFocus({
preventDefault: sinon.spy(),
type: "touchend",
touches: [1, 2]
});
cameraManager.pinchToZoomDistance = 1;
cameraManager.triggerManualFocus({
preventDefault: sinon.spy(),
type: "mousedown"
});
cameraManager.triggerManualFocus({
preventDefault: sinon.spy(),
type: "touchend",
touches: []
});
// Trigger manual focus when not supported
let mediaTrackCapabilities = {};
let applyConstraintsStub = (fakeMediaStream(cameraManager, mediaTrackCapabilities).getVideoTracks()[0].applyConstraints);
t.true(applyConstraintsStub.notCalled);
cameraManager.triggerManualFocus();
t.true(applyConstraintsStub.notCalled);
mediaTrackCapabilities = {
focusMode: [MeteringMode.SINGLE_SHOT, MeteringMode.CONTINUOUS] // this is a weird mix
};
applyConstraintsStub = (fakeMediaStream(cameraManager, mediaTrackCapabilities).getVideoTracks()[0].applyConstraints);
t.true(applyConstraintsStub.notCalled);
cameraManager.triggerManualFocus();
t.true(applyConstraintsStub.notCalled);
// Trigger manual focus when single-shot only is supported
mediaTrackCapabilities = {
focusMode: [MeteringMode.SINGLE_SHOT]
};
applyConstraintsStub = (fakeMediaStream(cameraManager, mediaTrackCapabilities).getVideoTracks()[0].applyConstraints);
t.true(applyConstraintsStub.notCalled);
cameraManager.triggerManualFocus();
t.true(applyConstraintsStub.calledOnce);
t.true(applyConstraintsStub.calledWith({ advanced: [{ focusMode: MeteringMode.SINGLE_SHOT }] }));
// Enable background single-shot autofocus
applyConstraintsStub.resetHistory();
cameraManager.storeStreamCapabilities();
cameraManager.setupAutofocus();
await wait(BarcodePickerCameraManager.autofocusIntervalMs * 4);
t.true(applyConstraintsStub.callCount >= 2);
t.true(applyConstraintsStub.alwaysCalledWith({ advanced: [{ focusMode: MeteringMode.SINGLE_SHOT }] }));
// Trigger manual focus when single-shot only is supported (while background single-shot autofocus is active)
cameraManager.triggerManualFocus();
applyConstraintsStub.resetHistory();
// Background single-shot autofocus should be disabled for a while
await wait(BarcodePickerCameraManager.autofocusIntervalMs * 2);
t.true(applyConstraintsStub.notCalled);
await wait(BarcodePickerCameraManager.manualToAutofocusResumeTimeoutMs * 2);
// Background single-shot autofocus should be enabled now
t.true(applyConstraintsStub.called);
t.true(applyConstraintsStub.alwaysCalledWith({ advanced: [{ focusMode: MeteringMode.SINGLE_SHOT }] }));
cameraManager.stopStream();
// Trigger manual focus when all focus modes are supported
mediaTrackCapabilities = {
focusMode: [MeteringMode.SINGLE_SHOT, MeteringMode.CONTINUOUS, MeteringMode.MANUAL]
};
applyConstraintsStub = (fakeMediaStream(cameraManager, mediaTrackCapabilities).getVideoTracks()[0].applyConstraints);
cameraManager.triggerManualFocus();
t.true(applyConstraintsStub.calledOnce);
t.true(applyConstraintsStub.calledWith({ advanced: [{ focusMode: MeteringMode.CONTINUOUS }] }));
applyConstraintsStub.resetHistory();
await wait(BarcodePickerCameraManager.manualFocusWaitTimeoutMs * 2);
t.true(applyConstraintsStub.calledOnce);
t.true(applyConstraintsStub.calledWith({ advanced: [{ focusMode: MeteringMode.MANUAL }] }));
applyConstraintsStub.resetHistory();
await wait(BarcodePickerCameraManager.manualToAutofocusResumeTimeoutMs * 2);
t.true(applyConstraintsStub.calledOnce);
t.true(applyConstraintsStub.calledWith({ advanced: [{ focusMode: MeteringMode.CONTINUOUS }] }));
});
// tslint:disable-next-line:max-func-body-length
test.serial("setupCameras", async (t) => {
const barcodePickerGui = sinon.createStubInstance(BarcodePickerGui);
const videoElementRemoveEventListener = sinon.spy();
barcodePickerGui.videoElement = {
loadedmetadataEventListener: null,
addEventListener(eventType, listener) {
if (eventType === "loadedmetadata") {
this.loadedmetadataEventListener = listener;
}
},
removeEventListener: videoElementRemoveEventListener,
dispatchEvent: sinon.spy()
};
barcodePickerGui.videoElement.load = function () {
this.loadedmetadataEventListener();
this.videoWidth = 640;
this.videoHeight = 480;
this.currentTime = 0;
this.onloadeddata();
setTimeout(() => {
this.currentTime = 1;
}, BarcodePickerCameraManager.cameraMetadataCheckIntervalMs * 2);
};
const cameraManager = new BarcodePickerCameraManager(triggerFatalErrorSpy, barcodePickerGui);
cameraManager.setInteractionOptions(true, true, true, true);
t.true(cameraManager.isCameraSwitcherEnabled());
t.is(barcodePickerGui.setCameraSwitcherVisible.callCount, 0);
// Intentionally make optimistic initial back camera access fail
fakeAccessCameraStream("user");
fakeGetCameras(2, [Camera.Type.FRONT, Camera.Type.FRONT]);
await cameraManager.setupCameras();
t.is(barcodePickerGui.setCameraSwitcherVisible.callCount, 1);
barcodePickerGui.setCameraSwitcherVisible.resetHistory();
t.is(CameraAccess.accessCameraStream.callCount, 2);
t.is(CameraAccess.getCameras.callCount, 1);
cameraManager.selectedCamera = undefined;
fakeAccessCameraStream("user");
fakeGetCameras(2, [Camera.Type.BACK, Camera.Type.FRONT]);
await cameraManager.setupCameras();
t.is(barcodePickerGui.setCameraSwitcherVisible.callCount, 1);
barcodePickerGui.setCameraSwitcherVisible.resetHistory();
t.is(CameraAccess.accessCameraStream.callCount, 2);
t.is(CameraAccess.getCameras.callCount, 1);
cameraManager.selectedCamera = undefined;
fakeAccessCameraStream("user");
fakeGetCameras(0);
let error = await t.throwsAsync(cameraManager.setupCameras());
t.is(barcodePickerGui.setCameraSwitcherVisible.callCount, 0);
t.is(error.message, "No camera available");
// Access primary back camera in common triple camera setups
cameraManager.selectedCamera = undefined;
fakeAccessCameraStream("user");
fakeGetCameras(3, [Camera.Type.FRONT, Camera.Type.BACK, Camera.Type.BACK], ["", "camera2 2, facing back", "camera2 0, facing back"]);
await cameraManager.setupCameras();
t.not(cameraManager.selectedCamera, null);
t.is(cameraManager.selectedCamera.label, "camera2 0, facing back");
let mediaTrackCapabilities = {
torch: true
};
cameraManager.selectedCamera = undefined;
fakeAccessCameraStream("environment", mediaTrackCapabilities);
fakeGetCameras(1);
barcodePickerGui.setCameraSwitcherVisible.resetHistory();
await cameraManager.setupCameras();
t.is(barcodePickerGui.setCameraSwitcherVisible.callCount, 0);
t.is(CameraAccess.accessCameraStream.callCount, 2);
t.is(CameraAccess.getCameras.callCount, 1);
await wait(BarcodePickerCameraManager.getCapabilitiesTimeoutMs * 2);
t.deepEqual(cameraManager.mediaTrackCapabilities, mediaTrackCapabilities);
mediaTrackCapabilities = {
torch: false,
focusMode: [MeteringMode.SINGLE_SHOT]
};
cameraManager.selectedCamera = undefined;
fakeAccessCameraStream("environment", mediaTrackCapabilities);
fakeGetCameras(2);
await cameraManager.setupCameras();
t.is(barcodePickerGui.setCameraSwitcherVisible.callCount, 1);
t.is(CameraAccess.accessCameraStream.callCount, 2);
t.is(CameraAccess.getCameras.callCount, 1);
await wait(BarcodePickerCameraManager.getCapabilitiesTimeoutMs * 2);
t.deepEqual(cameraManager.mediaTrackCapabilities, mediaTrackCapabilities);
barcodePickerGui.videoElement.load = function () {
this.loadedmetadataEventListener();
this.videoWidth = 640;
this.videoHeight = 480;
this.currentTime = 0;
this.onloadeddata();
// Intentionally never have valid metadata
};
error = await t.throwsAsync(cameraManager.setupCameras());
t.is(error.message, "Could not initialize camera correctly");
barcodePickerGui.videoElement.load = function () {
this.loadedmetadataEventListener();
// Intentionally never call onloadeddata()
};
error = await t.throwsAsync(cameraManager.setupCameras());
t.is(error.message, "Could not initialize camera correctly");
fakeAccessCameraStreamFailure(new Error("Test error 1"));
fakeGetCameras(1);
cameraManager.selectedCamera = undefined;
error = await t.throwsAsync(cameraManager.setupCameras());
t.is(error.message, "Test error 1");
t.true(CameraAccess.accessCameraStream
.getCall(0)
.calledBefore(CameraAccess.getCameras.firstCall));
t.true(CameraAccess.accessCameraStream
.getCall(4)
.calledAfter(CameraAccess.getCameras.firstCall));
t.is(CameraAccess.accessCameraStream.callCount, 8); // 2 times 4 calls (resolution fallbacks)
t.is(CameraAccess.getCameras.callCount, 1);
fakeAccessCameraStreamFailure(new Error("Test error 2"));
fakeGetCameras(1);
error = await t.throwsAsync(cameraManager.setupCameras());
t.is(error.message, "Test error 2");
t.true(CameraAccess.accessCameraStream
.getCall(0)
.calledAfter(CameraAccess.getCameras.firstCall));
t.is(CameraAccess.accessCameraStream.callCount, 4); // 1 time 4 calls (resolution fallbacks)
t.is(CameraAccess.getCameras.callCount, 1);
});
//# sourceMappingURL=barcodePickerCameraManager.spec.js.map