gecko-dev/dom/camera/test/camera_common.js

468 lines
15 KiB
JavaScript

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");