/* -*- js-indent-level: 2; tab-width: 2; indent-tabs-mode: nil -*- */
/* vim:set ts=2 sw=2 sts=2 et: */
// Generally gTestPath should be set by the harness.
/* global gTestPath */
/**
* SimpleTest framework object.
* @class
*/
var SimpleTest = {};
var parentRunner =
null;
// Using a try/catch rather than SpecialPowers.Cu.isRemoteProxy() because
// it doesn't cover the case where an iframe is xorigin but fission is
// not enabled.
let isSameOrigin =
function (w) {
try {
w.top.TestRunner;
}
catch (e) {
if (e
instanceof DOMException) {
return false;
}
}
return true;
};
let isXOrigin = !isSameOrigin(window);
// Note: duplicated in browser-test.js . See also bug 1820150.
function isErrorOrException(err) {
// It'd be nice if we had either `Error.isError(err)` or `Error.isInstance(err)`
// but we don't, so do it ourselves:
if (!err) {
return false;
}
if (err
instanceof SpecialPowers.Ci.nsIException) {
return true;
}
try {
let glob = SpecialPowers.Cu.getGlobalForObject(err);
return err
instanceof glob.Error;
}
catch {
// getGlobalForObject can be upset if it doesn't get passed an object.
// Just do a standard instanceof check using this global and cross fingers:
}
return err
instanceof Error;
}
// In normal test runs, the window that has a TestRunner in its parent is
// the primary window. In single test runs, if there is no parent and there
// is no opener then it is the primary window.
var isSingleTestRun =
parent == window &&
!(opener || (window.arguments && window.arguments[0].SimpleTest));
try {
var isPrimaryTestWindow =
(isXOrigin && parent != window && parent == top) ||
(!isXOrigin && (!!parent.TestRunner || isSingleTestRun));
}
catch (e) {
dump(
"TEST-UNEXPECTED-FAIL, Exception caught: " +
e.message +
", at: " +
e.fileName +
" (" +
e.lineNumber +
"), location: " +
window.location.href +
"\n"
);
}
let xOriginRunner = {
init(harnessWindow) {
this.harnessWindow = harnessWindow;
let url =
new URL(document.URL);
this.testFile = url.pathname;
this.showTestReport = url.searchParams.get(
"showTestReport") ==
"true";
this.expected = url.searchParams.get(
"expected");
},
callHarnessMethod(applyOn, command, ...params) {
// Message handled by xOriginTestRunnerHandler in TestRunner.js
this.harnessWindow.postMessage(
{
harnessType:
"SimpleTest",
applyOn,
command,
params,
},
"*"
);
},
getParameterInfo() {
let url =
new URL(document.URL);
return {
currentTestURL: url.searchParams.get(
"currentTestURL"),
testRoot: url.searchParams.get(
"testRoot"),
};
},
addFailedTest(test) {
this.callHarnessMethod(
"runner",
"addFailedTest", test);
},
expectAssertions(min, max) {
this.callHarnessMethod(
"runner",
"expectAssertions", min, max);
},
expectChildProcessCrash() {
this.callHarnessMethod(
"runner",
"expectChildProcessCrash");
},
requestLongerTimeout(factor) {
this.callHarnessMethod(
"runner",
"requestLongerTimeout", factor);
},
_lastAssertionCount: 0,
testFinished(tests) {
var newAssertionCount = SpecialPowers.assertionCount();
var numAsserts = newAssertionCount -
this._lastAssertionCount;
this._lastAssertionCount = newAssertionCount;
this.callHarnessMethod(
"runner",
"addAssertionCount", numAsserts);
this.callHarnessMethod(
"runner",
"testFinished", tests);
},
structuredLogger: {
info(msg) {
xOriginRunner.callHarnessMethod(
"logger",
"structuredLogger.info", msg);
},
warning(msg) {
xOriginRunner.callHarnessMethod(
"logger",
"structuredLogger.warning",
msg
);
},
error(msg) {
xOriginRunner.callHarnessMethod(
"logger",
"structuredLogger.error", msg);
},
activateBuffering() {
xOriginRunner.callHarnessMethod(
"logger",
"structuredLogger.activateBuffering"
);
},
deactivateBuffering() {
xOriginRunner.callHarnessMethod(
"logger",
"structuredLogger.deactivateBuffering"
);
},
testStatus(url, subtest, status, expected, diagnostic, stack) {
xOriginRunner.callHarnessMethod(
"logger",
"structuredLogger.testStatus",
url,
subtest,
status,
expected,
diagnostic,
stack
);
},
},
};
// Finds the TestRunner for this test run and the SpecialPowers object (in
// case it is not defined) from a parent/opener window.
//
// Finding the SpecialPowers object is needed when we have ChromePowers in
// harness.xhtml and we need SpecialPowers in the iframe, and also for tests
// like test_focus.xhtml where we open a window which opens another window which
// includes SimpleTest.js.
(
function () {
function ancestor(w) {
return w.parent != w
? w.parent
: w.opener ||
(!isXOrigin &&
w.arguments &&
SpecialPowers.wrap(Window).isInstance(w.arguments[0]) &&
w.arguments[0]);
}
var w = ancestor(window);
while (w && !parentRunner) {
isXOrigin = !isSameOrigin(w);
if (isXOrigin) {
if (w.parent != w) {
w = w.top;
}
xOriginRunner.init(w);
parentRunner = xOriginRunner;
}
if (!parentRunner) {
parentRunner = w.TestRunner;
if (!parentRunner && w.wrappedJSObject) {
parentRunner = w.wrappedJSObject.TestRunner;
}
}
w = ancestor(w);
}
if (parentRunner) {
SimpleTest.harnessParameters = parentRunner.getParameterInfo();
}
})();
/* Helper functions pulled out of various MochiKit modules */
if (
typeof repr ==
"undefined") {
this.repr =
function repr(o) {
if (
typeof o ==
"undefined") {
return "undefined";
}
else if (o ===
null) {
return "null";
}
try {
if (
typeof o.__repr__ ==
"function") {
return o.__repr__();
}
else if (
typeof o.repr ==
"function" && o.repr != repr) {
return o.repr();
}
}
catch (e) {}
try {
if (
typeof o.NAME ==
"string" &&
(o.toString ==
Function.prototype.toString ||
o.toString == Object.prototype.toString)
) {
return o.NAME;
}
}
catch (e) {}
var ostring;
try {
if (o === 0) {
ostring = 1 / o > 0 ?
"+0" :
"-0";
}
else if (
typeof o ===
"string") {
ostring = JSON.stringify(o);
}
else if (Array.isArray(o)) {
ostring =
"[" + o.map(val => repr(val)).join(
", ") +
"]";
}
else {
ostring = o +
"";
}
}
catch (e) {
return "[" +
typeof o +
"]";
}
if (
typeof o ==
"function") {
o = ostring.replace(/^\s+/,
"");
var idx = o.indexOf(
"{");
if (idx != -1) {
o = o.substr(0, idx) +
"{...}";
}
}
return ostring;
};
}
/* This returns a function that applies the previously given parameters.
* This is used by SimpleTest.showReport
*/
if (
typeof partial ==
"undefined") {
this.partial =
function (func) {
var args = [];
for (let i = 1; i < arguments.length; i++) {
args.push(arguments[i]);
}
return function () {
if (arguments.length) {
for (let i = 1; i < arguments.length; i++) {
args.push(arguments[i]);
}
}
func(args);
};
};
}
if (
typeof getElement ==
"undefined") {
this.getElement =
function (id) {
return typeof id ==
"string" ? document.getElementById(id) : id;
};
this.$ =
this.getElement;
}
SimpleTest._newCallStack =
function (path) {
var rval =
function callStackHandler() {
var callStack = callStackHandler.callStack;
for (
var i = 0; i < callStack.length; i++) {
if (callStack[i].apply(
this, arguments) ===
false) {
break;
}
}
try {
this[path] =
null;
}
catch (e) {
// pass
}
};
rval.callStack = [];
return rval;
};
if (
typeof addLoadEvent ==
"undefined") {
this.addLoadEvent =
function (func) {
var existing = window.onload;
var regfunc = existing;
if (
!(
typeof existing ==
"function" &&
typeof existing.callStack ==
"object" &&
existing.callStack !==
null
)
) {
regfunc = SimpleTest._newCallStack(
"onload");
if (
typeof existing ==
"function") {
regfunc.callStack.push(existing);
}
window.onload = regfunc;
}
regfunc.callStack.push(func);
};
}
function createEl(type, attrs, html) {
//use createElementNS so the xul/xhtml tests have no issues
var el;
if (!document.body) {
el = document.createElementNS(
"http://www.w3.org/1999/xhtml", type);
}
else {
el = document.createElement(type);
}
if (attrs !==
null && attrs !== undefined) {
for (
var k in attrs) {
el.setAttribute(k, attrs[k]);
}
}
if (html !==
null && html !== undefined) {
el.appendChild(document.createTextNode(html));
}
return el;
}
/* lots of tests use this as a helper to get css properties */
if (
typeof computedStyle ==
"undefined") {
this.computedStyle =
function (elem, cssProperty) {
elem = getElement(elem);
if (elem.currentStyle) {
return elem.currentStyle[cssProperty];
}
if (
typeof document.defaultView ==
"undefined" || document ===
null) {
return undefined;
}
var style = document.defaultView.getComputedStyle(elem);
if (
typeof style ==
"undefined" || style ===
null) {
return undefined;
}
var selectorCase = cssProperty.replace(/([A-Z])/g,
"-$1").toLowerCase();
return style.getPropertyValue(selectorCase);
};
}
SimpleTest._tests = [];
SimpleTest._stopOnLoad =
true;
SimpleTest._cleanupFunctions = [];
SimpleTest._taskCleanupFunctions = [];
SimpleTest._currentTask =
null;
SimpleTest._timeoutFunctions = [];
SimpleTest._inChaosMode =
false;
// When using failure pattern file to filter unexpected issues,
// SimpleTest.expected would be an array of [pattern, expected count],
// and SimpleTest.num_failed would be an array of actual counts which
// has the same length as SimpleTest.expected.
SimpleTest.expected =
"pass";
SimpleTest.num_failed = 0;
SpecialPowers.setAsDefaultAssertHandler();
function usesFailurePatterns() {
return Array.isArray(SimpleTest.expected);
}
/**
* Checks whether there is any failure pattern matches the given error
* message, and if found, bumps the counter of the failure pattern.
*
* @return {boolean} Whether a matched failure pattern is found.
*/
function recordIfMatchesFailurePattern(name, diag) {
let index = SimpleTest.expected.findIndex(([pat]) => {
return (
pat ==
null ||
(
typeof name ==
"string" && name.includes(pat)) ||
(
typeof diag ==
"string" && diag.includes(pat))
);
});
if (index >= 0) {
SimpleTest.num_failed[index]++;
return true;
}
return false;
}
SimpleTest.setExpected =
function () {
if (!parentRunner) {
return;
}
if (!Array.isArray(parentRunner.expected)) {
SimpleTest.expected = parentRunner.expected;
}
else {
// Assertions are checked by the runner.
SimpleTest.expected = parentRunner.expected.filter(
([pat]) => pat !=
"ASSERTION"
);
SimpleTest.num_failed =
new Array(SimpleTest.expected.length);
SimpleTest.num_failed.fill(0);
}
};
SimpleTest.setExpected();
/**
* Something like assert.
**/
SimpleTest.ok =
function (condition, name) {
if (arguments.length > 2) {
const diag =
"Too many arguments passed to `ok(condition, name)`";
SimpleTest.record(
false, name, diag);
}
else {
SimpleTest.record(condition, name);
}
};
SimpleTest.record =
function (condition, name, diag, stack, expected) {
var test = { result: !!condition, name, diag };
let successInfo;
let failureInfo;
if (SimpleTest.expected ==
"fail") {
if (!test.result) {
SimpleTest.num_failed++;
test.result =
true;
}
successInfo = {
status:
"PASS",
expected:
"PASS",
message:
"TEST-PASS",
};
failureInfo = {
status:
"FAIL",
expected:
"FAIL",
message:
"TEST-KNOWN-FAIL",
};
}
else if (!test.result && usesFailurePatterns()) {
if (recordIfMatchesFailurePattern(name, diag)) {
test.result =
true;
// Add a mark for unexpected failures suppressed by failure pattern.
name =
"[suppressed] " + name;
}
successInfo = {
status:
"FAIL",
expected:
"FAIL",
message:
"TEST-KNOWN-FAIL",
};
failureInfo = {
status:
"FAIL",
expected:
"PASS",
message:
"TEST-UNEXPECTED-FAIL",
};
}
else if (expected ==
"fail") {
successInfo = {
status:
"PASS",
expected:
"FAIL",
message:
"TEST-UNEXPECTED-PASS",
};
failureInfo = {
status:
"FAIL",
expected:
"FAIL",
message:
"TEST-KNOWN-FAIL",
};
}
else {
successInfo = {
status:
"PASS",
expected:
"PASS",
message:
"TEST-PASS",
};
failureInfo = {
status:
"FAIL",
expected:
"PASS",
message:
"TEST-UNEXPECTED-FAIL",
};
}
if (condition) {
stack =
null;
}
else if (!stack) {
stack =
new Error().stack
.replace(/^(.*@)http:\/\/mochi.test:8888\/tests\
//gm, " $1")
.split(
"\n");
stack.splice(0, 1);
stack = stack.join(
"\n");
}
SimpleTest._logResult(test, successInfo, failureInfo, stack);
SimpleTest._tests.push(test);
};
/**
* Roughly equivalent to ok(Object.is(a, b), name)
**/
SimpleTest.is =
function (a, b, name) {
// Be lazy and use Object.is til we want to test a browser without it.
var pass = Object.is(a, b);
var diag = pass ?
"" :
"got " + repr(a) +
", expected " + repr(b);
SimpleTest.record(pass, name, diag);
};
SimpleTest.isfuzzy =
function (a, b, epsilon, name) {
var pass = a >= b - epsilon && a <= b + epsilon;
var diag = pass
?
""
:
"got " +
repr(a) +
", expected " +
repr(b) +
" epsilon: +/- " +
repr(epsilon);
SimpleTest.record(pass, name, diag);
};
SimpleTest.isnot =
function (a, b, name) {
var pass = !Object.is(a, b);
var diag = pass ?
"" :
"didn't expect " + repr(a) +
", but got it";
SimpleTest.record(pass, name, diag);
};
/**
* Check that the function call throws an exception.
*/
SimpleTest.doesThrow =
function (fn, name) {
var gotException =
false;
try {
fn();
}
catch (ex) {
gotException =
true;
}
ok(gotException, name);
};
// --------------- Test.Builder/Test.More todo() -----------------
SimpleTest.todo =
function (condition, name, diag) {
var test = { result: !!condition, name, diag, todo:
true };
if (
test.result &&
usesFailurePatterns() &&
recordIfMatchesFailurePattern(name, diag)
) {
// Flipping the result to false so we don't get unexpected result. There
// is no perfect way here. A known failure can trigger unexpected pass,
// in which case, tagging it as KNOWN-FAIL probably makes more sense than
// marking it PASS.
test.result =
false;
// Add a mark for unexpected failures suppressed by failure pattern.
name =
"[suppressed] " + name;
}
var successInfo = {
status:
"PASS",
expected:
"FAIL",
message:
"TEST-UNEXPECTED-PASS",
};
var failureInfo = {
status:
"FAIL",
expected:
"FAIL",
message:
"TEST-KNOWN-FAIL",
};
SimpleTest._logResult(test, successInfo, failureInfo);
SimpleTest._tests.push(test);
};
/*
* Returns the absolute URL to a test data file from where tests
* are served. i.e. the file doesn't necessarely exists where tests
* are executed.
*
* (For android, mochitest are executed on the device, while
* all mochitest html (and others) files are served from the test runner
* slave)
*/
SimpleTest.getTestFileURL =
function (path) {
var location = window.location;
// Remove mochitest html file name from the path
var remotePath = location.pathname.replace(/\/[^\/]+?$/,
"");
var url = location.origin + remotePath +
"/" + path;
return url;
};
SimpleTest._getCurrentTestURL =
function () {
return (
(SimpleTest.harnessParameters &&
SimpleTest.harnessParameters.currentTestURL) ||
(parentRunner && parentRunner.currentTestURL) ||
(
typeof gTestPath ==
"string" && gTestPath) ||
"unknown test url"
);
};
SimpleTest._forceLogMessageOutput =
false;
/**
* Force all test messages to be displayed. Only applies for the current test.
*/
SimpleTest.requestCompleteLog =
function () {
if (!parentRunner || SimpleTest._forceLogMessageOutput) {
return;
}
parentRunner.structuredLogger.deactivateBuffering();
SimpleTest._forceLogMessageOutput =
true;
SimpleTest.registerCleanupFunction(
function () {
parentRunner.structuredLogger.activateBuffering();
SimpleTest._forceLogMessageOutput =
false;
});
};
SimpleTest._logResult =
function (test, passInfo, failInfo, stack) {
var url = SimpleTest._getCurrentTestURL();
var result = test.result ? passInfo : failInfo;
var diagnostic = test.diag ||
null;
// BUGFIX : coercing test.name to a string, because some a11y tests pass an xpconnect object
var subtest = test.name ? String(test.name) :
null;
var isError = !test.result == !test.todo;
if (parentRunner) {
if (!result.status || !result.expected) {
if (diagnostic) {
parentRunner.structuredLogger.info(diagnostic);
}
return;
}
if (isError) {
parentRunner.addFailedTest(url);
}
parentRunner.structuredLogger.testStatus(
url,
subtest,
result.status,
result.expected,
diagnostic,
stack
);
}
else if (
typeof dump ===
"function") {
var diagMessage = test.name + (test.diag ?
" - " + test.diag :
"");
var debugMsg = [result.message, url, diagMessage].join(
" | ");
dump(debugMsg +
"\n");
}
else {
// Non-Mozilla browser? Just do nothing.
}
};
SimpleTest.info =
function (name, message) {
var log = message ? name +
" | " + message : name;
if (parentRunner) {
parentRunner.structuredLogger.info(log);
}
else {
dump(log +
"\n");
}
};
/**
* Copies of is and isnot with the call to ok replaced by a call to todo.
**/
SimpleTest.todo_is =
function (a, b, name) {
var pass = Object.is(a, b);
var diag = pass
? repr(a) +
" should equal " + repr(b)
:
"got " + repr(a) +
", expected " + repr(b);
SimpleTest.todo(pass, name, diag);
};
SimpleTest.todo_isnot =
function (a, b, name) {
var pass = !Object.is(a, b);
var diag = pass
? repr(a) +
" should not equal " + repr(b)
:
"didn't expect " + repr(a) +
", but got it";
SimpleTest.todo(pass, name, diag);
};
/**
* Makes a test report, returns it as a DIV element.
**/
SimpleTest.report =
function () {
var passed = 0;
var failed = 0;
var todo = 0;
var tallyAndCreateDiv =
function (test) {
var cls, msg, div;
var diag = test.diag ?
" - " + test.diag :
"";
if (test.todo && !test.result) {
todo++;
cls =
"test_todo";
msg =
"todo | " + test.name + diag;
}
else if (test.result && !test.todo) {
passed++;
cls =
"test_ok";
msg =
"passed | " + test.name + diag;
}
else {
failed++;
cls =
"test_not_ok";
msg =
"failed | " + test.name + diag;
}
div = createEl(
"div", {
class: cls }, msg);
return div;
};
var results = [];
for (
var d = 0; d < SimpleTest._tests.length; d++) {
results.push(tallyAndCreateDiv(SimpleTest._tests[d]));
}
var summary_class =
// eslint-disable-next-line no-nested-ternary
failed != 0 ?
"some_fail" : passed == 0 ?
"todo_only" :
"all_pass";
var div1 = createEl(
"div", {
class:
"tests_report" });
var div2 = createEl(
"div", {
class:
"tests_summary " + summary_class });
var div3 = createEl(
"div", {
class:
"tests_passed" },
"Passed: " + passed);
var div4 = createEl(
"div", {
class:
"tests_failed" },
"Failed: " + failed);
var div5 = createEl(
"div", {
class:
"tests_todo" },
"Todo: " + todo);
div2.appendChild(div3);
div2.appendChild(div4);
div2.appendChild(div5);
div1.appendChild(div2);
for (
var t = 0; t < results.length; t++) {
//iterate in order
div1.appendChild(results[t]);
}
return div1;
};
/**
* Toggle element visibility
**/
SimpleTest.toggle =
function (el) {
if (computedStyle(el,
"display") ==
"block") {
el.style.display =
"none";
}
else {
el.style.display =
"block";
}
};
/**
* Toggle visibility for divs with a specific class.
**/
SimpleTest.toggleByClass =
function (cls, evt) {
var children = document.getElementsByTagName(
"div");
var elements = [];
for (
var i = 0; i < children.length; i++) {
var child = children[i];
var clsName = child.className;
if (!clsName) {
continue;
}
var classNames = clsName.split(
" ");
for (
var j = 0; j < classNames.length; j++) {
if (classNames[j] == cls) {
elements.push(child);
break;
}
}
}
for (
var t = 0; t < elements.length; t++) {
//TODO: again, for-in loop over elems seems to break this
SimpleTest.toggle(elements[t]);
}
if (evt) {
evt.preventDefault();
}
};
/**
* Shows the report in the browser
**/
SimpleTest.showReport =
function () {
var togglePassed = createEl(
"a", { href:
"#" },
"Toggle passed checks");
var toggleFailed = createEl(
"a", { href:
"#" },
"Toggle failed checks");
var toggleTodo = createEl(
"a", { href:
"#" },
"Toggle todo checks");
togglePassed.onclick = partial(SimpleTest.toggleByClass,
"test_ok");
toggleFailed.onclick = partial(SimpleTest.toggleByClass,
"test_not_ok");
toggleTodo.onclick = partial(SimpleTest.toggleByClass,
"test_todo");
var body = document.body;
// Handles HTML documents
if (!body) {
// Do the XML thing.
body = document.getElementsByTagNameNS(
"http://www.w3.org/1999/xhtml",
"body"
)[0];
}
var firstChild = body.childNodes[0];
var addNode;
if (firstChild) {
addNode =
function (el) {
body.insertBefore(el, firstChild);
};
}
else {
addNode =
function (el) {
body.appendChild(el);
};
}
addNode(togglePassed);
addNode(createEl(
"span",
null,
" "));
addNode(toggleFailed);
addNode(createEl(
"span",
null,
" "));
addNode(toggleTodo);
addNode(SimpleTest.report());
// Add a separator from the test content.
addNode(createEl(
"hr"));
};
/**
* Tells SimpleTest to don't finish the test when the document is loaded,
* useful for asynchronous tests.
*
* When SimpleTest.waitForExplicitFinish is called,
* explicit SimpleTest.finish() is required.
**/
SimpleTest.waitForExplicitFinish =
function () {
SimpleTest._stopOnLoad =
false;
};
/**
* Multiply the timeout the parent runner uses for this test by the
* given factor.
*
* For example, in a test that may take a long time to complete, using
* "SimpleTest.requestLongerTimeout(5)" will give it 5 times as long to
* finish.
*
* @param {Number} factor
* The multiplication factor to use on the timeout for this test.
*/
SimpleTest.requestLongerTimeout =
function (factor) {
if (parentRunner) {
parentRunner.requestLongerTimeout(factor);
}
else {
dump(
"[SimpleTest.requestLongerTimeout()] ignoring request, maybe you meant to call the global `requestLongerTimeout` instead?\n"
);
}
};
/**
* Note that the given range of assertions is to be expected. When
* this function is not called, 0 assertions are expected. When only
* one argument is given, that number of assertions are expected.
*
* A test where we expect to have assertions (which should largely be a
* transitional mechanism to get assertion counts down from our current
* situation) can call the SimpleTest.expectAssertions() function, with
* either one or two arguments: one argument gives an exact number
* expected, and two arguments give a range. For example, a test might do
* one of the following:
*
* @example
*
* // Currently triggers two assertions (bug NNNNNN).
* SimpleTest.expectAssertions(2);
*
* // Currently triggers one assertion on Mac (bug NNNNNN).
* if (navigator.platform.indexOf("Mac") == 0) {
* SimpleTest.expectAssertions(1);
* }
*
* // Currently triggers two assertions on all platforms (bug NNNNNN),
* // but intermittently triggers two additional assertions (bug NNNNNN)
* // on Windows.
* if (navigator.platform.indexOf("Win") == 0) {
* SimpleTest.expectAssertions(2, 4);
* } else {
* SimpleTest.expectAssertions(2);
* }
*
* // Intermittently triggers up to three assertions (bug NNNNNN).
* SimpleTest.expectAssertions(0, 3);
*/
SimpleTest.expectAssertions =
function (min, max) {
if (parentRunner) {
parentRunner.expectAssertions(min, max);
}
};
SimpleTest._flakyTimeoutIsOK =
false;
SimpleTest._originalSetTimeout = window.setTimeout;
window.setTimeout =
function SimpleTest_setTimeoutShim() {
// Don't break tests that are loaded without a parent runner.
if (parentRunner) {
// Right now, we only enable these checks for mochitest-plain.
switch (SimpleTest.harnessParameters.testRoot) {
case "browser":
case "chrome":
case "a11y":
break;
default:
if (
!SimpleTest._alreadyFinished &&
arguments.length > 1 &&
arguments[1] > 0
) {
if (SimpleTest._flakyTimeoutIsOK) {
SimpleTest.todo(
false,
"The author of the test has indicated that flaky timeouts are expected. Reason: " +
SimpleTest._flakyTimeoutReason
);
}
else {
SimpleTest.ok(
false,
"Test attempted to use a flaky timeout value " + arguments[1]
);
}
}
}
}
return SimpleTest._originalSetTimeout.apply(window, arguments);
};
/**
* Request the framework to allow usage of setTimeout(func, timeout)
* where ``timeout > 0``. This is required to note that the author of
* the test is aware of the inherent flakiness in the test caused by
* that, and asserts that there is no way around using the magic timeout
* value number for some reason.
*
* Use of this function is **STRONGLY** discouraged. Think twice before
* using it. Such magic timeout values could result in intermittent
* failures in your test, and are almost never necessary!
*
* @param {String} reason
* A string representation of the reason why the test needs timeouts.
*
*/
SimpleTest.requestFlakyTimeout =
function (reason) {
SimpleTest.is(
typeof reason,
"string",
"A valid string reason is expected");
SimpleTest.isnot(reason,
"",
"Reason cannot be empty");
SimpleTest._flakyTimeoutIsOK =
true;
SimpleTest._flakyTimeoutReason = reason;
};
/**
* If the page is not yet loaded, waits for the load event. If the page is
* not yet focused, focuses and waits for the window to be focused.
* If the current page is 'about:blank', then the page is assumed to not
* yet be loaded. Pass true for expectBlankPage to not make this assumption
* if you expect a blank page to be present.
*
* The target object should be specified if it is different than 'window'. The
* actual focused window may be a descendant window of aObject.
*
* @param {Window|browser|BrowsingContext} [aObject]
* Optional object to be focused, and may be any of:
* window - a window object to focus
* browser - a <browser>/<iframe> element. The top-level window
* within the frame will be focused.
* browsing context - a browsing context containing a window to focus
* If not specified, defaults to the global 'window'.
* @param {boolean} [expectBlankPage=false]
* True if targetWindow.location is 'about:blank'.
* @param {boolean} [aBlurSubframe=false]
* If true, and a subframe within the window to focus is focused, blur
* it so that the specified window or browsing context will receive
* focus events.
*
* @returns The browsing context that was focused.
*/
SimpleTest.promiseFocus = async
function (
aObject,
aExpectBlankPage =
false,
aBlurSubframe =
false
) {
let browser;
let browsingContext;
let windowToFocus;
if (!aObject) {
aObject = window;
}
async
function waitForEvent(aTarget, aEventName) {
return new Promise(resolve => {
aTarget.addEventListener(aEventName, resolve, {
capture:
true,
once:
true,
});
});
}
if (SpecialPowers.wrap(Window).isInstance(aObject)) {
windowToFocus = aObject;
let isBlank = windowToFocus.location.href ==
"about:blank";
if (
aExpectBlankPage != isBlank ||
windowToFocus.document.readyState !=
"complete"
) {
info(
"must wait for load");
await waitForEvent(windowToFocus,
"load");
}
}
else {
if (SpecialPowers.wrap(Element).isInstance(aObject)) {
// assume this is a browser/iframe element
browsingContext = aObject.browsingContext;
}
else {
browsingContext = aObject;
}
browser =
browsingContext == aObject ? aObject.top.embedderElement : aObject;
windowToFocus = browser.ownerGlobal;
}
if (!windowToFocus.document.hasFocus()) {
info(
"must wait for focus");
let focusPromise = waitForEvent(windowToFocus.document,
"focus");
SpecialPowers.focus(windowToFocus);
await focusPromise;
}
if (browser) {
if (windowToFocus.document.activeElement != browser) {
browser.focus();
}
info(
"must wait for focus in content");
// Make sure that the child process thinks it is focused as well.
await SpecialPowers.ensureFocus(browsingContext, aBlurSubframe);
}
else {
if (aBlurSubframe) {
SpecialPowers.clearFocus(windowToFocus);
}
browsingContext = windowToFocus.browsingContext;
}
// Some tests rely on this delay, likely expecting layout or paint to occur.
await
new Promise(resolve => {
SimpleTest.executeSoon(resolve);
});
return browsingContext;
};
/**
* Version of promiseFocus that uses a callback. For compatibility,
* the callback is passed one argument, the window that was focused.
* If the focused window is not in the same process, null is supplied.
*/
SimpleTest.waitForFocus =
function (callback, aObject, expectBlankPage) {
SimpleTest.promiseFocus(aObject, expectBlankPage).then(focusedBC => {
callback(focusedBC?.window);
});
};
/* eslint-enable mozilla/use-services */
SimpleTest.stripLinebreaksAndWhitespaceAfterTags =
function (aString) {
return aString.replace(/(>\s*(\r\n|\n|\r)*\s*)/gm,
">");
};
/*
* `navigator.platform` should include this, when the platform is Windows.
*/
const kPlatformWindows =
"Win";
/*
* See `SimpleTest.waitForClipboard`.
*/
const kTextHtmlPrefixClipboardDataWindows =
"\n";
/*
* See `SimpleTest.waitForClipboard`.
*/
const kTextHtmlSuffixClipboardDataWindows =
"\n\n";
/*
* Polls the clipboard waiting for the expected value. A known value different than
* the expected value is put on the clipboard first (and also polled for) so we
* can be sure the value we get isn't just the expected value because it was already
* on the clipboard. This only uses the global clipboard and only for text/plain
* values.
*
* @param {String|Function} aExpectedStringOrValidatorFn
* The string value that is expected to be on the clipboard, or a
* validator function getting expected clipboard data and returning a bool.
* If you specify string value, line breakers in clipboard are treated
* as LineFeed. Therefore, you cannot include CarriageReturn to the
* string.
* If you specify string value and expect "text/html" data, this wraps
* the expected value with `kTextHtmlPrefixClipboardDataWindows` and
* `kTextHtmlSuffixClipboardDataWindows` only when it runs on Windows
* because they are appended only by nsDataObj.cpp for Windows.
* https://searchfox.org/mozilla-central/rev/8f7b017a31326515cb467e69eef1f6c965b4f00e/widget/windows/nsDataObj.cpp#1798-1805,1839-1840,1842
* Therefore, you can specify selected (copied) HTML data simply on any
* platforms.
* @param {Function} aSetupFn
* A function responsible for setting the clipboard to the expected value,
* called after the known value setting succeeds.
* @param {Function} aSuccessFn
* A function called when the expected value is found on the clipboard.
* @param {Function} aFailureFn
* A function called if the expected value isn't found on the clipboard
* within 5s. It can also be called if the known value can't be found.
* @param {String} [aFlavor="text/plain"]
* The flavor to look for.
* @param {Number} [aTimeout=5000]
* The timeout (in milliseconds) to wait for a clipboard change.
* @param {boolean} [aExpectFailure=false]
* If true, fail if the clipboard contents are modified within the timeout
* interval defined by aTimeout. When aExpectFailure is true, the argument
* aExpectedStringOrValidatorFn must be null, as it won't be used.
* @param {boolean} [aDontInitializeClipboardIfExpectFailure=false]
* If aExpectFailure and this is set to true, this does NOT initialize
* clipboard with random data before running aSetupFn.
*/
SimpleTest.waitForClipboard =
function (
aExpectedStringOrValidatorFn,
aSetupFn,
aSuccessFn,
aFailureFn,
aFlavor,
aTimeout,
aExpectFailure,
aDontInitializeClipboardIfExpectFailure
) {
let promise = SimpleTest.promiseClipboardChange(
aExpectedStringOrValidatorFn,
aSetupFn,
aFlavor,
aTimeout,
aExpectFailure,
aDontInitializeClipboardIfExpectFailure
);
promise.then(aSuccessFn).
catch(aFailureFn);
};
/**
* Promise-oriented version of waitForClipboard.
*/
SimpleTest.promiseClipboardChange = async
function (
aExpectedStringOrValidatorFn,
aSetupFn,
aFlavor,
aTimeout,
aExpectFailure,
aDontInitializeClipboardIfExpectFailure
) {
let requestedFlavor = aFlavor ||
"text/plain";
// The known value we put on the clipboard before running aSetupFn
let initialVal =
"waitForClipboard-known-value-" + Math.random();
let preExpectedVal = initialVal;
let inputValidatorFn;
if (aExpectFailure) {
// If we expect failure, the aExpectedStringOrValidatorFn should be null
if (aExpectedStringOrValidatorFn !==
null) {
SimpleTest.ok(
false,
"When expecting failure, aExpectedStringOrValidatorFn must be null"
);
}
inputValidatorFn =
function (aData) {
return aData != initialVal;
};
// Build a default validator function for common string input.
}
else if (
typeof aExpectedStringOrValidatorFn ==
"string") {
if (aExpectedStringOrValidatorFn.includes(
"\r")) {
throw new Error(
"Use function instead of string to compare raw line breakers in clipboard"
);
}
if (requestedFlavor ===
"text/html" && navigator.platform.includes(
"Win")) {
inputValidatorFn =
function (aData) {
return (
aData.replace(/\r\n?/g,
"\n") ===
kTextHtmlPrefixClipboardDataWindows +
aExpectedStringOrValidatorFn +
kTextHtmlSuffixClipboardDataWindows
);
};
}
else {
inputValidatorFn =
function (aData) {
return aData.replace(/\r\n?/g,
"\n") === aExpectedStringOrValidatorFn;
};
}
}
else {
inputValidatorFn = aExpectedStringOrValidatorFn;
}
let maxPolls = aTimeout ? aTimeout / 100 : 50;
async
function putAndVerify(operationFn, validatorFn, flavor, expectFailure) {
await operationFn();
let data;
for (let i = 0; i < maxPolls; i++) {
data = SpecialPowers.getClipboardData(flavor);
if (validatorFn(data)) {
// Don't show the success message when waiting for preExpectedVal
if (preExpectedVal) {
preExpectedVal =
null;
}
else {
SimpleTest.ok(
!expectFailure,
"Clipboard has the given value: '" + data +
"'"
);
}
return data;
}
// Wait 100ms and check again.
await
new Promise(resolve => {
SimpleTest._originalSetTimeout.apply(window, [resolve, 100]);
});
}
let errorMsg = `Timed out
while polling clipboard
for ${
preExpectedVal ?
"initialized" :
"requested"
} data, got: ${data}`;
SimpleTest.ok(expectFailure, errorMsg);
if (!expectFailure) {
throw new Error(errorMsg);
}
return data;
}
if (!aExpectFailure || !aDontInitializeClipboardIfExpectFailure) {
// First we wait for a known value different from the expected one.
SimpleTest.info(`Initializing clipboard with
"${preExpectedVal}"...`);
await putAndVerify(
function () {
SpecialPowers.clipboardCopyString(preExpectedVal);
},
function (aData) {
return aData == preExpectedVal;
},
"text/plain",
false
);
SimpleTest.info(
"Succeeded initializing clipboard, start requested things..."
);
}
else {
preExpectedVal =
null;
}
return putAndVerify(
aSetupFn,
inputValidatorFn,
requestedFlavor,
aExpectFailure
);
};
/**
* Wait for a condition for a while (actually up to 3s here).
*
* @param {Function} aCond
* A function returns the result of the condition
* @param {Function} aCallback
* A function called after the condition is passed or timeout.
* @param {String} aErrorMsg
* The message displayed when the condition failed to pass
* before timeout.
*/
SimpleTest.waitForCondition =
function (aCond, aCallback, aErrorMsg) {
this.promiseWaitForCondition(aCond, aErrorMsg).then(() => aCallback());
};
SimpleTest.promiseWaitForCondition = async
function (aCond, aErrorMsg) {
for (let tries = 0; tries < 30; ++tries) {
// Wait 100ms between checks.
await
new Promise(resolve => {
SimpleTest._originalSetTimeout.apply(window, [resolve, 100]);
});
let conditionPassed;
try {
conditionPassed = await aCond();
}
catch (e) {
ok(
false, `${e}\n${e.stack}`);
conditionPassed =
false;
}
if (conditionPassed) {
return;
}
}
ok(
false, aErrorMsg);
};
/**
* Executes a function shortly after the call, but lets the caller continue
* working (or finish).
*
* @param {Function} aFunc
* Function to execute soon.
*/
SimpleTest.executeSoon =
function (aFunc) {
if (
"SpecialPowers" in window) {
return SpecialPowers.executeSoon(aFunc, window);
}
setTimeout(aFunc, 0);
return null;
// Avoid warning.
};
/**
* Register a cleanup/teardown function (which may be async) to run after all
* tasks have finished, before running the next test. If async (or the function
* returns a promise), the framework will wait for the promise/async function
* to resolve.
*
* @param {Function} aFunc
* The cleanup/teardown function to run.
*/
SimpleTest.registerCleanupFunction =
function (aFunc) {
SimpleTest._cleanupFunctions.push(aFunc);
};
/**
* Register a cleanup/teardown function (which may be async) to run after the
* current task has finished, before running the next task. If async (or the
* function returns a promise), the framework will wait for the promise/async
* function to resolve.
*
* @param {Function} aFunc
* The cleanup/teardown function to run.
*/
SimpleTest.registerCurrentTaskCleanupFunction =
function (aFunc) {
if (!SimpleTest._currentTask) {
return;
}
SimpleTest._currentTask._cleanupFunctions ||= [];
SimpleTest._currentTask._cleanupFunctions.push(aFunc);
};
/**
* Register a cleanup/teardown function (which may be async) to run after each
* task has finished, before running the next task. If async (or the
* function returns a promise), the framework will wait for the promise/async
* function to resolve.
*
* @param {Function} aFunc
* The cleanup/teardown function to run.
*/
SimpleTest.registerTaskCleanupFunction =
function (aFunc) {
SimpleTest._taskCleanupFunctions.push(aFunc);
};
SimpleTest.registerTimeoutFunction =
function (aFunc) {
SimpleTest._timeoutFunctions.push(aFunc);
};
SimpleTest.testInChaosMode =
function () {
if (SimpleTest._inChaosMode) {
// It's already enabled for this test, don't enter twice
return;
}
SpecialPowers.DOMWindowUtils.enterChaosMode();
SimpleTest._inChaosMode =
true;
// increase timeout here as chaosmode is very slow (i.e. 10x)
// doing 20x as this overwrites anything the tests set
SimpleTest.requestLongerTimeout(20);
};
SimpleTest.timeout = async
function () {
for (
const func of SimpleTest._timeoutFunctions) {
await func();
}
SimpleTest._timeoutFunctions = [];
};
SimpleTest.finishWithFailure =
function (msg) {
SimpleTest.ok(
false, msg);
SimpleTest.finish();
};
/**
* Finishes the tests. This is automatically called, except when
* SimpleTest.waitForExplicitFinish() has been invoked.
**/
SimpleTest.finish =
function () {
if (SimpleTest._alreadyFinished) {
var err =
"TEST-UNEXPECTED-FAIL | SimpleTest | this test already called finish!";
if (parentRunner) {
parentRunner.structuredLogger.error(err);
}
else {
dump(err +
"\n");
}
}
if (SimpleTest.expected ==
"fail" && SimpleTest.num_failed <= 0) {
let msg =
"We expected at least one failure";
let test = {
result:
false,
name:
"fail-if condition in manifest",
diag: msg,
};
let successInfo = {
status:
"FAIL",
expected:
"FAIL",
message:
"TEST-KNOWN-FAIL",
};
let failureInfo = {
status:
"PASS",
expected:
"FAIL",
message:
"TEST-UNEXPECTED-PASS",
};
SimpleTest._logResult(test, successInfo, failureInfo);
SimpleTest._tests.push(test);
}
else if (usesFailurePatterns()) {
SimpleTest.expected.forEach(([pat, expected_count], i) => {
let count = SimpleTest.num_failed[i];
let diag;
if (expected_count ===
null && count == 0) {
diag =
"expected some failures but got none";
}
else if (expected_count !==
null && expected_count != count) {
diag = `expected ${expected_count} failures but got ${count}`;
}
else {
return;
}
let name = pat
? `failure pattern \`${pat}\` in
this test`
:
"failures in this test";
let test = { result:
false, name, diag };
let successInfo = {
status:
"PASS",
expected:
"PASS",
message:
"TEST-PASS",
};
let failureInfo = {
status:
"FAIL",
expected:
"PASS",
message:
"TEST-UNEXPECTED-FAIL",
};
SimpleTest._logResult(test, successInfo, failureInfo);
SimpleTest._tests.push(test);
});
}
SimpleTest._timeoutFunctions = [];
SimpleTest.testsLength = SimpleTest._tests.length;
SimpleTest._alreadyFinished =
true;
if (SimpleTest._inChaosMode) {
SpecialPowers.DOMWindowUtils.leaveChaosMode();
SimpleTest._inChaosMode =
false;
}
var afterCleanup = async
function () {
SpecialPowers.removeFiles();
if (SpecialPowers.DOMWindowUtils.isTestControllingRefreshes) {
SimpleTest.ok(
false,
"test left refresh driver under test control");
SpecialPowers.DOMWindowUtils.restoreNormalRefresh();
}
if (SimpleTest._expectingUncaughtException) {
SimpleTest.ok(
false,
"expectUncaughtException was called but no uncaught exception was detected!"
);
}
if (!SimpleTest._tests.length) {
SimpleTest.ok(
false,
"[SimpleTest.finish()] No checks actually run. " +
"(You need to call ok(), is(), or similar " +
"functions at least once. Make sure you use " +
"SimpleTest.waitForExplicitFinish() if you need " +
"it.)"
);
}
let workers = await SpecialPowers.registeredServiceWorkers();
let promise =
null;
if (SimpleTest._expectingRegisteredServiceWorker) {
if (workers.length === 0) {
SimpleTest.ok(
false,
"This test is expected to leave a service worker registered"
);
}
}
else if (workers.length) {
let FULL_PROFILE_WORKERS_TO_IGNORE =
new Set();
if (parentRunner.conditionedProfile) {
// Full profile has service workers in the profile, without clearing the
// profile service workers will be leftover.
for (
const knownWorker of parentRunner.conditionedProfile
.knownServiceWorkers) {
FULL_PROFILE_WORKERS_TO_IGNORE.add(knownWorker.scriptSpec);
}
}
else {
SimpleTest.ok(
false,
"This test left a service worker registered without cleaning it up"
);
}
for (let worker of workers) {
if (FULL_PROFILE_WORKERS_TO_IGNORE.has(worker.scriptSpec)) {
continue;
}
SimpleTest.ok(
false,
`Left over worker: ${worker.scriptSpec} (scope: ${worker.scope})`
);
}
promise = SpecialPowers.removeAllServiceWorkerData();
}
// If we want to wait for removeAllServiceWorkerData to finish, above,
// there's a small chance that spinning the event loop could cause
// SpecialPowers and SimpleTest to go away (e.g. if the test did
// document.open). promise being non-null should be rare (a test would
// have had to already fail by leaving a service worker around), so
// limit the chances of the async wait happening to that case.
function finish() {
if (parentRunner) {
/* We're running in an iframe, and the parent has a TestRunner */
parentRunner.testFinished(SimpleTest._tests);
}
if (!parentRunner || parentRunner.showTestReport) {
SpecialPowers.cleanupAllClipboard(window);
SpecialPowers.flushPermissions(
function () {
SpecialPowers.flushPrefEnv(
function () {
SimpleTest.showReport();
});
});
}
}
if (promise) {
promise.then(finish);
}
else {
finish();
}
};
var executeCleanupFunction =
function () {
var func = SimpleTest._cleanupFunctions.pop();
if (!func) {
afterCleanup();
return;
}
var ret;
try {
ret = func();
}
catch (ex) {
SimpleTest.ok(
false,
"Cleanup function threw exception: " + ex);
}
if (ret && ret.constructor.name ==
"Promise") {
ret.then(executeCleanupFunction, ex =>
SimpleTest.ok(
false,
"Cleanup promise rejected: " + ex)
);
}
else {
executeCleanupFunction();
}
};
executeCleanupFunction();
SpecialPowers.notifyObservers(
null,
"test-complete");
};
/**
* Monitor console output from now until endMonitorConsole is called.
*
* Expect to receive all console messages described by the elements of
* ``msgs``, an array, in the order listed in ``msgs``; each element is an
* object which may have any number of the following properties:
*
* message, errorMessage, sourceName, sourceLine, category: string or regexp
* lineNumber, columnNumber: number
* isScriptError, isWarning: boolean
*
* Strings, numbers, and booleans must compare equal to the named
* property of the Nth console message. Regexps must match. Any
* fields present in the message but not in the pattern object are ignored.
*
* In addition to the above properties, elements in ``msgs`` may have a ``forbid``
* boolean property. When ``forbid`` is true, a failure is logged each time a
* matching message is received.
*
* If ``forbidUnexpectedMsgs`` is true, then the messages received in the console
* must exactly match the non-forbidden messages in ``msgs``; for each received
* message not described by the next element in ``msgs``, a failure is logged. If
* false, then other non-forbidden messages are ignored, but all expected
* messages must still be received.
*
* After endMonitorConsole is called, ``continuation`` will be called
* asynchronously. (Normally, you will want to pass ``SimpleTest.finish`` here.)
*
* It is incorrect to use this function in a test which has not called
* SimpleTest.waitForExplicitFinish.
*/
SimpleTest.monitorConsole =
function (
continuation,
msgs,
forbidUnexpectedMsgs
) {
if (SimpleTest._stopOnLoad) {
ok(
false,
"Console monitoring requires use of waitForExplicitFinish.");
}
function msgMatches(msg, pat) {
for (
var k in pat) {
if (!(k in msg)) {
return false;
}
if (pat[k]
instanceof RegExp &&
typeof msg[k] ===
"string") {
if (!pat[k].test(msg[k])) {
return false;
}
}
else if (msg[k] !== pat[k]) {
return false;
}
}
return true;
}
var forbiddenMsgs = [];
var i = 0;
while (i < msgs.length) {
let pat = msgs[i];
if (
"forbid" in pat) {
var forbid = pat.forbid;
delete pat.forbid;
if (forbid) {
forbiddenMsgs.push(pat);
msgs.splice(i, 1);
continue;
}
}
i++;
}
var counter = 0;
var assertionLabel = JSON.stringify(msgs);
function listener(msg) {
if (msg.message ===
"SENTINEL" && !msg.isScriptError) {
is(
counter,
msgs.length,
"monitorConsole | number of messages " + assertionLabel
);
SimpleTest.executeSoon(continuation);
return;
}
for (let pat of forbiddenMsgs) {
if (msgMatches(msg, pat)) {
ok(
false,
"monitorConsole | observed forbidden message " + JSON.stringify(msg)
);
return;
}
}
if (counter >= msgs.length) {
var str =
"monitorConsole | extra message | " + JSON.stringify(msg);
if (forbidUnexpectedMsgs) {
ok(
false, str);
}
else {
info(str);
}
return;
}
var matches = msgMatches(msg, msgs[counter]);
if (forbidUnexpectedMsgs) {
ok(
matches,
"monitorConsole | [" + counter +
"] must match " + JSON.stringify(msg)
);
}
else {
info(
"monitorConsole | [" +
counter +
"] " +
(matches ?
"matched " :
"did not match ") +
JSON.stringify(msg)
);
}
if (matches) {
counter++;
}
}
SpecialPowers.registerConsoleListener(listener);
};
/**
* Enter this window to legacy unpartitioned storage mode.
* This disables Storage and cookie partitioning for this frame.
* This is useful where the test utilized BroadcastChannels
* to coordinate with opened windows and aren't validating
* storage partitioning behavior.
*/
SimpleTest.enableLegacyUnpartitionedStorage = async
function () {
if (isXOrigin) {
await SpecialPowers.pushPrefEnv({
set: [
[
"privacy.partition.always_partition_third_party_non_cookie_storage",
false,
],
],
});
SpecialPowers.wrap(window.document).notifyUserGestureActivation();
await SpecialPowers.pushPermissions([
{
type:
"storageAccessAPI",
allow:
true,
context: window.document,
},
]);
await SpecialPowers.wrap(window.document).requestStorageAccess();
}
};
/**
* Stop monitoring console output.
*/
SimpleTest.endMonitorConsole =
function () {
SpecialPowers.postConsoleSentinel();
};
/**
* Run ``testfn`` synchronously, and monitor its console output.
*
* ``msgs`` is handled as described above for monitorConsole.
*
* After ``testfn`` returns, console monitoring will stop, and ``continuation``
* will be called asynchronously.
*
*/
SimpleTest.expectConsoleMessages =
function (testfn, msgs, continuation) {
SimpleTest.monitorConsole(continuation, msgs);
testfn();
SimpleTest.executeSoon(SimpleTest.endMonitorConsole);
};
/**
* Wrapper around ``expectConsoleMessages`` for the case where the test has
* only one ``testfn`` to run.
*/
SimpleTest.runTestExpectingConsoleMessages =
function (testfn, msgs) {
SimpleTest.waitForExplicitFinish();
SimpleTest.expectConsoleMessages(testfn, msgs, SimpleTest.finish);
};
/**
* Indicates to the test framework that the current test expects one or
* more crashes (from plugins or IPC documents), and that the minidumps from
* those crashes should be removed.
*/
SimpleTest.expectChildProcessCrash =
function () {
if (parentRunner) {
parentRunner.expectChildProcessCrash();
}
};
/**
* Indicates to the test framework that the next uncaught exception during
* the test is expected, and should not cause a test failure.
*/
SimpleTest.expectUncaughtException =
function (aExpecting) {
SimpleTest._expectingUncaughtException =
aExpecting ===
void 0 || !!aExpecting;
};
/**
* Returns whether the test has indicated that it expects an uncaught exception
* to occur.
*/
SimpleTest.isExpectingUncaughtException =
function () {
return SimpleTest._expectingUncaughtException;
};
/**
* Indicates to the test framework that all of the uncaught exceptions
* during the test are known problems that should be fixed in the future,
* but which should not cause the test to fail currently.
*/
SimpleTest.ignoreAllUncaughtExceptions =
function (aIgnoring) {
SimpleTest._ignoringAllUncaughtExceptions =
aIgnoring ===
void 0 || !!aIgnoring;
};
/**
* Returns whether the test has indicated that all uncaught exceptions should be
* ignored.
*/
SimpleTest.isIgnoringAllUncaughtExceptions =
function () {
return SimpleTest._ignoringAllUncaughtExceptions;
};
/**
* Indicates to the test framework that this test is expected to leave a
* service worker registered when it finishes.
*/
SimpleTest.expectRegisteredServiceWorker =
function () {
SimpleTest._expectingRegisteredServiceWorker =
true;
};
/**
* Resets any state this SimpleTest object has. This is important for
* browser chrome mochitests, which reuse the same SimpleTest object
* across a run.
*/
SimpleTest.reset =
function () {
SimpleTest._ignoringAllUncaughtExceptions =
false;
SimpleTest._expectingUncaughtException =
false;
SimpleTest._expectingRegisteredServiceWorker =
false;
SimpleTest._bufferedMessages = [];
};
if (isPrimaryTestWindow) {
addLoadEvent(
function () {
if (SimpleTest._stopOnLoad) {
SimpleTest.finish();
}
});
}
// --------------- Test.Builder/Test.More isDeeply() -----------------
SimpleTest.DNE = { dne:
"Does not exist" };
SimpleTest.LF =
"\r\n";
SimpleTest._deepCheck =
function (e1, e2, stack, seen) {
var ok =
false;
if (Object.is(e1, e2)) {
// Handles identical primitives and references.
ok =
true;
}
else if (
typeof e1 !=
"object" ||
typeof e2 !=
"object" ||
e1 ===
null ||
e2 ===
null
) {
// If either argument is a primitive or function, don't consider the arguments the same.
ok =
false;
}
else if (e1 == SimpleTest.DNE || e2 == SimpleTest.DNE) {
ok =
false;
}
else if (SimpleTest.isa(e1,
"Array") && SimpleTest.isa(e2,
"Array")) {
ok = SimpleTest._eqArray(e1, e2, stack, seen);
}
else {
ok = SimpleTest._eqAssoc(e1, e2, stack, seen);
}
return ok;
};
SimpleTest._eqArray =
function (a1, a2, stack, seen) {
// Return if they're the same object.
if (a1 == a2) {
return true;
}
// JavaScript objects have no unique identifiers, so we have to store
// references to them all in an array, and then compare the references
// directly. It's slow, but probably won't be much of an issue in
// practice. Start by making a local copy of the array to as to avoid
// confusing a reference seen more than once (such as [a, a]) for a
// circular reference.
for (
var j = 0; j < seen.length; j++) {
if (seen[j][0] == a1) {
return seen[j][1] == a2;
}
}
// If we get here, we haven't seen a1 before, so store it with reference
// to a2.
seen.push([a1, a2]);
var ok =
true;
// Only examines enumerable attributes. Only works for numeric arrays!
// Associative arrays return 0. So call _eqAssoc() for them, instead.
var max = Math.max(a1.length, a2.length);
if (max == 0) {
return SimpleTest._eqAssoc(a1, a2, stack, seen);
}
for (
var i = 0; i < max; i++) {
var e1 = i < a1.length ? a1[i] : SimpleTest.DNE;
var e2 = i < a2.length ? a2[i] : SimpleTest.DNE;
stack.push({ type:
"Array", idx: i, vals: [e1, e2] });
ok = SimpleTest._deepCheck(e1, e2, stack, seen);
if (ok) {
stack.pop();
}
else {
break;
}
}
return ok;
};
SimpleTest._eqAssoc =
function (o1, o2, stack, seen) {
// Return if they're the same object.
if (o1 == o2) {
return true;
}
// JavaScript objects have no unique identifiers, so we have to store
// references to them all in an array, and then compare the references
// directly. It's slow, but probably won't be much of an issue in
// practice. Start by making a local copy of the array to as to avoid
// confusing a reference seen more than once (such as [a, a]) for a
// circular reference.
seen = seen.slice(0);
for (let j = 0; j < seen.length; j++) {
if (seen[j][0] == o1) {
return seen[j][1] == o2;
}
}
// If we get here, we haven't seen o1 before, so store it with reference
// to o2.
seen.push([o1, o2]);
// They should be of the same class.
var ok =
true;
// Only examines enumerable attributes.
var o1Size = 0;
// eslint-disable-next-line no-unused-vars
for (let i in o1) {
o1Size++;
}
var o2Size = 0;
// eslint-disable-next-line no-unused-vars
for (let i in o2) {
o2Size++;
}
var bigger = o1Size > o2Size ? o1 : o2;
for (let i in bigger) {
var e1 = i in o1 ? o1[i] : SimpleTest.DNE;
var e2 = i in o2 ? o2[i] : SimpleTest.DNE;
stack.push({ type:
"Object", idx: i, vals: [e1, e2] });
ok = SimpleTest._deepCheck(e1, e2, stack, seen);
if (ok) {
stack.pop();
}
else {
break;
}
}
return ok;
};
SimpleTest._formatStack =
function (stack) {
var variable =
"$Foo";
for (let i = 0; i < stack.length; i++) {
var entry = stack[i];
var type = entry.type;
var idx = entry.idx;
if (idx !=
null) {
if (type ==
"Array") {
// Numeric array index.
variable +=
"[" + idx +
"]";
}
else {
// Associative array index.
idx = idx.replace(
"'",
"\\'");
variable +=
"['" + idx +
"']";
}
}
}
var vals = stack[stack.length - 1].vals.slice(0, 2);
var vars = [
variable.replace(
"$Foo",
"got"),
variable.replace(
"$Foo",
"expected"),
];
var out =
"Structures begin differing at:" + SimpleTest.LF;
for (let i = 0; i < vals.length; i++) {
var val = vals[i];
if (val === SimpleTest.DNE) {
val =
"Does not exist";
}
else {
val = repr(val);
}
out += vars[i] +
" = " + val + SimpleTest.LF;
}
return " " + out;
};
SimpleTest.isDeeply =
function (it, as, name) {
var stack = [{ vals: [it, as] }];
var seen = [];
if (SimpleTest._deepCheck(it, as, stack, seen)) {
SimpleTest.record(
true, name);
}
else {
SimpleTest.record(
false, name, SimpleTest._formatStack(stack));
}
};
SimpleTest.
typeOf =
function (object) {
var c = Object.prototype.toString.apply(object);
var name = c.substring(8, c.length - 1);
if (name !=
"Object") {
return name;
}
// It may be a non-core class. Try to extract the class name from
// the constructor function. This may not work in all implementations.
if (/
function ([^(\s]+)/.test(
Function.toString.call(object.constructor))) {
return RegExp.$1;
}
// No idea. :-(
return name;
};
SimpleTest.isa =
function (object, clas) {
return SimpleTest.
typeOf(object) == clas;
};
// Global symbols:
var ok = SimpleTest.ok;
var record = SimpleTest.record;
var is = SimpleTest.is;
var isfuzzy = SimpleTest.isfuzzy;
var isnot = SimpleTest.isnot;
var todo = SimpleTest.todo;
var todo_is = SimpleTest.todo_is;
var todo_isnot = SimpleTest.todo_isnot;
var isDeeply = SimpleTest.isDeeply;
var info = SimpleTest.info;
var gOldOnError = window.onerror;
window.onerror =
function simpletestOnerror(
errorMsg,
url,
lineNumber,
columnNumber,
originalException
) {
// Log the message.
// XXX Chrome mochitests sometimes trigger this window.onerror handler,
// but there are a number of uncaught JS exceptions from those tests.
// For now, for tests that self identify as having unintentional uncaught
// exceptions, just dump it so that the error is visible but doesn't cause
// a test failure. See bug 652494.
var isExpected = !!SimpleTest._expectingUncaughtException;
var message = (isExpected ?
"expected " :
"") +
"uncaught exception";
var error = errorMsg +
" at ";
try {
error += originalException.stack;
}
catch (e) {
// At least use the url+line+column we were given
error += url +
":" + lineNumber +
":" + columnNumber;
}
if (!SimpleTest._ignoringAllUncaughtExceptions) {
// Don't log if SimpleTest.finish() is already called, it would cause failures
if (!SimpleTest._alreadyFinished) {
SimpleTest.record(isExpected, message, error);
}
SimpleTest._expectingUncaughtException =
false;
}
else {
SimpleTest.todo(
false, message +
": " + error);
}
// There is no Components.stack.caller to log. (See bug 511888.)
// Call previous handler.
if (gOldOnError) {
try {
// Ignore return value: always run default handler.
gOldOnError(errorMsg, url, lineNumber);
}
catch (e) {
// Log the error.
SimpleTest.info(
"Exception thrown by gOldOnError(): " + e);
// Log its stack.
if (e.stack) {
SimpleTest.info(
"JavaScript error stack:\n" + e.stack);
}
}
}
if (!SimpleTest._stopOnLoad && !isExpected && !SimpleTest._alreadyFinished) {
// Need to finish() manually here, yet let the test actually end first.
SimpleTest.executeSoon(SimpleTest.finish);
}
};
// Lifted from dom/media/test/manifest.js
// Make sure to not touch navigator in here, since we want to push prefs that
// will affect the APIs it exposes, but the set of exposed APIs is determined
// when Navigator.prototype is created. So if we touch navigator before pushing
// the prefs, the APIs it exposes will not take those prefs into account. We
// work around this by using a navigator object from a different global for our
// UA string testing.
var gAndroidSdk =
null;
function getAndroidSdk() {
if (gAndroidSdk ===
null) {
var iframe = document.documentElement.appendChild(
document.createElement(
"iframe")
);
iframe.style.display =
"none";
var nav = iframe.contentWindow.navigator;
if (
!nav.userAgent.includes(
"Mobile") &&
!nav.userAgent.includes(
"Tablet")
) {
gAndroidSdk = -1;
}
else {
// See nsSystemInfo.cpp, the getProperty('version') returns different value
// on each platforms, so we need to distinguish the android platform.
var versionString = nav.userAgent.includes(
"Android")
?
"version"
:
"sdk_version";
gAndroidSdk = SpecialPowers.Services.sysinfo.getProperty(versionString);
}
document.documentElement.removeChild(iframe);
}
return gAndroidSdk;
}
// add_task(generatorFunction):
// Call `add_task(generatorFunction)` for each separate
// asynchronous task in a mochitest. Tasks are run consecutively.
// Before the first task, `SimpleTest.waitForExplicitFinish()`
// will be called automatically, and after the last task,
// `SimpleTest.finish()` will be called.
var add_task = (
function () {
// The list of tasks to run.
var task_list = [];
var run_only_this_task =
null;
function isGenerator(value) {
return (
value &&
typeof value ===
"object" &&
typeof value.next ===
"function"
);
}
// The "add_task" function
return function (generatorFunction, options = { isSetup:
false }) {
if (task_list.length === 0) {
// This is the first time add_task has been called.
// First, confirm that SimpleTest is available.
if (!SimpleTest) {
throw new Error(
"SimpleTest not available.");
}
// Don't stop tests until asynchronous tasks are finished.
SimpleTest.waitForExplicitFinish();
// Because the client is using add_task for this set of tests,
// we need to spawn a "master task" that calls each task in succesion.
// Use setTimeout to ensure the master task runs after the client
// script finishes.
setTimeout(
function nextTick() {
// If we are in a HTML document, we should wait for the document
// to be fully loaded.
// These checks ensure that we are in an HTML document without
// throwing TypeError; also I am told that readyState in XUL documents
// are totally bogus so we don't try to do this there.
if (
typeof window !==
"undefined" &&
typeof HTMLDocument !==
"undefined" &&
window.document
instanceof HTMLDocument &&
window.document.readyState !==
"complete"
) {
setTimeout(nextTick);
return;
}
(async () => {
// Allow for a task to be skipped; we need only use the structured logger
// for this, whilst deactivating log buffering to ensure that messages
// are always printed to stdout.
function skipTask(name) {
let logger = parentRunner && parentRunner.structuredLogger;
if (!logger) {
info(
"add_task | Skipping test " + name);
return;
}
logger.deactivateBuffering();
--> --------------------
--> maximum size reached
--> --------------------