function isDefinedObj(obj) { return typeof(obj) !== 'undefined' && obj != null; } function isDefined(obj) { return typeof(obj) !== 'undefined'; } /* This is a simple test suite class removing the need to write a lot of boilerplate for camera tests. It can manage the platform configurations for testing, any cleanup required, and common actions such as fetching the camera or waiting for the preview to be completed. To create the suite: var suite = new CameraTestSuite(); To add a test case to the suite: suite.test('test-name', function() { function startAutoFocus(p) { return suite.camera.autoFocus(); } return suite.getCamera() .then(startAutoFocus, suite.rejectGetCamera); }); Finally, to execute the test cases: suite.setup() .then(suite.run); Behind the scenes, suite configured the native camera to use the JS hardware, setup that hardware such that the getCamera would succeed, got a camera control reference and saved it to suite.camera, and after the tests were finished, it reset any modified state, released the camera object, and concluded the mochitest appropriately. */ function CameraTestSuite() { SimpleTest.waitForExplicitFinish(); this._window = window; this._document = document; this.viewfinder = document.getElementById('viewfinder'); this._tests = []; this.hwType = ''; /* Ensure that the this pointer is bound to all functions so that they may be used as promise resolve/reject handlers without any special effort, permitting code like this: getCamera().catch(suite.rejectGetCamera); instead of: getCamera().catch(suite.rejectGetCamera.bind(suite)); */ this.setup = this._setup.bind(this); this.teardown = this._teardown.bind(this); this.test = this._test.bind(this); this.run = this._run.bind(this); this.waitPreviewStarted = this._waitPreviewStarted.bind(this); this.waitParameterPush = this._waitParameterPush.bind(this); this.initJsHw = this._initJsHw.bind(this); this.getCamera = this._getCamera.bind(this); this.setLowMemoryPlatform = this._setLowMemoryPlatform.bind(this); this.logError = this._logError.bind(this); this.expectedError = this._expectedError.bind(this); this.expectedRejectGetCamera = this._expectedRejectGetCamera.bind(this); this.expectedRejectConfigure = this._expectedRejectConfigure.bind(this); this.expectedRejectAutoFocus = this._expectedRejectAutoFocus.bind(this); this.expectedRejectTakePicture = this._expectedRejectTakePicture.bind(this); this.expectedRejectStartRecording = this._expectedRejectStartRecording.bind(this); this.expectedRejectStopRecording = this._expectedRejectStopRecording.bind(this); this.rejectGetCamera = this._rejectGetCamera.bind(this); this.rejectConfigure = this._rejectConfigure.bind(this); this.rejectRelease = this._rejectRelease.bind(this); this.rejectAutoFocus = this._rejectAutoFocus.bind(this); this.rejectTakePicture = this._rejectTakePicture.bind(this); this.rejectStartRecording = this._rejectStartRecording.bind(this); this.rejectStopRecording = this._rejectStopRecording.bind(this); this.rejectPauseRecording = this._rejectPauseRecording.bind(this); this.rejectResumeRecording = this._rejectResumeRecording.bind(this); this.rejectPreviewStarted = this._rejectPreviewStarted.bind(this); var self = this; this._window.addEventListener('beforeunload', function() { if (isDefinedObj(self.viewfinder)) { self.viewfinder.srcObject = null; } self.hw = null; if (isDefinedObj(self.camera)) { ok(false, 'window unload triggered camera release instead of test completion'); self.camera.release(); self.camera = null; } }); } CameraTestSuite.prototype = { camera: null, hw: null, _lowMemSet: false, _reloading: false, _setupPermission: function(permission) { if (!SpecialPowers.hasPermission(permission, document)) { info("requesting " + permission + " permission"); SpecialPowers.addPermission(permission, true, document); this._reloading = true; } }, /* Returns a promise which is resolved when the test suite is ready to be executing individual test cases. One may provide the expected hardware type here if desired; the default is to use the JS test hardware. Use '' for the native emulated camera hardware. */ _setup: function(hwType) { /* Depending on how we run the mochitest, we may not have the necessary permissions yet. If we do need to request them, then we have to reload the window to ensure the reconfiguration propogated properly. */ this._setupPermission("camera"); this._setupPermission("device-storage:videos"); this._setupPermission("device-storage:videos-create"); this._setupPermission("device-storage:videos-write"); if (this._reloading) { window.location.reload(); return Promise.reject(); } info("has necessary permissions"); if (!isDefined(hwType)) { hwType = 'hardware'; } this._hwType = hwType; return new Promise(function(resolve, reject) { SpecialPowers.pushPrefEnv({'set': [['device.storage.prompt.testing', true]]}, function() { SpecialPowers.pushPrefEnv({'set': [['camera.control.test.permission', true]]}, function() { SpecialPowers.pushPrefEnv({'set': [['camera.control.test.enabled', hwType]]}, function() { resolve(); }); }); }); }); }, /* Returns a promise which is resolved when all of the SpecialPowers parameters that were set while testing are flushed. This includes camera.control.test.enabled and camera.control.test.is_low_memory. */ _teardown: function() { return new Promise(function(resolve, reject) { SpecialPowers.flushPrefEnv(function() { resolve(); }); }); }, /* Returns a promise which is resolved when the set low memory parameter is set. If no value is given, it defaults to true. This is intended to be used inside a test case at the beginning of its promise chain to configure the platform as desired. */ _setLowMemoryPlatform: function(val) { if (typeof(val) === 'undefined') { val = true; } if (this._lowMemSet === val) { return Promise.resolve(); } var self = this; return new Promise(function(resolve, reject) { SpecialPowers.pushPrefEnv({'set': [['camera.control.test.is_low_memory', val]]}, function() { self._lowMemSet = val; resolve(); }); }).catch(function(e) { return self.logError('set low memory ' + val + ' failed', e); }); }, /* Add a test case to the test suite to be executed later. */ _test: function(aName, aCb) { this._tests.push({ name: aName, cb: aCb }); }, /* Execute all test cases (after setup is called). */ _run: function() { if (this._reloading) { return; } var test = this._tests.shift(); var self = this; if (test) { info(test.name + ' started'); function runNextTest() { self.run(); } function resetLowMem() { return self.setLowMemoryPlatform(false); } function postTest(pass) { ok(pass, test.name + ' finished'); var camera = self.camera; self.viewfinder.srcObject = null; self.camera = null; if (!isDefinedObj(camera)) { return Promise.resolve(); } function handler(e) { ok(typeof(e) === 'undefined', 'camera released'); return Promise.resolve(); } return camera.release().then(handler).catch(handler); } this.initJsHw(); var testPromise; try { testPromise = test.cb(); if (!isDefinedObj(testPromise)) { testPromise = Promise.resolve(); } } catch(e) { ok(false, 'caught exception while running test: ' + e); testPromise = Promise.reject(e); } testPromise .then(function(p) { return postTest(true); }, function(e) { self.logError('unhandled error', e); return postTest(false); }) .then(resetLowMem, resetLowMem) .then(runNextTest, runNextTest); } else { ok(true, 'all tests completed'); var finish = SimpleTest.finish.bind(SimpleTest); this.teardown().then(finish, finish); } }, /* If the JS hardware is in use, get (and possibly initialize) the service XPCOM object. The native Gonk layers are able to get it via the same mechanism. Save a reference to it so that the test case may manipulate it as it sees fit in this.hw. Minimal setup is done for the test hardware such that the camera is able to be brought up without issue. This function has no effect if the JS hardware is not used. */ _initJsHw: function() { if (this._hwType === 'hardware') { this.hw = SpecialPowers.Cc['@mozilla.org/cameratesthardware;1'] .getService(SpecialPowers.Ci.nsICameraTestHardware); this.hw.reset(this._window); /* Minimum parameters required to get camera started */ this.hw.params['preview-size'] = '320x240'; this.hw.params['preview-size-values'] = '320x240'; this.hw.params['picture-size-values'] = '320x240'; } else { this.hw = null; } }, /* Returns a promise which resolves when the camera has been successfully opened with the given name and configuration. If no name is given, it uses the first camera in the list from the camera manager. */ _getCamera: function(name, config) { var cameraManager = navigator.mozCameras; if (!isDefined(name)) { name = cameraManager.getListOfCameras()[0]; } var self = this; return cameraManager.getCamera(name, config).then( function(p) { ok(isDefinedObj(p) && isDefinedObj(p.camera), 'got camera'); self.camera = p.camera; /* Ensure a followup promise can verify config by returning the same parameter again. */ return Promise.resolve(p); } ); }, /* Returns a promise which resolves when the camera has successfully started the preview and is bound to the given viewfinder object. Note that this requires that a video element be present with the ID 'viewfinder'. */ _waitPreviewStarted: function() { var self = this; return new Promise(function(resolve, reject) { function onPreviewStateChange(e) { try { if (e.newState === 'started') { ok(true, 'viewfinder is ready and playing'); self.camera.removeEventListener('previewstatechange', onPreviewStateChange); resolve(); } } catch(e) { reject(e); } } if (!isDefinedObj(self.viewfinder)) { reject(new Error('no viewfinder object')); return; } self.viewfinder.srcObject = self.camera; self.viewfinder.play(); self.camera.addEventListener('previewstatechange', onPreviewStateChange); }); }, /* Returns a promise which resolves when the camera hardware has received a push parameters request. This is useful when setting camera parameters from the application and you want confirmation when the operation is complete if there is no asynchronous notification provided. */ _waitParameterPush: function() { var self = this; return new Promise(function(resolve, reject) { self.hw.attach({ 'pushParameters': function() { self._window.setTimeout(resolve); } }); }); }, /* When an error occurs in the promise chain, all of the relevant rejection functions will be triggered. Most of the time however we only want the first rejection to be handled and then let the failure trickle down the chain to terminate the test. There is no way to exit a promise chain early so the convention is to handle the error in the first reject and then give an empty error for subsequent reject handlers so they know it is not for them. For example: function rejectSomething(e) { return suite.logError('something call failed'); } getCamera() .then(, suite.rejectGetCamera) .then(something) .then(, rejectSomething) If the getCamera promise is rejected, suite.rejectGetCamera reports an error, but rejectSomething remains silent. */ _logError: function(msg, e) { if (isDefined(e)) { ok(false, msg + ': ' + e); } // Make sure the error is undefined for later handlers return Promise.reject(); }, /* The reject handlers below are intended to be used when a test case does not expect a particular call to fail but otherwise does not require any special handling of that situation beyond failing the test case and logging why.*/ _rejectGetCamera: function(e) { return this.logError('get camera failed', e); }, _rejectConfigure: function(e) { return this.logError('set configuration failed', e); }, _rejectRelease: function(e) { return this.logError('release camera failed', e); }, _rejectAutoFocus: function(e) { return this.logError('auto focus failed', e); }, _rejectTakePicture: function(e) { return this.logError('take picture failed', e); }, _rejectStartRecording: function(e) { return this.logError('start recording failed', e); }, _rejectStopRecording: function(e) { return this.logError('stop recording failed', e); }, _rejectPauseRecording: function(e) { return this.logError('pause recording failed', e); }, _rejectResumeRecording: function(e) { return this.logError('resume recording failed', e); }, _rejectPreviewStarted: function(e) { return this.logError('preview start failed', e); }, /* The success handlers below are intended to be used when a test case does not expect a particular call to succed but otherwise does not require any special handling of that situation beyond failing the test case and logging why.*/ _expectedError: function(msg) { ok(false, msg); /* Since the original promise was technically resolved we actually want to pass up a rejection to try and end the test case sooner */ return Promise.reject(); }, _expectedRejectGetCamera: function(p) { /* Copy handle to ensure it gets released at the end of the test case */ self.camera = p.camera; return this.expectedError('expected get camera to fail'); }, _expectedRejectConfigure: function(p) { return this.expectedError('expected set configuration to fail'); }, _expectedRejectAutoFocus: function(p) { return this.expectedError('expected auto focus to fail'); }, _expectedRejectTakePicture: function(p) { return this.expectedError('expected take picture to fail'); }, _expectedRejectStartRecording: function(p) { return this.expectedError('expected start recording to fail'); }, _expectedRejectStopRecording: function(p) { return this.expectedError('expected stop recording to fail'); }, }; is(SpecialPowers.sanityCheck(), "foo", "SpecialPowers passed sanity check");