2015-03-01 21:48:37 +03:00
|
|
|
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);
|
2015-03-11 02:39:49 +03:00
|
|
|
this.expectedRejectConfigure = this._expectedRejectConfigure.bind(this);
|
2015-03-01 21:48:37 +03:00
|
|
|
this.expectedRejectAutoFocus = this._expectedRejectAutoFocus.bind(this);
|
|
|
|
this.expectedRejectTakePicture = this._expectedRejectTakePicture.bind(this);
|
2015-03-11 02:39:49 +03:00
|
|
|
this.expectedRejectStartRecording = this._expectedRejectStartRecording.bind(this);
|
|
|
|
this.expectedRejectStopRecording = this._expectedRejectStopRecording.bind(this);
|
2015-03-01 21:48:37 +03:00
|
|
|
this.rejectGetCamera = this._rejectGetCamera.bind(this);
|
2015-03-11 02:39:49 +03:00
|
|
|
this.rejectConfigure = this._rejectConfigure.bind(this);
|
2015-03-01 21:48:37 +03:00
|
|
|
this.rejectRelease = this._rejectRelease.bind(this);
|
|
|
|
this.rejectAutoFocus = this._rejectAutoFocus.bind(this);
|
|
|
|
this.rejectTakePicture = this._rejectTakePicture.bind(this);
|
2015-03-11 02:39:49 +03:00
|
|
|
this.rejectStartRecording = this._rejectStartRecording.bind(this);
|
|
|
|
this.rejectStopRecording = this._rejectStopRecording.bind(this);
|
2015-08-17 22:20:28 +03:00
|
|
|
this.rejectPauseRecording = this._rejectPauseRecording.bind(this);
|
|
|
|
this.rejectResumeRecording = this._rejectResumeRecording.bind(this);
|
2015-03-01 21:48:37 +03:00
|
|
|
this.rejectPreviewStarted = this._rejectPreviewStarted.bind(this);
|
|
|
|
|
|
|
|
var self = this;
|
|
|
|
this._window.addEventListener('beforeunload', function() {
|
|
|
|
if (isDefinedObj(self.viewfinder)) {
|
2015-07-14 17:12:31 +03:00
|
|
|
self.viewfinder.srcObject = null;
|
2015-03-01 21:48:37 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
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,
|
2015-03-11 02:39:49 +03:00
|
|
|
_reloading: false,
|
2015-03-01 21:48:37 +03:00
|
|
|
|
2015-03-17 20:01:24 +03:00
|
|
|
_setupPermission: function(permission) {
|
|
|
|
if (!SpecialPowers.hasPermission(permission, document)) {
|
|
|
|
info("requesting " + permission + " permission");
|
|
|
|
SpecialPowers.addPermission(permission, true, document);
|
|
|
|
this._reloading = true;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2015-03-01 21:48:37 +03:00
|
|
|
/* 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) {
|
2015-03-11 02:39:49 +03:00
|
|
|
/* 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. */
|
2015-03-17 20:01:24 +03:00
|
|
|
this._setupPermission("camera");
|
|
|
|
this._setupPermission("device-storage:videos");
|
|
|
|
this._setupPermission("device-storage:videos-create");
|
|
|
|
this._setupPermission("device-storage:videos-write");
|
|
|
|
|
|
|
|
if (this._reloading) {
|
2015-03-11 02:39:49 +03:00
|
|
|
window.location.reload();
|
|
|
|
return Promise.reject();
|
|
|
|
}
|
|
|
|
|
2015-03-17 20:01:24 +03:00
|
|
|
info("has necessary permissions");
|
2015-03-01 21:48:37 +03:00
|
|
|
if (!isDefined(hwType)) {
|
|
|
|
hwType = 'hardware';
|
|
|
|
}
|
|
|
|
|
|
|
|
this._hwType = hwType;
|
|
|
|
return new Promise(function(resolve, reject) {
|
2015-03-17 20:01:24 +03:00
|
|
|
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();
|
|
|
|
});
|
2015-03-11 02:39:49 +03:00
|
|
|
});
|
2015-03-01 21:48:37 +03:00
|
|
|
});
|
2014-07-13 19:50:48 +04:00
|
|
|
});
|
2015-03-01 21:48:37 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
/* 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();
|
|
|
|
});
|
2014-02-20 08:18:52 +04:00
|
|
|
});
|
2015-03-01 21:48:37 +03:00
|
|
|
},
|
2014-02-20 08:18:52 +04:00
|
|
|
|
2015-03-01 21:48:37 +03:00
|
|
|
/* 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;
|
2014-02-20 08:18:52 +04:00
|
|
|
}
|
2015-03-01 21:48:37 +03:00
|
|
|
|
|
|
|
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() {
|
2015-03-11 02:39:49 +03:00
|
|
|
if (this._reloading) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-03-01 21:48:37 +03:00
|
|
|
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;
|
2015-07-14 17:12:31 +03:00
|
|
|
self.viewfinder.srcObject = null;
|
2015-03-01 21:48:37 +03:00
|
|
|
self.camera = null;
|
|
|
|
|
|
|
|
if (!isDefinedObj(camera)) {
|
|
|
|
return Promise.resolve();
|
2014-02-20 08:18:52 +04:00
|
|
|
}
|
2015-03-01 21:48:37 +03:00
|
|
|
|
|
|
|
function handler(e) {
|
|
|
|
ok(typeof(e) === 'undefined', 'camera released');
|
|
|
|
return Promise.resolve();
|
|
|
|
}
|
|
|
|
|
|
|
|
return camera.release().then(handler).catch(handler);
|
2014-02-20 08:18:52 +04:00
|
|
|
}
|
2015-03-01 21:48:37 +03:00
|
|
|
|
|
|
|
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);
|
2014-02-20 08:18:52 +04:00
|
|
|
}
|
2015-03-01 21:48:37 +03:00
|
|
|
|
|
|
|
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);
|
2014-03-01 02:51:26 +04:00
|
|
|
}
|
2015-03-01 21:48:37 +03:00
|
|
|
},
|
2014-10-03 12:19:00 +04:00
|
|
|
|
2015-03-01 21:48:37 +03:00
|
|
|
/* 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;
|
2014-02-20 08:18:52 +04:00
|
|
|
}
|
2015-03-01 21:48:37 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
/* 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];
|
2014-02-20 08:18:52 +04:00
|
|
|
}
|
2015-03-01 21:48:37 +03:00
|
|
|
|
|
|
|
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);
|
2014-07-13 19:50:48 +04:00
|
|
|
}
|
2015-03-01 21:48:37 +03:00
|
|
|
);
|
|
|
|
},
|
|
|
|
|
|
|
|
/* 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;
|
2014-03-01 02:51:26 +04:00
|
|
|
}
|
2015-03-01 21:48:37 +03:00
|
|
|
|
2015-07-14 17:12:31 +03:00
|
|
|
self.viewfinder.srcObject = self.camera;
|
2015-03-01 21:48:37 +03:00
|
|
|
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);
|
2014-03-01 02:51:26 +04:00
|
|
|
}
|
2015-03-01 21:48:37 +03:00
|
|
|
// 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);
|
|
|
|
},
|
|
|
|
|
2015-03-11 02:39:49 +03:00
|
|
|
_rejectConfigure: function(e) {
|
|
|
|
return this.logError('set configuration failed', e);
|
|
|
|
},
|
|
|
|
|
2015-03-01 21:48:37 +03:00
|
|
|
_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);
|
|
|
|
},
|
|
|
|
|
2015-03-11 02:39:49 +03:00
|
|
|
_rejectStartRecording: function(e) {
|
|
|
|
return this.logError('start recording failed', e);
|
|
|
|
},
|
|
|
|
|
|
|
|
_rejectStopRecording: function(e) {
|
|
|
|
return this.logError('stop recording failed', e);
|
|
|
|
},
|
|
|
|
|
2015-08-17 22:20:28 +03:00
|
|
|
_rejectPauseRecording: function(e) {
|
|
|
|
return this.logError('pause recording failed', e);
|
|
|
|
},
|
|
|
|
|
|
|
|
_rejectResumeRecording: function(e) {
|
|
|
|
return this.logError('resume recording failed', e);
|
|
|
|
},
|
|
|
|
|
2015-03-01 21:48:37 +03:00
|
|
|
_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');
|
|
|
|
},
|
2014-03-01 02:51:26 +04:00
|
|
|
|
2015-03-11 02:39:49 +03:00
|
|
|
_expectedRejectConfigure: function(p) {
|
|
|
|
return this.expectedError('expected set configuration to fail');
|
|
|
|
},
|
|
|
|
|
2015-03-01 21:48:37 +03:00
|
|
|
_expectedRejectAutoFocus: function(p) {
|
|
|
|
return this.expectedError('expected auto focus to fail');
|
|
|
|
},
|
2014-02-20 08:18:52 +04:00
|
|
|
|
2015-03-01 21:48:37 +03:00
|
|
|
_expectedRejectTakePicture: function(p) {
|
|
|
|
return this.expectedError('expected take picture to fail');
|
|
|
|
},
|
2015-03-11 02:39:49 +03:00
|
|
|
|
|
|
|
_expectedRejectStartRecording: function(p) {
|
|
|
|
return this.expectedError('expected start recording to fail');
|
|
|
|
},
|
|
|
|
|
|
|
|
_expectedRejectStopRecording: function(p) {
|
|
|
|
return this.expectedError('expected stop recording to fail');
|
|
|
|
},
|
2015-03-01 21:48:37 +03:00
|
|
|
};
|
2014-02-20 08:18:52 +04:00
|
|
|
|
2015-05-13 02:52:01 +03:00
|
|
|
is(SpecialPowers.sanityCheck(), "foo", "SpecialPowers passed sanity check");
|