summaryrefslogtreecommitdiffstats
path: root/toolkit/jetpack/sdk/test
diff options
context:
space:
mode:
authorMatt A. Tobin <email@mattatobin.com>2018-02-10 02:51:36 -0500
committerMatt A. Tobin <email@mattatobin.com>2018-02-10 02:51:36 -0500
commit37d5300335d81cecbecc99812747a657588c63eb (patch)
tree765efa3b6a56bb715d9813a8697473e120436278 /toolkit/jetpack/sdk/test
parentb2bdac20c02b12f2057b9ef70b0a946113a00e00 (diff)
parent4fb11cd5966461bccc3ed1599b808237be6b0de9 (diff)
downloadUXP-37d5300335d81cecbecc99812747a657588c63eb.tar
UXP-37d5300335d81cecbecc99812747a657588c63eb.tar.gz
UXP-37d5300335d81cecbecc99812747a657588c63eb.tar.lz
UXP-37d5300335d81cecbecc99812747a657588c63eb.tar.xz
UXP-37d5300335d81cecbecc99812747a657588c63eb.zip
Merge branch 'ext-work'
Diffstat (limited to 'toolkit/jetpack/sdk/test')
-rw-r--r--toolkit/jetpack/sdk/test/assert.js366
-rw-r--r--toolkit/jetpack/sdk/test/harness.js645
-rw-r--r--toolkit/jetpack/sdk/test/httpd.js6
-rw-r--r--toolkit/jetpack/sdk/test/loader.js123
-rw-r--r--toolkit/jetpack/sdk/test/memory.js11
-rw-r--r--toolkit/jetpack/sdk/test/options.js23
-rw-r--r--toolkit/jetpack/sdk/test/runner.js131
-rw-r--r--toolkit/jetpack/sdk/test/utils.js199
8 files changed, 1504 insertions, 0 deletions
diff --git a/toolkit/jetpack/sdk/test/assert.js b/toolkit/jetpack/sdk/test/assert.js
new file mode 100644
index 000000000..8478c8414
--- /dev/null
+++ b/toolkit/jetpack/sdk/test/assert.js
@@ -0,0 +1,366 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+module.metadata = {
+ "stability": "unstable"
+};
+
+const { isFunction, isNull, isObject, isString,
+ isRegExp, isArray, isDate, isPrimitive,
+ isUndefined, instanceOf, source } = require("../lang/type");
+
+/**
+ * The `AssertionError` is defined in assert.
+ * @extends Error
+ * @example
+ * new assert.AssertionError({
+ * message: message,
+ * actual: actual,
+ * expected: expected
+ * })
+ */
+function AssertionError(options) {
+ let assertionError = Object.create(AssertionError.prototype);
+
+ if (isString(options))
+ options = { message: options };
+ if ("actual" in options)
+ assertionError.actual = options.actual;
+ if ("expected" in options)
+ assertionError.expected = options.expected;
+ if ("operator" in options)
+ assertionError.operator = options.operator;
+
+ assertionError.message = options.message;
+ assertionError.stack = new Error().stack;
+ return assertionError;
+}
+AssertionError.prototype = Object.create(Error.prototype, {
+ constructor: { value: AssertionError },
+ name: { value: "AssertionError", enumerable: true },
+ toString: { value: function toString() {
+ let value;
+ if (this.message) {
+ value = this.name + " : " + this.message;
+ }
+ else {
+ value = [
+ this.name + " : ",
+ source(this.expected),
+ this.operator,
+ source(this.actual)
+ ].join(" ");
+ }
+ return value;
+ }}
+});
+exports.AssertionError = AssertionError;
+
+function Assert(logger) {
+ let assert = Object.create(Assert.prototype, { _log: { value: logger }});
+
+ assert.fail = assert.fail.bind(assert);
+ assert.pass = assert.pass.bind(assert);
+
+ return assert;
+}
+
+Assert.prototype = {
+ fail: function fail(e) {
+ if (!e || typeof(e) !== 'object') {
+ this._log.fail(e);
+ return;
+ }
+ let message = e.message;
+ try {
+ if ('operator' in e) {
+ message += [
+ " -",
+ source(e.actual),
+ e.operator,
+ source(e.expected)
+ ].join(" ");
+ }
+ }
+ catch(e) {}
+ this._log.fail(message);
+ },
+ pass: function pass(message) {
+ this._log.pass(message);
+ return true;
+ },
+ error: function error(e) {
+ this._log.exception(e);
+ },
+ ok: function ok(value, message) {
+ if (!!!value) {
+ this.fail({
+ actual: value,
+ expected: true,
+ message: message,
+ operator: "=="
+ });
+ return false;
+ }
+
+ this.pass(message);
+ return true;
+ },
+
+ /**
+ * The equality assertion tests shallow, coercive equality with `==`.
+ * @example
+ * assert.equal(1, 1, "one is one");
+ */
+ equal: function equal(actual, expected, message) {
+ if (actual == expected) {
+ this.pass(message);
+ return true;
+ }
+
+ this.fail({
+ actual: actual,
+ expected: expected,
+ message: message,
+ operator: "=="
+ });
+ return false;
+ },
+
+ /**
+ * The non-equality assertion tests for whether two objects are not equal
+ * with `!=`.
+ * @example
+ * assert.notEqual(1, 2, "one is not two");
+ */
+ notEqual: function notEqual(actual, expected, message) {
+ if (actual != expected) {
+ this.pass(message);
+ return true;
+ }
+
+ this.fail({
+ actual: actual,
+ expected: expected,
+ message: message,
+ operator: "!=",
+ });
+ return false;
+ },
+
+ /**
+ * The equivalence assertion tests a deep (with `===`) equality relation.
+ * @example
+ * assert.deepEqual({ a: "foo" }, { a: "foo" }, "equivalent objects")
+ */
+ deepEqual: function deepEqual(actual, expected, message) {
+ if (isDeepEqual(actual, expected)) {
+ this.pass(message);
+ return true;
+ }
+
+ this.fail({
+ actual: actual,
+ expected: expected,
+ message: message,
+ operator: "deepEqual"
+ });
+ return false;
+ },
+
+ /**
+ * The non-equivalence assertion tests for any deep (with `===`) inequality.
+ * @example
+ * assert.notDeepEqual({ a: "foo" }, Object.create({ a: "foo" }),
+ * "object's inherit from different prototypes");
+ */
+ notDeepEqual: function notDeepEqual(actual, expected, message) {
+ if (!isDeepEqual(actual, expected)) {
+ this.pass(message);
+ return true;
+ }
+
+ this.fail({
+ actual: actual,
+ expected: expected,
+ message: message,
+ operator: "notDeepEqual"
+ });
+ return false;
+ },
+
+ /**
+ * The strict equality assertion tests strict equality, as determined by
+ * `===`.
+ * @example
+ * assert.strictEqual(null, null, "`null` is `null`")
+ */
+ strictEqual: function strictEqual(actual, expected, message) {
+ if (actual === expected) {
+ this.pass(message);
+ return true;
+ }
+
+ this.fail({
+ actual: actual,
+ expected: expected,
+ message: message,
+ operator: "==="
+ });
+ return false;
+ },
+
+ /**
+ * The strict non-equality assertion tests for strict inequality, as
+ * determined by `!==`.
+ * @example
+ * assert.notStrictEqual(null, undefined, "`null` is not `undefined`");
+ */
+ notStrictEqual: function notStrictEqual(actual, expected, message) {
+ if (actual !== expected) {
+ this.pass(message);
+ return true;
+ }
+
+ this.fail({
+ actual: actual,
+ expected: expected,
+ message: message,
+ operator: "!=="
+ });
+ return false;
+ },
+
+ /**
+ * The assertion whether or not given `block` throws an exception. If optional
+ * `Error` argument is provided and it's type of function thrown error is
+ * asserted to be an instance of it, if type of `Error` is string then message
+ * of throw exception is asserted to contain it.
+ * @param {Function} block
+ * Function that is expected to throw.
+ * @param {Error|RegExp} [Error]
+ * Error constructor that is expected to be thrown or a string that
+ * must be contained by a message of the thrown exception, or a RegExp
+ * matching a message of the thrown exception.
+ * @param {String} message
+ * Description message
+ *
+ * @examples
+ *
+ * assert.throws(function block() {
+ * doSomething(4)
+ * }, "Object is expected", "Incorrect argument is passed");
+ *
+ * assert.throws(function block() {
+ * Object.create(5)
+ * }, TypeError, "TypeError is thrown");
+ */
+ throws: function throws(block, Error, message) {
+ let threw = false;
+ let exception = null;
+
+ // If third argument is not provided and second argument is a string it
+ // means that optional `Error` argument was not passed, so we shift
+ // arguments.
+ if (isString(Error) && isUndefined(message)) {
+ message = Error;
+ Error = undefined;
+ }
+
+ // Executing given `block`.
+ try {
+ block();
+ }
+ catch (e) {
+ threw = true;
+ exception = e;
+ }
+
+ // If exception was thrown and `Error` argument was not passed assert is
+ // passed.
+ if (threw && (isUndefined(Error) ||
+ // If passed `Error` is RegExp using it's test method to
+ // assert thrown exception message.
+ (isRegExp(Error) && (Error.test(exception.message) || Error.test(exception.toString()))) ||
+ // If passed `Error` is a constructor function testing if
+ // thrown exception is an instance of it.
+ (isFunction(Error) && instanceOf(exception, Error))))
+ {
+ this.pass(message);
+ return true;
+ }
+
+ // Otherwise we report assertion failure.
+ let failure = {
+ message: message,
+ operator: "matches"
+ };
+
+ if (exception) {
+ failure.actual = exception.message || exception.toString();
+ }
+
+ if (Error) {
+ failure.expected = Error.toString();
+ }
+
+ this.fail(failure);
+ return false;
+ }
+};
+exports.Assert = Assert;
+
+function isDeepEqual(actual, expected) {
+ // 7.1. All identical values are equivalent, as determined by ===.
+ if (actual === expected) {
+ return true;
+ }
+
+ // 7.2. If the expected value is a Date object, the actual value is
+ // equivalent if it is also a Date object that refers to the same time.
+ else if (isDate(actual) && isDate(expected)) {
+ return actual.getTime() === expected.getTime();
+ }
+
+ // XXX specification bug: this should be specified
+ else if (isPrimitive(actual) || isPrimitive(expected)) {
+ return expected === actual;
+ }
+
+ // 7.3. Other pairs that do not both pass typeof value == "object",
+ // equivalence is determined by ==.
+ else if (!isObject(actual) && !isObject(expected)) {
+ return actual == expected;
+ }
+
+ // 7.4. For all other Object pairs, including Array objects, equivalence is
+ // determined by having the same number of owned properties (as verified
+ // with Object.prototype.hasOwnProperty.call), the same set of keys
+ // (although not necessarily the same order), equivalent values for every
+ // corresponding key, and an identical "prototype" property. Note: this
+ // accounts for both named and indexed properties on Arrays.
+ else {
+ return actual.prototype === expected.prototype &&
+ isEquivalent(actual, expected);
+ }
+}
+
+function isEquivalent(a, b, stack) {
+ let aKeys = Object.keys(a);
+ let bKeys = Object.keys(b);
+
+ return aKeys.length === bKeys.length &&
+ isArrayEquivalent(aKeys.sort(), bKeys.sort()) &&
+ aKeys.every(function(key) {
+ return isDeepEqual(a[key], b[key], stack)
+ });
+}
+
+function isArrayEquivalent(a, b, stack) {
+ return isArray(a) && isArray(b) &&
+ a.every(function(value, index) {
+ return isDeepEqual(value, b[index]);
+ });
+}
diff --git a/toolkit/jetpack/sdk/test/harness.js b/toolkit/jetpack/sdk/test/harness.js
new file mode 100644
index 000000000..1b31a1c79
--- /dev/null
+++ b/toolkit/jetpack/sdk/test/harness.js
@@ -0,0 +1,645 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+module.metadata = {
+ "stability": "experimental"
+};
+
+const { Cc, Ci, Cu } = require("chrome");
+const { Loader } = require('./loader');
+const { serializeStack, parseStack } = require("toolkit/loader");
+const { setTimeout } = require('../timers');
+const { PlainTextConsole } = require("../console/plain-text");
+const { when: unload } = require("../system/unload");
+const { format, fromException } = require("../console/traceback");
+const system = require("../system");
+const { gc: gcPromise } = require('./memory');
+const { defer } = require('../core/promise');
+const { extend } = require('../core/heritage');
+
+// Trick manifest builder to make it think we need these modules ?
+const unit = require("../deprecated/unit-test");
+const test = require("../../test");
+const url = require("../url");
+
+function emptyPromise() {
+ let { promise, resolve } = defer();
+ resolve();
+ return promise;
+}
+
+var cService = Cc['@mozilla.org/consoleservice;1'].getService(Ci.nsIConsoleService);
+
+// The console used to log messages
+var testConsole;
+
+// Cuddlefish loader in which we load and execute tests.
+var loader;
+
+// Function to call when we're done running tests.
+var onDone;
+
+// Function to print text to a console, w/o CR at the end.
+var print;
+
+// How many more times to run all tests.
+var iterationsLeft;
+
+// Whether to report memory profiling information.
+var profileMemory;
+
+// Whether we should stop as soon as a test reports a failure.
+var stopOnError;
+
+// Function to call to retrieve a list of tests to execute
+var findAndRunTests;
+
+// Combined information from all test runs.
+var results;
+
+// A list of the compartments and windows loaded after startup
+var startLeaks;
+
+// JSON serialization of last memory usage stats; we keep it stringified
+// so we don't actually change the memory usage stats (in terms of objects)
+// of the JSRuntime we're profiling.
+var lastMemoryUsage;
+
+function analyzeRawProfilingData(data) {
+ var graph = data.graph;
+ var shapes = {};
+
+ // Convert keys in the graph from strings to ints.
+ // TODO: Can we get rid of this ridiculousness?
+ var newGraph = {};
+ for (id in graph) {
+ newGraph[parseInt(id)] = graph[id];
+ }
+ graph = newGraph;
+
+ var modules = 0;
+ var moduleIds = [];
+ var moduleObjs = {UNKNOWN: 0};
+ for (let name in data.namedObjects) {
+ moduleObjs[name] = 0;
+ moduleIds[data.namedObjects[name]] = name;
+ modules++;
+ }
+
+ var count = 0;
+ for (id in graph) {
+ var parent = graph[id].parent;
+ while (parent) {
+ if (parent in moduleIds) {
+ var name = moduleIds[parent];
+ moduleObjs[name]++;
+ break;
+ }
+ if (!(parent in graph)) {
+ moduleObjs.UNKNOWN++;
+ break;
+ }
+ parent = graph[parent].parent;
+ }
+ count++;
+ }
+
+ print("\nobject count is " + count + " in " + modules + " modules" +
+ " (" + data.totalObjectCount + " across entire JS runtime)\n");
+ if (lastMemoryUsage) {
+ var last = JSON.parse(lastMemoryUsage);
+ var diff = {
+ moduleObjs: dictDiff(last.moduleObjs, moduleObjs),
+ totalObjectClasses: dictDiff(last.totalObjectClasses,
+ data.totalObjectClasses)
+ };
+
+ for (let name in diff.moduleObjs)
+ print(" " + diff.moduleObjs[name] + " in " + name + "\n");
+ for (let name in diff.totalObjectClasses)
+ print(" " + diff.totalObjectClasses[name] + " instances of " +
+ name + "\n");
+ }
+ lastMemoryUsage = JSON.stringify(
+ {moduleObjs: moduleObjs,
+ totalObjectClasses: data.totalObjectClasses}
+ );
+}
+
+function dictDiff(last, curr) {
+ var diff = {};
+
+ for (let name in last) {
+ var result = (curr[name] || 0) - last[name];
+ if (result)
+ diff[name] = (result > 0 ? "+" : "") + result;
+ }
+ for (let name in curr) {
+ var result = curr[name] - (last[name] || 0);
+ if (result)
+ diff[name] = (result > 0 ? "+" : "") + result;
+ }
+ return diff;
+}
+
+function reportMemoryUsage() {
+ if (!profileMemory) {
+ return emptyPromise();
+ }
+
+ return gcPromise().then((() => {
+ var mgr = Cc["@mozilla.org/memory-reporter-manager;1"]
+ .getService(Ci.nsIMemoryReporterManager);
+ let count = 0;
+ function logReporter(process, path, kind, units, amount, description) {
+ print(((++count == 1) ? "\n" : "") + description + ": " + amount + "\n");
+ }
+ mgr.getReportsForThisProcess(logReporter, null, /* anonymize = */ false);
+ }));
+}
+
+var gWeakrefInfo;
+
+function checkMemory() {
+ return gcPromise().then(_ => {
+ let leaks = getPotentialLeaks();
+
+ let compartmentURLs = Object.keys(leaks.compartments).filter(function(url) {
+ return !(url in startLeaks.compartments);
+ });
+
+ let windowURLs = Object.keys(leaks.windows).filter(function(url) {
+ return !(url in startLeaks.windows);
+ });
+
+ for (let url of compartmentURLs)
+ console.warn("LEAKED", leaks.compartments[url]);
+
+ for (let url of windowURLs)
+ console.warn("LEAKED", leaks.windows[url]);
+ }).then(showResults);
+}
+
+function showResults() {
+ let { promise, resolve } = defer();
+
+ if (gWeakrefInfo) {
+ gWeakrefInfo.forEach(
+ function(info) {
+ var ref = info.weakref.get();
+ if (ref !== null) {
+ var data = ref.__url__ ? ref.__url__ : ref;
+ var warning = data == "[object Object]"
+ ? "[object " + data.constructor.name + "(" +
+ Object.keys(data).join(", ") + ")]"
+ : data;
+ console.warn("LEAK", warning, info.bin);
+ }
+ }
+ );
+ }
+
+ onDone(results);
+
+ resolve();
+ return promise;
+}
+
+function cleanup() {
+ let coverObject = {};
+ try {
+ loader.unload();
+
+ if (loader.globals.console.errorsLogged && !results.failed) {
+ results.failed++;
+ console.error("warnings and/or errors were logged.");
+ }
+
+ if (consoleListener.errorsLogged && !results.failed) {
+ console.warn(consoleListener.errorsLogged + " " +
+ "warnings or errors were logged to the " +
+ "platform's nsIConsoleService, which could " +
+ "be of no consequence; however, they could also " +
+ "be indicative of aberrant behavior.");
+ }
+
+ // read the code coverage object, if it exists, from CoverJS-moz
+ if (typeof loader.globals.global == "object") {
+ coverObject = loader.globals.global['__$coverObject'] || {};
+ }
+
+ consoleListener.errorsLogged = 0;
+ loader = null;
+
+ consoleListener.unregister();
+
+ Cu.forceGC();
+ }
+ catch (e) {
+ results.failed++;
+ console.error("unload.send() threw an exception.");
+ console.exception(e);
+ };
+
+ setTimeout(require("./options").checkMemory ? checkMemory : showResults, 1);
+
+ // dump the coverobject
+ if (Object.keys(coverObject).length){
+ const self = require('sdk/self');
+ const {pathFor} = require("sdk/system");
+ let file = require('sdk/io/file');
+ const {env} = require('sdk/system/environment');
+ console.log("CWD:", env.PWD);
+ let out = file.join(env.PWD,'coverstats-'+self.id+'.json');
+ console.log('coverstats:', out);
+ let outfh = file.open(out,'w');
+ outfh.write(JSON.stringify(coverObject,null,2));
+ outfh.flush();
+ outfh.close();
+ }
+}
+
+function getPotentialLeaks() {
+ Cu.forceGC();
+
+ // Things we can assume are part of the platform and so aren't leaks
+ let GOOD_BASE_URLS = [
+ "chrome://",
+ "resource:///",
+ "resource://app/",
+ "resource://gre/",
+ "resource://gre-resources/",
+ "resource://pdf.js/",
+ "resource://pdf.js.components/",
+ "resource://services-common/",
+ "resource://services-crypto/",
+ "resource://services-sync/"
+ ];
+
+ let ioService = Cc["@mozilla.org/network/io-service;1"].
+ getService(Ci.nsIIOService);
+ let uri = ioService.newURI("chrome://global/content/", "UTF-8", null);
+ let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].
+ getService(Ci.nsIChromeRegistry);
+ uri = chromeReg.convertChromeURL(uri);
+ let spec = uri.spec;
+ let pos = spec.indexOf("!/");
+ GOOD_BASE_URLS.push(spec.substring(0, pos + 2));
+
+ let zoneRegExp = new RegExp("^explicit/js-non-window/zones/zone[^/]+/compartment\\((.+)\\)");
+ let compartmentRegexp = new RegExp("^explicit/js-non-window/compartments/non-window-global/compartment\\((.+)\\)/");
+ let compartmentDetails = new RegExp("^([^,]+)(?:, (.+?))?(?: \\(from: (.*)\\))?$");
+ let windowRegexp = new RegExp("^explicit/window-objects/top\\((.*)\\)/active");
+ let windowDetails = new RegExp("^(.*), id=.*$");
+
+ function isPossibleLeak(item) {
+ if (!item.location)
+ return false;
+
+ for (let url of GOOD_BASE_URLS) {
+ if (item.location.substring(0, url.length) == url) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ let compartments = {};
+ let windows = {};
+ function logReporter(process, path, kind, units, amount, description) {
+ let matches;
+
+ if ((matches = compartmentRegexp.exec(path)) || (matches = zoneRegExp.exec(path))) {
+ if (matches[1] in compartments)
+ return;
+
+ let details = compartmentDetails.exec(matches[1]);
+ if (!details) {
+ console.error("Unable to parse compartment detail " + matches[1]);
+ return;
+ }
+
+ let item = {
+ path: matches[1],
+ principal: details[1],
+ location: details[2] ? details[2].replace(/\\/g, "/") : undefined,
+ source: details[3] ? details[3].split(" -> ").reverse() : undefined,
+ toString: function() {
+ return this.location;
+ }
+ };
+
+ if (!isPossibleLeak(item))
+ return;
+
+ compartments[matches[1]] = item;
+ return;
+ }
+
+ if ((matches = windowRegexp.exec(path))) {
+ if (matches[1] in windows)
+ return;
+
+ let details = windowDetails.exec(matches[1]);
+ if (!details) {
+ console.error("Unable to parse window detail " + matches[1]);
+ return;
+ }
+
+ let item = {
+ path: matches[1],
+ location: details[1].replace(/\\/g, "/"),
+ source: [details[1].replace(/\\/g, "/")],
+ toString: function() {
+ return this.location;
+ }
+ };
+
+ if (!isPossibleLeak(item))
+ return;
+
+ windows[matches[1]] = item;
+ }
+ }
+
+ Cc["@mozilla.org/memory-reporter-manager;1"]
+ .getService(Ci.nsIMemoryReporterManager)
+ .getReportsForThisProcess(logReporter, null, /* anonymize = */ false);
+
+ return { compartments: compartments, windows: windows };
+}
+
+function nextIteration(tests) {
+ if (tests) {
+ results.passed += tests.passed;
+ results.failed += tests.failed;
+
+ reportMemoryUsage().then(_ => {
+ let testRun = [];
+ for (let test of tests.testRunSummary) {
+ let testCopy = {};
+ for (let info in test) {
+ testCopy[info] = test[info];
+ }
+ testRun.push(testCopy);
+ }
+
+ results.testRuns.push(testRun);
+ iterationsLeft--;
+
+ checkForEnd();
+ })
+ }
+ else {
+ checkForEnd();
+ }
+}
+
+function checkForEnd() {
+ if (iterationsLeft && (!stopOnError || results.failed == 0)) {
+ // Pass the loader which has a hooked console that doesn't dispatch
+ // errors to the JS console and avoid firing false alarm in our
+ // console listener
+ findAndRunTests(loader, nextIteration);
+ }
+ else {
+ setTimeout(cleanup, 0);
+ }
+}
+
+var POINTLESS_ERRORS = [
+ 'Invalid chrome URI:',
+ 'OpenGL LayerManager Initialized Succesfully.',
+ '[JavaScript Error: "TelemetryStopwatch:',
+ 'reference to undefined property',
+ '[JavaScript Error: "The character encoding of the HTML document was ' +
+ 'not declared.',
+ '[Javascript Warning: "Error: Failed to preserve wrapper of wrapped ' +
+ 'native weak map key',
+ '[JavaScript Warning: "Duplicate resource declaration for',
+ 'file: "chrome://browser/content/',
+ 'file: "chrome://global/content/',
+ '[JavaScript Warning: "The character encoding of a framed document was ' +
+ 'not declared.',
+ 'file: "chrome://browser/skin/'
+];
+
+// These are messages that will cause a test to fail if logged through the
+// console service
+var IMPORTANT_ERRORS = [
+ 'Sending message that cannot be cloned. Are you trying to send an XPCOM object?',
+];
+
+var consoleListener = {
+ registered: false,
+
+ register: function() {
+ if (this.registered)
+ return;
+ cService.registerListener(this);
+ this.registered = true;
+ },
+
+ unregister: function() {
+ if (!this.registered)
+ return;
+ cService.unregisterListener(this);
+ this.registered = false;
+ },
+
+ errorsLogged: 0,
+
+ observe: function(object) {
+ if (!(object instanceof Ci.nsIScriptError))
+ return;
+ this.errorsLogged++;
+ var message = object.QueryInterface(Ci.nsIConsoleMessage).message;
+ if (IMPORTANT_ERRORS.find(msg => message.indexOf(msg) >= 0)) {
+ testConsole.error(message);
+ return;
+ }
+ var pointless = POINTLESS_ERRORS.filter(err => message.indexOf(err) >= 0);
+ if (pointless.length == 0 && message)
+ testConsole.log(message);
+ }
+};
+
+function TestRunnerConsole(base, options) {
+ let proto = extend(base, {
+ errorsLogged: 0,
+ warn: function warn() {
+ this.errorsLogged++;
+ base.warn.apply(base, arguments);
+ },
+ error: function error() {
+ this.errorsLogged++;
+ base.error.apply(base, arguments);
+ },
+ info: function info(first) {
+ if (options.verbose)
+ base.info.apply(base, arguments);
+ else
+ if (first == "pass:")
+ print(".");
+ },
+ });
+ return Object.create(proto);
+}
+
+function stringify(arg) {
+ try {
+ return String(arg);
+ }
+ catch(ex) {
+ return "<toString() error>";
+ }
+}
+
+function stringifyArgs(args) {
+ return Array.map(args, stringify).join(" ");
+}
+
+function TestRunnerTinderboxConsole(base, options) {
+ this.base = base;
+ this.print = options.print;
+ this.verbose = options.verbose;
+ this.errorsLogged = 0;
+
+ // Binding all the public methods to an instance so that they can be used
+ // as callback / listener functions straightaway.
+ this.log = this.log.bind(this);
+ this.info = this.info.bind(this);
+ this.warn = this.warn.bind(this);
+ this.error = this.error.bind(this);
+ this.debug = this.debug.bind(this);
+ this.exception = this.exception.bind(this);
+ this.trace = this.trace.bind(this);
+};
+
+TestRunnerTinderboxConsole.prototype = {
+ testMessage: function testMessage(pass, expected, test, message) {
+ let type = "TEST-";
+ if (expected) {
+ if (pass)
+ type += "PASS";
+ else
+ type += "KNOWN-FAIL";
+ }
+ else {
+ this.errorsLogged++;
+ if (pass)
+ type += "UNEXPECTED-PASS";
+ else
+ type += "UNEXPECTED-FAIL";
+ }
+
+ this.print(type + " | " + test + " | " + message + "\n");
+ if (!expected)
+ this.trace();
+ },
+
+ log: function log() {
+ this.print("TEST-INFO | " + stringifyArgs(arguments) + "\n");
+ },
+
+ info: function info(first) {
+ this.print("TEST-INFO | " + stringifyArgs(arguments) + "\n");
+ },
+
+ warn: function warn() {
+ this.errorsLogged++;
+ this.print("TEST-UNEXPECTED-FAIL | " + stringifyArgs(arguments) + "\n");
+ },
+
+ error: function error() {
+ this.errorsLogged++;
+ this.print("TEST-UNEXPECTED-FAIL | " + stringifyArgs(arguments) + "\n");
+ this.base.error.apply(this.base, arguments);
+ },
+
+ debug: function debug() {
+ this.print("TEST-INFO | " + stringifyArgs(arguments) + "\n");
+ },
+
+ exception: function exception(e) {
+ this.print("An exception occurred.\n" +
+ require("../console/traceback").format(e) + "\n" + e + "\n");
+ },
+
+ trace: function trace() {
+ var traceback = require("../console/traceback");
+ var stack = traceback.get();
+ stack.splice(-1, 1);
+ this.print("TEST-INFO | " + stringify(traceback.format(stack)) + "\n");
+ }
+};
+
+var runTests = exports.runTests = function runTests(options) {
+ iterationsLeft = options.iterations;
+ profileMemory = options.profileMemory;
+ stopOnError = options.stopOnError;
+ onDone = options.onDone;
+ print = options.print;
+ findAndRunTests = options.findAndRunTests;
+
+ results = {
+ passed: 0,
+ failed: 0,
+ testRuns: []
+ };
+
+ try {
+ consoleListener.register();
+ print("Running tests on " + system.name + " " + system.version +
+ "/Gecko " + system.platformVersion + " (Build " +
+ system.build + ") (" + system.id + ") under " +
+ system.platform + "/" + system.architecture + ".\n");
+
+ if (options.parseable)
+ testConsole = new TestRunnerTinderboxConsole(new PlainTextConsole(), options);
+ else
+ testConsole = new TestRunnerConsole(new PlainTextConsole(), options);
+
+ loader = Loader(module, {
+ console: testConsole,
+ global: {} // useful for storing things like coverage testing.
+ });
+
+ // Load these before getting initial leak stats as they will still be in
+ // memory when we check later
+ require("../deprecated/unit-test");
+ require("../deprecated/unit-test-finder");
+ if (profileMemory)
+ startLeaks = getPotentialLeaks();
+
+ nextIteration();
+ } catch (e) {
+ let frames = fromException(e).reverse().reduce(function(frames, frame) {
+ if (frame.fileName.split("/").pop() === "unit-test-finder.js")
+ frames.done = true
+ if (!frames.done) frames.push(frame)
+
+ return frames
+ }, [])
+
+ let prototype = typeof(e) === "object" ? e.constructor.prototype :
+ Error.prototype;
+ let stack = serializeStack(frames.reverse());
+
+ let error = Object.create(prototype, {
+ message: { value: e.message, writable: true, configurable: true },
+ fileName: { value: e.fileName, writable: true, configurable: true },
+ lineNumber: { value: e.lineNumber, writable: true, configurable: true },
+ stack: { value: stack, writable: true, configurable: true },
+ toString: { value: () => String(e), writable: true, configurable: true },
+ });
+
+ print("Error: " + error + " \n " + format(error));
+ onDone({passed: 0, failed: 1});
+ }
+};
+
+unload(_ => consoleListener.unregister());
diff --git a/toolkit/jetpack/sdk/test/httpd.js b/toolkit/jetpack/sdk/test/httpd.js
new file mode 100644
index 000000000..218493924
--- /dev/null
+++ b/toolkit/jetpack/sdk/test/httpd.js
@@ -0,0 +1,6 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+throw new Error(`This file was removed. A copy can be obtained from:
+ https://github.com/mozilla/addon-sdk/blob/master/test/lib/httpd.js`);
diff --git a/toolkit/jetpack/sdk/test/loader.js b/toolkit/jetpack/sdk/test/loader.js
new file mode 100644
index 000000000..33ba2ca5a
--- /dev/null
+++ b/toolkit/jetpack/sdk/test/loader.js
@@ -0,0 +1,123 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const { resolveURI, Require,
+ unload, override, descriptor } = require('../../toolkit/loader');
+const { ensure } = require('../system/unload');
+const addonWindow = require('../addon/window');
+const { PlainTextConsole } = require('sdk/console/plain-text');
+
+var defaultGlobals = override(require('../system/globals'), {
+ console: console
+});
+
+function CustomLoader(module, globals, packaging, overrides={}) {
+ let options = packaging || require("@loader/options");
+ options = override(options, {
+ id: overrides.id || options.id,
+ globals: override(defaultGlobals, globals || {}),
+ modules: override(override(options.modules || {}, overrides.modules || {}), {
+ 'sdk/addon/window': addonWindow
+ })
+ });
+
+ let loaderModule = options.isNative ? '../../toolkit/loader' : '../loader/cuddlefish';
+ let { Loader } = require(loaderModule);
+ let loader = Loader(options);
+ let wrapper = Object.create(loader, descriptor({
+ require: Require(loader, module),
+ sandbox: function(id) {
+ let requirement = loader.resolve(id, module.id);
+ if (!requirement)
+ requirement = id;
+ let uri = resolveURI(requirement, loader.mapping);
+ return loader.sandboxes[uri];
+ },
+ unload: function(reason) {
+ unload(loader, reason);
+ }
+ }));
+ ensure(wrapper);
+ return wrapper;
+};
+exports.Loader = CustomLoader;
+
+function HookedPlainTextConsole(hook, print, innerID) {
+ this.log = hook.bind(null, "log", innerID);
+ this.info = hook.bind(null, "info", innerID);
+ this.warn = hook.bind(null, "warn", innerID);
+ this.error = hook.bind(null, "error", innerID);
+ this.debug = hook.bind(null, "debug", innerID);
+ this.exception = hook.bind(null, "exception", innerID);
+ this.time = hook.bind(null, "time", innerID);
+ this.timeEnd = hook.bind(null, "timeEnd", innerID);
+
+ this.__exposedProps__ = {
+ log: "rw", info: "rw", warn: "rw", error: "rw", debug: "rw",
+ exception: "rw", time: "rw", timeEnd: "rw"
+ };
+}
+
+// Creates a custom loader instance whose console module is hooked in order
+// to avoid printing messages to the console, and instead, expose them in the
+// returned `messages` array attribute
+exports.LoaderWithHookedConsole = function (module, callback) {
+ let messages = [];
+ function hook(type, innerID, msg) {
+ messages.push({ type: type, msg: msg, innerID: innerID });
+ if (callback)
+ callback(type, msg, innerID);
+ }
+
+ return {
+ loader: CustomLoader(module, {
+ console: new HookedPlainTextConsole(hook, null, null)
+ }, null, {
+ modules: {
+ 'sdk/console/plain-text': {
+ PlainTextConsole: HookedPlainTextConsole.bind(null, hook)
+ }
+ }
+ }),
+ messages: messages
+ };
+}
+
+// Same than LoaderWithHookedConsole with lower level, instead we get what is
+// actually printed to the command line console
+exports.LoaderWithHookedConsole2 = function (module, callback) {
+ let messages = [];
+ return {
+ loader: CustomLoader(module, {
+ console: new PlainTextConsole(function (msg) {
+ messages.push(msg);
+ if (callback)
+ callback(msg);
+ })
+ }),
+ messages: messages
+ };
+}
+
+// Creates a custom loader with a filtered console. The callback is passed every
+// console message type and message and if it returns false the message will
+// not be logged normally
+exports.LoaderWithFilteredConsole = function (module, callback) {
+ function hook(type, innerID, msg) {
+ if (callback && callback(type, msg, innerID) == false)
+ return;
+ console[type](msg);
+ }
+
+ return CustomLoader(module, {
+ console: new HookedPlainTextConsole(hook, null, null)
+ }, null, {
+ modules: {
+ 'sdk/console/plain-text': {
+ PlainTextConsole: HookedPlainTextConsole.bind(null, hook)
+ }
+ }
+ });
+}
diff --git a/toolkit/jetpack/sdk/test/memory.js b/toolkit/jetpack/sdk/test/memory.js
new file mode 100644
index 000000000..bd1198bfe
--- /dev/null
+++ b/toolkit/jetpack/sdk/test/memory.js
@@ -0,0 +1,11 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+'use strict';
+
+const { Cu } = require("chrome");
+
+function gc() {
+ return new Promise(resolve => Cu.schedulePreciseGC(resolve));
+}
+exports.gc = gc;
diff --git a/toolkit/jetpack/sdk/test/options.js b/toolkit/jetpack/sdk/test/options.js
new file mode 100644
index 000000000..9bc611ca5
--- /dev/null
+++ b/toolkit/jetpack/sdk/test/options.js
@@ -0,0 +1,23 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+module.metadata = {
+ "stability": "unstable"
+};
+
+const options = require("@test/options");
+const { id } = require("../self");
+const { get } = require("../preferences/service");
+
+const readPref = (key) => get("extensions." + id + ".sdk." + key);
+
+exports.iterations = readPref("test.iterations") || options.iterations;
+exports.filter = readPref("test.filter") || options.filter;
+exports.profileMemory = readPref("profile.memory") || options.profileMemory;
+exports.stopOnError = readPref("test.stop") || options.stopOnError;
+exports.keepOpen = readPref("test.keepOpen") || false;
+exports.verbose = (readPref("output.logLevel") == "verbose") || options.verbose;
+exports.parseable = (readPref("output.format") == "tbpl") || options.parseable;
+exports.checkMemory = readPref("profile.leaks") || options.check_memory;
diff --git a/toolkit/jetpack/sdk/test/runner.js b/toolkit/jetpack/sdk/test/runner.js
new file mode 100644
index 000000000..ea37ac84f
--- /dev/null
+++ b/toolkit/jetpack/sdk/test/runner.js
@@ -0,0 +1,131 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+module.metadata = {
+ "stability": "experimental"
+};
+
+var { exit, stdout } = require("../system");
+var cfxArgs = require("../test/options");
+var events = require("../system/events");
+const { resolve } = require("../core/promise");
+
+function runTests(findAndRunTests) {
+ var harness = require("./harness");
+
+ function onDone(tests) {
+ stdout.write("\n");
+ var total = tests.passed + tests.failed;
+ stdout.write(tests.passed + " of " + total + " tests passed.\n");
+
+ events.emit("sdk:test:results", { data: JSON.stringify(tests) });
+
+ if (tests.failed == 0) {
+ if (tests.passed === 0)
+ stdout.write("No tests were run\n");
+ if (!cfxArgs.keepOpen)
+ exit(0);
+ } else {
+ if (cfxArgs.verbose || cfxArgs.parseable)
+ printFailedTests(tests, stdout.write);
+ if (!cfxArgs.keepOpen)
+ exit(1);
+ }
+ };
+
+ // We may have to run test on next cycle, otherwise XPCOM components
+ // are not correctly updated.
+ // For ex: nsIFocusManager.getFocusedElementForWindow may throw
+ // NS_ERROR_ILLEGAL_VALUE exception.
+ require("../timers").setTimeout(_ => harness.runTests({
+ findAndRunTests: findAndRunTests,
+ iterations: cfxArgs.iterations || 1,
+ filter: cfxArgs.filter,
+ profileMemory: cfxArgs.profileMemory,
+ stopOnError: cfxArgs.stopOnError,
+ verbose: cfxArgs.verbose,
+ parseable: cfxArgs.parseable,
+ print: stdout.write,
+ onDone: onDone
+ }));
+}
+
+function printFailedTests(tests, print) {
+ let iterationNumber = 0;
+ let singleIteration = (tests.testRuns || []).length == 1;
+ let padding = singleIteration ? "" : " ";
+
+ print("\nThe following tests failed:\n");
+
+ for (let testRun of tests.testRuns) {
+ iterationNumber++;
+
+ if (!singleIteration)
+ print(" Iteration " + iterationNumber + ":\n");
+
+ for (let test of testRun) {
+ if (test.failed > 0) {
+ print(padding + " " + test.name + ": " + test.errors +"\n");
+ }
+ }
+ print("\n");
+ }
+}
+
+function main() {
+ var testsStarted = false;
+
+ if (!testsStarted) {
+ testsStarted = true;
+ runTests(function findAndRunTests(loader, nextIteration) {
+ loader.require("../deprecated/unit-test").findAndRunTests({
+ testOutOfProcess: false,
+ testInProcess: true,
+ stopOnError: cfxArgs.stopOnError,
+ filter: cfxArgs.filter,
+ onDone: nextIteration
+ });
+ });
+ }
+};
+
+if (require.main === module)
+ main();
+
+exports.runTestsFromModule = function runTestsFromModule(module) {
+ let id = module.id;
+ // Make a copy of exports as it may already be frozen by module loader
+ let exports = {};
+ Object.keys(module.exports).forEach(key => {
+ exports[key] = module.exports[key];
+ });
+
+ runTests(function findAndRunTests(loader, nextIteration) {
+ // Consider that all these tests are CommonJS ones
+ loader.require('../../test').run(exports);
+
+ // Reproduce what is done in sdk/deprecated/unit-test-finder.findTests()
+ let tests = [];
+ for (let name of Object.keys(exports).sort()) {
+ tests.push({
+ setup: exports.setup,
+ teardown: exports.teardown,
+ testFunction: exports[name],
+ name: id + "." + name
+ });
+ }
+
+ // Reproduce what is done by unit-test.findAndRunTests()
+ var { TestRunner } = loader.require("../deprecated/unit-test");
+ var runner = new TestRunner();
+ runner.startMany({
+ tests: {
+ getNext: () => resolve(tests.shift())
+ },
+ stopOnError: cfxArgs.stopOnError,
+ onDone: nextIteration
+ });
+ });
+}
diff --git a/toolkit/jetpack/sdk/test/utils.js b/toolkit/jetpack/sdk/test/utils.js
new file mode 100644
index 000000000..b01df67d4
--- /dev/null
+++ b/toolkit/jetpack/sdk/test/utils.js
@@ -0,0 +1,199 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+'use strict';
+
+module.metadata = {
+ 'stability': 'unstable'
+};
+
+const { defer } = require('../core/promise');
+const { setInterval, clearInterval } = require('../timers');
+const { getTabs, closeTab } = require("../tabs/utils");
+const { windows: getWindows } = require("../window/utils");
+const { close: closeWindow } = require("../window/helpers");
+const { isGenerator } = require("../lang/type");
+const { env } = require("../system/environment");
+const { Task } = require("resource://gre/modules/Task.jsm");
+
+const getTestNames = (exports) =>
+ Object.keys(exports).filter(name => /^test/.test(name));
+
+const isTestAsync = ({length}) => length > 1;
+const isHelperAsync = ({length}) => length > 2;
+
+/*
+ * Takes an `exports` object of a test file and a function `beforeFn`
+ * to be run before each test. `beforeFn` is called with a `name` string
+ * as the first argument of the test name, and may specify a second
+ * argument function `done` to indicate that this function should
+ * resolve asynchronously
+ */
+function before (exports, beforeFn) {
+ getTestNames(exports).map(name => {
+ let testFn = exports[name];
+
+ // GENERATOR TESTS
+ if (isGenerator(testFn) && isGenerator(beforeFn)) {
+ exports[name] = function*(assert) {
+ yield Task.spawn(beforeFn.bind(null, name, assert));
+ yield Task.spawn(testFn.bind(null, assert));
+ }
+ }
+ else if (isGenerator(testFn) && !isHelperAsync(beforeFn)) {
+ exports[name] = function*(assert) {
+ beforeFn(name, assert);
+ yield Task.spawn(testFn.bind(null, assert));
+ }
+ }
+ else if (isGenerator(testFn) && isHelperAsync(beforeFn)) {
+ exports[name] = function*(assert) {
+ yield new Promise(resolve => beforeFn(name, assert, resolve));
+ yield Task.spawn(testFn.bind(null, assert));
+ }
+ }
+ // SYNC TESTS
+ else if (!isTestAsync(testFn) && isGenerator(beforeFn)) {
+ exports[name] = function*(assert) {
+ yield Task.spawn(beforeFn.bind(null, name, assert));
+ testFn(assert);
+ };
+ }
+ else if (!isTestAsync(testFn) && !isHelperAsync(beforeFn)) {
+ exports[name] = function (assert) {
+ beforeFn(name, assert);
+ testFn(assert);
+ };
+ }
+ else if (!isTestAsync(testFn) && isHelperAsync(beforeFn)) {
+ exports[name] = function (assert, done) {
+ beforeFn(name, assert, () => {
+ testFn(assert);
+ done();
+ });
+ };
+ }
+ // ASYNC TESTS
+ else if (isTestAsync(testFn) && isGenerator(beforeFn)) {
+ exports[name] = function*(assert) {
+ yield Task.spawn(beforeFn.bind(null, name, assert));
+ yield new Promise(resolve => testFn(assert, resolve));
+ };
+ }
+ else if (isTestAsync(testFn) && !isHelperAsync(beforeFn)) {
+ exports[name] = function (assert, done) {
+ beforeFn(name, assert);
+ testFn(assert, done);
+ };
+ }
+ else if (isTestAsync(testFn) && isHelperAsync(beforeFn)) {
+ exports[name] = function (assert, done) {
+ beforeFn(name, assert, () => {
+ testFn(assert, done);
+ });
+ };
+ }
+ });
+}
+exports.before = before;
+
+/*
+ * Takes an `exports` object of a test file and a function `afterFn`
+ * to be run after each test. `afterFn` is called with a `name` string
+ * as the first argument of the test name, and may specify a second
+ * argument function `done` to indicate that this function should
+ * resolve asynchronously
+ */
+function after (exports, afterFn) {
+ getTestNames(exports).map(name => {
+ let testFn = exports[name];
+
+ // GENERATOR TESTS
+ if (isGenerator(testFn) && isGenerator(afterFn)) {
+ exports[name] = function*(assert) {
+ yield Task.spawn(testFn.bind(null, assert));
+ yield Task.spawn(afterFn.bind(null, name, assert));
+ }
+ }
+ else if (isGenerator(testFn) && !isHelperAsync(afterFn)) {
+ exports[name] = function*(assert) {
+ yield Task.spawn(testFn.bind(null, assert));
+ afterFn(name, assert);
+ }
+ }
+ else if (isGenerator(testFn) && isHelperAsync(afterFn)) {
+ exports[name] = function*(assert) {
+ yield Task.spawn(testFn.bind(null, assert));
+ yield new Promise(resolve => afterFn(name, assert, resolve));
+ }
+ }
+ // SYNC TESTS
+ else if (!isTestAsync(testFn) && isGenerator(afterFn)) {
+ exports[name] = function*(assert) {
+ testFn(assert);
+ yield Task.spawn(afterFn.bind(null, name, assert));
+ };
+ }
+ else if (!isTestAsync(testFn) && !isHelperAsync(afterFn)) {
+ exports[name] = function (assert) {
+ testFn(assert);
+ afterFn(name, assert);
+ };
+ }
+ else if (!isTestAsync(testFn) && isHelperAsync(afterFn)) {
+ exports[name] = function (assert, done) {
+ testFn(assert);
+ afterFn(name, assert, done);
+ };
+ }
+ // ASYNC TESTS
+ else if (isTestAsync(testFn) && isGenerator(afterFn)) {
+ exports[name] = function*(assert) {
+ yield new Promise(resolve => testFn(assert, resolve));
+ yield Task.spawn(afterFn.bind(null, name, assert));
+ };
+ }
+ else if (isTestAsync(testFn) && !isHelperAsync(afterFn)) {
+ exports[name] = function*(assert) {
+ yield new Promise(resolve => testFn(assert, resolve));
+ afterFn(name, assert);
+ };
+ }
+ else if (isTestAsync(testFn) && isHelperAsync(afterFn)) {
+ exports[name] = function*(assert) {
+ yield new Promise(resolve => testFn(assert, resolve));
+ yield new Promise(resolve => afterFn(name, assert, resolve));
+ };
+ }
+ });
+}
+exports.after = after;
+
+function waitUntil (predicate, delay) {
+ let { promise, resolve } = defer();
+ let interval = setInterval(() => {
+ if (!predicate()) return;
+ clearInterval(interval);
+ resolve();
+ }, delay || 10);
+ return promise;
+}
+exports.waitUntil = waitUntil;
+
+var cleanUI = function cleanUI() {
+ let { promise, resolve } = defer();
+
+ let windows = getWindows(null, { includePrivate: true });
+ if (windows.length > 1) {
+ return closeWindow(windows[1]).then(cleanUI);
+ }
+
+ getTabs(windows[0]).slice(1).forEach(closeTab);
+
+ resolve();
+
+ return promise;
+}
+exports.cleanUI = cleanUI;
+
+exports.isTravisCI = ("TRAVIS" in env && "CI" in env);