2272 lines
67 KiB
JavaScript
2272 lines
67 KiB
JavaScript
/* -*- 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 =
|
|
"<html><body>\n<!--StartFragment-->";
|
|
|
|
/*
|
|
* See `SimpleTest.waitForClipboard`.
|
|
*/
|
|
const kTextHtmlSuffixClipboardDataWindows =
|
|
"<!--EndFragment-->\n</body>\n</html>";
|
|
|
|
/*
|
|
* 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);
|
|
};
|
|
|
|
/**
|
|
* 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();
|
|
logger.testStatus(SimpleTest._getCurrentTestURL(), name, "SKIP");
|
|
logger.warning("add_task | Skipping test " + name);
|
|
logger.activateBuffering();
|
|
}
|
|
|
|
// We stop the entire test file at the first exception because this
|
|
// may mean that the state of subsequent tests may be corrupt.
|
|
try {
|
|
for (var task of task_list) {
|
|
SimpleTest._currentTask = task;
|
|
var name = task.name || "";
|
|
if (
|
|
task.__skipMe ||
|
|
(run_only_this_task &&
|
|
task != run_only_this_task &&
|
|
!task.isSetup)
|
|
) {
|
|
skipTask(name);
|
|
continue;
|
|
}
|
|
const taskInfo = action =>
|
|
info(
|
|
`${
|
|
task.isSetup ? "add_setup" : "add_task"
|
|
} | ${action} ${name}`
|
|
);
|
|
taskInfo("Entering");
|
|
let result = await task();
|
|
if (isGenerator(result)) {
|
|
ok(false, "Task returned a generator");
|
|
}
|
|
if (task._cleanupFunctions) {
|
|
for (const fn of task._cleanupFunctions) {
|
|
await fn();
|
|
}
|
|
}
|
|
for (const fn of SimpleTest._taskCleanupFunctions) {
|
|
await fn();
|
|
}
|
|
taskInfo("Leaving");
|
|
delete SimpleTest._currentTask;
|
|
}
|
|
} catch (ex) {
|
|
try {
|
|
let serializedEx;
|
|
if (
|
|
typeof ex == "string" ||
|
|
(typeof ex == "object" && isErrorOrException(ex))
|
|
) {
|
|
serializedEx = `${ex}`;
|
|
} else {
|
|
serializedEx = JSON.stringify(ex);
|
|
}
|
|
|
|
SimpleTest.record(
|
|
false,
|
|
serializedEx,
|
|
"Should not throw any errors",
|
|
ex.stack
|
|
);
|
|
} catch (ex2) {
|
|
SimpleTest.record(
|
|
false,
|
|
"(The exception cannot be converted to string.)",
|
|
"Should not throw any errors",
|
|
ex.stack
|
|
);
|
|
}
|
|
}
|
|
// All tasks are finished.
|
|
SimpleTest.finish();
|
|
})();
|
|
});
|
|
}
|
|
generatorFunction.skip = () => (generatorFunction.__skipMe = true);
|
|
generatorFunction.only = () => (run_only_this_task = generatorFunction);
|
|
// Add the task to the list of tasks to run after
|
|
// the main thread is finished.
|
|
if (options.isSetup) {
|
|
generatorFunction.isSetup = true;
|
|
let lastSetupIndex = task_list.findLastIndex(t => t.isSetup) + 1;
|
|
task_list.splice(lastSetupIndex, 0, generatorFunction);
|
|
} else {
|
|
task_list.push(generatorFunction);
|
|
}
|
|
return generatorFunction;
|
|
};
|
|
})();
|
|
|
|
// Like add_task, but setup tasks are executed first.
|
|
function add_setup(generatorFunction) {
|
|
return add_task(generatorFunction, { isSetup: true });
|
|
}
|
|
|
|
// Request complete log when using failure patterns so that failure info
|
|
// from infra can be useful.
|
|
if (usesFailurePatterns()) {
|
|
SimpleTest.requestCompleteLog();
|
|
}
|
|
|
|
addEventListener("message", async event => {
|
|
if (event.data == "SimpleTest:timeout") {
|
|
await SimpleTest.timeout();
|
|
SimpleTest.finish();
|
|
}
|
|
});
|
|
|
|
/* import-globals-from ../../../modules/Mochia.js */
|
|
SpecialPowers.Services.scriptloader.loadSubScript(
|
|
"resource://testing-common/Mochia.js",
|
|
this
|
|
);
|
|
|
|
Mochia(this);
|