From c4cbf10976d50a514512d20779641087b633781d Mon Sep 17 00:00:00 2001 From: Christoph Oberhofer Date: Mon, 3 Oct 2016 14:46:01 -0400 Subject: [PATCH] Added tests for selecting camera; related to #128 --- karma-integration.conf.js | 1 + karma.conf.js | 1 + src/common/mediaDevices.js | 17 ++ src/input/camera_access.js | 36 ++-- test/mocks/mediaDevices.js | 36 ++++ test/spec/camera_access.spec.js | 255 ++++++++++++++++------------- test/spec/result_collector.spec.js | 155 +++++++++--------- webpack.config.js | 1 + webpack.node.config.js | 1 + 9 files changed, 292 insertions(+), 211 deletions(-) create mode 100644 src/common/mediaDevices.js create mode 100644 test/mocks/mediaDevices.js diff --git a/karma-integration.conf.js b/karma-integration.conf.js index 5bab904..b10e9f4 100644 --- a/karma-integration.conf.js +++ b/karma-integration.conf.js @@ -24,6 +24,7 @@ module.exports = function(config) { resolve: { modules: [ path.resolve('./src/input/'), + path.resolve('./src/common/'), 'node_modules' ] }, diff --git a/karma.conf.js b/karma.conf.js index 59b26e9..8c570ed 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -29,6 +29,7 @@ module.exports = function(config) { resolve: { modules: [ path.resolve('./src/input/'), + path.resolve('./test/mocks/'), 'node_modules' ] }, diff --git a/src/common/mediaDevices.js b/src/common/mediaDevices.js new file mode 100644 index 0000000..818b1fd --- /dev/null +++ b/src/common/mediaDevices.js @@ -0,0 +1,17 @@ + +export function enumerateDevices() { + if (navigator.mediaDevices + && typeof navigator.mediaDevices.enumerateDevices === 'function') { + return navigator.mediaDevices.enumerateDevices(); + } + return Promise.reject(new Error('enumerateDevices is not defined')); +}; + +export function getUserMedia(constraints) { + if (navigator.mediaDevices + && typeof navigator.mediaDevices.getUserMedia === 'function') { + return navigator.mediaDevices + .getUserMedia(constraints); + } + return Promise.reject(new Error('getUserMedia is not defined')); +} diff --git a/src/input/camera_access.js b/src/input/camera_access.js index 8203fdd..b11bb6b 100644 --- a/src/input/camera_access.js +++ b/src/input/camera_access.js @@ -1,4 +1,5 @@ import {omit, pick} from 'lodash'; +import {getUserMedia, enumerateDevices} from 'mediaDevices'; const facingMatching = { "user": /front/i, @@ -37,24 +38,19 @@ function waitForVideo(video) { * @param {Object} video */ function initCamera(video, constraints) { - if (navigator.mediaDevices - && typeof navigator.mediaDevices.getUserMedia === 'function') { - return navigator.mediaDevices - .getUserMedia(constraints) - .then((stream) => { - return new Promise((resolve) => { - streamRef = stream; - video.setAttribute("autoplay", 'true'); - video.srcObject = stream; - video.addEventListener('loadedmetadata', () => { - video.play(); - resolve(); - }); - }); - }) - .then(waitForVideo.bind(null, video)); - } - return Promise.reject(new Error('getUserMedia is not defined')); + return getUserMedia(constraints) + .then((stream) => { + return new Promise((resolve) => { + streamRef = stream; + video.setAttribute("autoplay", 'true'); + video.srcObject = stream; + video.addEventListener('loadedmetadata', () => { + video.play(); + resolve(); + }); + }); + }) + .then(waitForVideo.bind(null, video)); } function deprecatedConstraints(videoConstraints) { @@ -80,7 +76,7 @@ function pickDevice(constraints) { if (!facingMatch) { return Promise.resolve(constraints); } - return navigator.mediaDevices.enumerateDevices() + return enumerateDevices() .then(devices => { const selectedDeviceId = devices .filter(device => device.kind === 'videoinput' && facingMatch.test(device.label)) @@ -98,7 +94,7 @@ function pickDevice(constraints) { }); } -function pickConstraints(videoConstraints) { +export function pickConstraints(videoConstraints) { const normalizedConstraints = { audio: false, video: deprecatedConstraints(videoConstraints) diff --git a/test/mocks/mediaDevices.js b/test/mocks/mediaDevices.js new file mode 100644 index 0000000..2669e79 --- /dev/null +++ b/test/mocks/mediaDevices.js @@ -0,0 +1,36 @@ + +let devices = [], + stream, + _constraints, + _supported = true; + +export function enumerateDevices() { + console.log("enumerateDevices!!!!"); + return Promise.resolve(devices); +}; + +export function getUserMedia(constraints) { + console.log("getUserMedia!!!!"); + _constraints = constraints; + if (_supported) { + return Promise.resolve(stream); + } + return Promise.reject(new Error("das")); +} + +export function setDevices(newDevices) { + devices = [...newDevices]; +} + +export function setStream(newStream) { + stream = newStream; +} + +export function getConstraints() { + return _constraints; +} + +export function setSupported(supported) { + console.log("Supported: " + supported); + _supported = supported; +} diff --git a/test/spec/camera_access.spec.js b/test/spec/camera_access.spec.js index 85511b4..18bf11b 100644 --- a/test/spec/camera_access.spec.js +++ b/test/spec/camera_access.spec.js @@ -1,147 +1,172 @@ -import CameraAccess from '../../src/input/camera_access'; +import CameraAccess, {pickConstraints} from '../../src/input/camera_access'; +import {setDevices, setStream, getConstraints, setSupported} from 'mediaDevices'; var originalURL, originalMediaStreamTrack, video, - stream; - -beforeEach(function() { - var tracks = [{ - stop: function() {} - }]; - - originalURL = window.URL; - originalMediaStreamTrack = window.MediaStreamTrack; - window.MediaStreamTrack = {}; - window.URL = { - createObjectURL(stream) { - return stream; - } - }; - - stream = { - getVideoTracks: function() { - return tracks; - } - }; - sinon.spy(tracks[0], "stop"); - - video = { - srcObject: null, - addEventListener: function() {}, - removeEventListener: function() {}, - setAttribute: sinon.spy(), - play: function() {}, - videoWidth: 320, - videoHeight: 480 - }; - sinon.stub(video, "addEventListener", function(event, cb) { - cb(); - }); - sinon.stub(video, "play"); -}); + stream, + devices = []; -afterEach(function() { - window.URL = originalURL; - window.MediaStreamTrack = originalMediaStreamTrack; -}); - -describe('success', function() { +describe("camera_access", () => { beforeEach(function() { - sinon.stub(navigator.mediaDevices, "getUserMedia", function(constraints) { - return Promise.resolve(stream); + var tracks = [{ + stop: function() {} + }]; + + originalURL = window.URL; + originalMediaStreamTrack = window.MediaStreamTrack; + window.MediaStreamTrack = {}; + window.URL = { + createObjectURL(stream) { + return stream; + } + }; + + stream = { + getVideoTracks: function() { + return tracks; + } + }; + setStream(stream); + sinon.spy(tracks[0], "stop"); + + video = { + srcObject: null, + addEventListener: function() {}, + removeEventListener: function() {}, + setAttribute: sinon.spy(), + play: function() {}, + videoWidth: 320, + videoHeight: 480 + }; + sinon.stub(video, "addEventListener", function(event, cb) { + cb(); }); + sinon.stub(video, "play"); }); afterEach(function() { - navigator.mediaDevices.getUserMedia.restore(); + window.URL = originalURL; + window.MediaStreamTrack = originalMediaStreamTrack; }); - describe('request', function () { - it('should request the camera', function (done) { - CameraAccess.request(video, {}) - .then(function () { - expect(navigator.mediaDevices.getUserMedia.calledOnce).to.equal(true); - expect(video.srcObject).to.deep.equal(stream); - done(); - }) - }); - it("should allow deprecated constraints to be used", (done) => { - CameraAccess.request(video, { - width: 320, - height: 240, - facing: "user", - minAspectRatio: 2, - maxAspectRatio: 100 - }) - .then(function () { - const call = navigator.mediaDevices.getUserMedia.getCall(0), - args = call.args; - expect(call).to.be.defined; - expect(args[0].video.width).to.equal(320); - expect(args[0].video.height).to.equal(240); - expect(args[0].video.facingMode).to.equal("user"); - expect(args[0].video.aspectRatio).to.equal(2); - expect(args[0].video.facing).not.to.be.defined; - expect(args[0].video.minAspectRatio).not.to.be.defined; - expect(args[0].video.maxAspectRatio).not.to.be.defined; - done(); - }) + describe('success', function() { + describe('request', function () { + it('should request the camera', function (done) { + CameraAccess.request(video, {}) + .then(function () { + expect(video.srcObject).to.deep.equal(stream); + done(); + }) + }); + + it("should allow deprecated constraints to be used", (done) => { + CameraAccess.request(video, { + width: 320, + height: 240, + facing: "user", + minAspectRatio: 2, + maxAspectRatio: 100 + }) + .then(function () { + const constraints = getConstraints(); + expect(constraints.video.width).to.equal(320); + expect(constraints.video.height).to.equal(240); + expect(constraints.video.facingMode).to.equal("user"); + expect(constraints.video.aspectRatio).to.equal(2); + expect(constraints.video.facing).not.to.be.defined; + expect(constraints.video.minAspectRatio).not.to.be.defined; + expect(constraints.video.maxAspectRatio).not.to.be.defined; + done(); + }) + }); }); - }); - describe('release', function () { - it('should release the camera', function (done) { - CameraAccess.request(video, {}) - .then(function () { - expect(video.srcObject).to.deep.equal(stream); - CameraAccess.release(); - expect(video.srcObject.getVideoTracks()).to.have.length(1); - expect(video.srcObject.getVideoTracks()[0].stop.calledOnce).to.equal(true); - done(); + describe('release', function () { + it('should release the camera', function (done) { + CameraAccess.request(video, {}) + .then(function () { + expect(video.srcObject).to.deep.equal(stream); + CameraAccess.release(); + expect(video.srcObject.getVideoTracks()).to.have.length(1); + expect(video.srcObject.getVideoTracks()[0].stop.calledOnce).to.equal(true); + done(); + }); }); }); }); -}); -describe('failure', function() { - describe("permission denied", function(){ - beforeEach(function() { - sinon.stub(navigator.mediaDevices, "getUserMedia", function(constraints, success, failure) { - return Promise.reject(new Error()); - }); + describe('failure', function() { + beforeEach(() => { + setSupported(false); }); - afterEach(function() { - navigator.mediaDevices.getUserMedia.restore(); + afterEach(() => { + setSupported(true); }); - it('should throw if getUserMedia not available', function(done) { - CameraAccess.request(video, {}) - .catch(function (err) { - expect(err).to.be.defined; - done(); + describe("permission denied", function(){ + it('should throw if getUserMedia not available', function(done) { + CameraAccess.request(video, {}) + .catch(function (err) { + expect(err).to.be.defined; + done(); + }); }); }); - }); - describe("not available", function(){ - var originalGetUserMedia; + describe("not available", function(){ + var originalGetUserMedia; - beforeEach(function() { - originalGetUserMedia = navigator.mediaDevices.getUserMedia; - navigator.mediaDevices.getUserMedia = undefined; + it('should throw if getUserMedia not available', function(done) { + CameraAccess.request(video, {}) + .catch((err) => { + expect(err).to.be.defined; + done(); + }); + }); }); - afterEach(function() { - navigator.mediaDevices.getUserMedia = originalGetUserMedia; - }); + describe("pickConstraints", () => { + it("should return the given constraints if no facingMode is defined", (done) => { + const givenConstraints = {width: 180}; + return pickConstraints(givenConstraints).then((actualConstraints) => { + expect(actualConstraints.video).to.deep.equal(givenConstraints); + done(); + }) + .catch((err) => { + expect(err).to.equal(null); + console.log(err); + done(); + }); + }); + + it("should return the given constraints if deviceId is defined", (done) => { + const givenConstraints = {width: 180, deviceId: "4343"}; + return pickConstraints(givenConstraints).then((actualConstraints) => { + expect(actualConstraints.video).to.deep.equal(givenConstraints); + done(); + }) + .catch((err) => { + expect(err).to.equal(null); + console.log(err); + done(); + }); + }); - it('should throw if getUserMedia not available', function(done) { - CameraAccess.request(video, {}) - .catch((err) => { - expect(err).to.be.defined; - done(); + it("should set deviceId if facingMode is set to environment", (done) => { + setDevices([{deviceId: "front", kind: "videoinput", label: "front Facing"}, + {deviceId: "back", label: "back Facing", kind: "videoinput"}]); + const givenConstraints = {width: 180, facingMode: "environment"}; + return pickConstraints(givenConstraints).then((actualConstraints) => { + expect(actualConstraints.video).to.deep.equal({width: 180, deviceId: "back"}); + done(); + }) + .catch((err) => { + console.log(err); + expect(err).to.equal(null); + done(); + }); }); }); }); diff --git a/test/spec/result_collector.spec.js b/test/spec/result_collector.spec.js index ef45678..3ffe18f 100644 --- a/test/spec/result_collector.spec.js +++ b/test/spec/result_collector.spec.js @@ -5,99 +5,102 @@ var canvasMock, imageSize, config; -beforeEach(function() { - imageSize = {x: 320, y: 240}; - config = { - capture: true, - capacity: 20, - blacklist: [{code: "3574660239843", format: "ean_13"}], - filter: function() { - return true; - } - }; - canvasMock = { - getContext: function() { - return {}; - }, - toDataURL: sinon.spy(), - width: 0, - height: 0 - }; - sinon.stub(document, "createElement", function(type) { - if (type === "canvas") { - return canvasMock; - } - }); -}); - -afterEach(function() { - document.createElement.restore(); -}); - - -describe('create', function () { - it("should return a new collector", function() { - ResultCollector.create(config); - expect(document.createElement.calledOnce).to.be.equal(true); - expect(document.createElement.getCall(0).args[0]).to.equal("canvas"); - }); -}); -describe('addResult', function() { +describe("resultCollector", () => { beforeEach(function() { - sinon.stub(ImageDebug, "drawImage", function() {}); + imageSize = {x: 320, y: 240}; + config = { + capture: true, + capacity: 20, + blacklist: [{code: "3574660239843", format: "ean_13"}], + filter: function() { + return true; + } + }; + canvasMock = { + getContext: function() { + return {}; + }, + toDataURL: sinon.spy(), + width: 0, + height: 0 + }; + sinon.stub(document, "createElement", function(type) { + if (type === "canvas") { + return canvasMock; + } + }); }); afterEach(function() { - ImageDebug.drawImage.restore(); + document.createElement.restore(); }); - it("should not add result if capacity is full", function(){ - config.capacity = 1; - var collector = ResultCollector.create(config); - collector.addResult([], imageSize, {}); - collector.addResult([], imageSize, {}); - collector.addResult([], imageSize, {}); - expect(collector.getResults()).to.have.length(1); + + describe('create', function () { + it("should return a new collector", function() { + ResultCollector.create(config); + expect(document.createElement.calledOnce).to.be.equal(true); + expect(document.createElement.getCall(0).args[0]).to.equal("canvas"); + }); }); - it("should only add results which match constraints", function(){ - var collector = ResultCollector.create(config), - results; + describe('addResult', function() { + beforeEach(function() { + sinon.stub(ImageDebug, "drawImage", function() {}); + }); + + afterEach(function() { + ImageDebug.drawImage.restore(); + }); + + it("should not add result if capacity is full", function(){ + config.capacity = 1; + var collector = ResultCollector.create(config); + collector.addResult([], imageSize, {}); + collector.addResult([], imageSize, {}); + collector.addResult([], imageSize, {}); + expect(collector.getResults()).to.have.length(1); + }); - collector.addResult([], imageSize, {code: "423423443", format: "ean_13"}); - collector.addResult([], imageSize, {code: "3574660239843", format: "ean_13"}); - collector.addResult([], imageSize, {code: "3574660239843", format: "code_128"}); + it("should only add results which match constraints", function(){ + var collector = ResultCollector.create(config), + results; - results = collector.getResults(); - expect(results).to.have.length(2); + collector.addResult([], imageSize, {code: "423423443", format: "ean_13"}); + collector.addResult([], imageSize, {code: "3574660239843", format: "ean_13"}); + collector.addResult([], imageSize, {code: "3574660239843", format: "code_128"}); - results.forEach(function(result) { - expect(result).not.to.deep.equal(config.blacklist[0]); + results = collector.getResults(); + expect(results).to.have.length(2); + + results.forEach(function(result) { + expect(result).not.to.deep.equal(config.blacklist[0]); + }); }); - }); - it("should add result if no filter is set", function() { - delete config.filter; - var collector = ResultCollector.create(config); + it("should add result if no filter is set", function() { + delete config.filter; + var collector = ResultCollector.create(config); - collector.addResult([], imageSize, {code: "423423443", format: "ean_13"}); - expect(collector.getResults()).to.have.length(1); - }); + collector.addResult([], imageSize, {code: "423423443", format: "ean_13"}); + expect(collector.getResults()).to.have.length(1); + }); - it("should not add results if filter returns false", function() { - config.filter = () => (false); - var collector = ResultCollector.create(config); + it("should not add results if filter returns false", function() { + config.filter = () => (false); + var collector = ResultCollector.create(config); - collector.addResult([], imageSize, {code: "423423443", format: "ean_13"}); - expect(collector.getResults()).to.have.length(0); - }); + collector.addResult([], imageSize, {code: "423423443", format: "ean_13"}); + expect(collector.getResults()).to.have.length(0); + }); - it("should add result if no blacklist is set", function() { - delete config.blacklist; - var collector = ResultCollector.create(config); + it("should add result if no blacklist is set", function() { + delete config.blacklist; + var collector = ResultCollector.create(config); - collector.addResult([], imageSize, {code: "3574660239843", format: "ean_13"}); - expect(collector.getResults()).to.have.length(1); + collector.addResult([], imageSize, {code: "3574660239843", format: "ean_13"}); + expect(collector.getResults()).to.have.length(1); + }); }); -}); +}) diff --git a/webpack.config.js b/webpack.config.js index afc23cb..9944a41 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -17,6 +17,7 @@ module.exports = { resolve: { modules: [ path.resolve('./src/input/'), + path.resolve('./src/common/'), 'node_modules' ] }, diff --git a/webpack.node.config.js b/webpack.node.config.js index d32f36b..96ae73c 100644 --- a/webpack.node.config.js +++ b/webpack.node.config.js @@ -6,6 +6,7 @@ module.exports = require('./webpack.config.js'); module.exports.resolve = { modules: [ path.resolve('./lib/'), + path.resolve('./src/common/'), 'node_modules' ] };