summaryrefslogtreecommitdiffstats
path: root/devtools/shared/tests/unit
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/shared/tests/unit')
-rw-r--r--devtools/shared/tests/unit/.eslintrc.js6
-rw-r--r--devtools/shared/tests/unit/exposeLoader.js8
-rw-r--r--devtools/shared/tests/unit/head_devtools.js60
-rw-r--r--devtools/shared/tests/unit/test_assert.js36
-rw-r--r--devtools/shared/tests/unit/test_async-utils.js157
-rw-r--r--devtools/shared/tests/unit/test_console_filtering.js133
-rw-r--r--devtools/shared/tests/unit/test_css-properties-db.js136
-rw-r--r--devtools/shared/tests/unit/test_csslexer.js242
-rw-r--r--devtools/shared/tests/unit/test_defer.js32
-rw-r--r--devtools/shared/tests/unit/test_defineLazyPrototypeGetter.js68
-rw-r--r--devtools/shared/tests/unit/test_executeSoon.js48
-rw-r--r--devtools/shared/tests/unit/test_fetch-bom.js76
-rw-r--r--devtools/shared/tests/unit/test_fetch-chrome.js31
-rw-r--r--devtools/shared/tests/unit/test_fetch-file.js104
-rw-r--r--devtools/shared/tests/unit/test_fetch-http.js61
-rw-r--r--devtools/shared/tests/unit/test_fetch-resource.js31
-rw-r--r--devtools/shared/tests/unit/test_flatten.js24
-rw-r--r--devtools/shared/tests/unit/test_indentation.js133
-rw-r--r--devtools/shared/tests/unit/test_independent_loaders.js20
-rw-r--r--devtools/shared/tests/unit/test_invisible_loader.js60
-rw-r--r--devtools/shared/tests/unit/test_isSet.js25
-rw-r--r--devtools/shared/tests/unit/test_pluralForm-english.js29
-rw-r--r--devtools/shared/tests/unit/test_pluralForm-makeGetter.js38
-rw-r--r--devtools/shared/tests/unit/test_prettifyCSS.js68
-rw-r--r--devtools/shared/tests/unit/test_require.js20
-rw-r--r--devtools/shared/tests/unit/test_require_lazy.js32
-rw-r--r--devtools/shared/tests/unit/test_require_raw.js19
-rw-r--r--devtools/shared/tests/unit/test_safeErrorString.js58
-rw-r--r--devtools/shared/tests/unit/test_stack.js45
-rw-r--r--devtools/shared/tests/unit/xpcshell.ini40
30 files changed, 1840 insertions, 0 deletions
diff --git a/devtools/shared/tests/unit/.eslintrc.js b/devtools/shared/tests/unit/.eslintrc.js
new file mode 100644
index 000000000..012428019
--- /dev/null
+++ b/devtools/shared/tests/unit/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the common devtools xpcshell eslintrc config.
+ "extends": "../../../.eslintrc.xpcshell.js"
+};
diff --git a/devtools/shared/tests/unit/exposeLoader.js b/devtools/shared/tests/unit/exposeLoader.js
new file mode 100644
index 000000000..949640a03
--- /dev/null
+++ b/devtools/shared/tests/unit/exposeLoader.js
@@ -0,0 +1,8 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+exports.exerciseLazyRequire = (name, path) => {
+ const o = {};
+ loader.lazyRequireGetter(o, name, path);
+ return o;
+};
diff --git a/devtools/shared/tests/unit/head_devtools.js b/devtools/shared/tests/unit/head_devtools.js
new file mode 100644
index 000000000..f0f47c93a
--- /dev/null
+++ b/devtools/shared/tests/unit/head_devtools.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+var Cr = Components.results;
+
+const {require, DevToolsLoader, devtools} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const flags = require("devtools/shared/flags");
+
+flags.testing = true;
+do_register_cleanup(() => {
+ flags.testing = false;
+});
+
+// Register a console listener, so console messages don't just disappear
+// into the ether.
+
+// If for whatever reason the test needs to post console errors that aren't
+// failures, set this to true.
+var ALLOW_CONSOLE_ERRORS = false;
+
+var errorCount = 0;
+var listener = {
+ observe: function (aMessage) {
+ errorCount++;
+ try {
+ // If we've been given an nsIScriptError, then we can print out
+ // something nicely formatted, for tools like Emacs to pick up.
+ var scriptError = aMessage.QueryInterface(Ci.nsIScriptError);
+ dump(aMessage.sourceName + ":" + aMessage.lineNumber + ": " +
+ scriptErrorFlagsToKind(aMessage.flags) + ": " +
+ aMessage.errorMessage + "\n");
+ var string = aMessage.errorMessage;
+ } catch (x) {
+ // Be a little paranoid with message, as the whole goal here is to lose
+ // no information.
+ try {
+ var string = "" + aMessage.message;
+ } catch (x) {
+ var string = "<error converting error message to string>";
+ }
+ }
+
+ // Make sure we exit all nested event loops so that the test can finish.
+ while (DebuggerServer.xpcInspector.eventLoopNestLevel > 0) {
+ DebuggerServer.xpcInspector.exitNestedEventLoop();
+ }
+
+ if (!ALLOW_CONSOLE_ERRORS) {
+ do_throw("head_devtools.js got console message: " + string + "\n");
+ }
+ }
+};
+
+var consoleService = Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService);
+consoleService.registerListener(listener);
diff --git a/devtools/shared/tests/unit/test_assert.js b/devtools/shared/tests/unit/test_assert.js
new file mode 100644
index 000000000..b87171751
--- /dev/null
+++ b/devtools/shared/tests/unit/test_assert.js
@@ -0,0 +1,36 @@
+/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test DevToolsUtils.assert
+
+ALLOW_CONSOLE_ERRORS = true;
+
+function run_test() {
+ // Enable assertions.
+ flags.testing = true;
+
+ const { assert } = DevToolsUtils;
+ equal(typeof assert, "function");
+
+ try {
+ assert(true, "this assertion should not fail");
+ } catch (e) {
+ // If you catch assertion failures in practice, I will hunt you down. I get
+ // email notifications every time it happens.
+ ok(false, "Should not get an error for an assertion that should not fail. Got "
+ + DevToolsUtils.safeErrorString(e));
+ }
+
+ let assertionFailed = false;
+ try {
+ assert(false, "this assertion should fail");
+ } catch (e) {
+ ok(e.message.startsWith("Assertion failure:"),
+ "Should be an assertion failure error");
+ assertionFailed = true;
+ }
+
+ ok(assertionFailed,
+ "The assertion should have failed, which should throw an error when assertions are enabled.");
+}
diff --git a/devtools/shared/tests/unit/test_async-utils.js b/devtools/shared/tests/unit/test_async-utils.js
new file mode 100644
index 000000000..2b09b8260
--- /dev/null
+++ b/devtools/shared/tests/unit/test_async-utils.js
@@ -0,0 +1,157 @@
+/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test async-utils.js
+
+const {Task} = require("devtools/shared/task");
+// |const| will not work because
+// it will make the Promise object immutable before assigning.
+// Using Object.defineProperty() instead.
+Object.defineProperty(this, "Promise", {
+ value: require("promise"),
+ writable: false, configurable: false
+});
+const {asyncOnce, promiseInvoke, promiseCall} = require("devtools/shared/async-utils");
+
+function run_test() {
+ do_test_pending();
+ Task.spawn(function* () {
+ yield test_async_args(asyncOnce);
+ yield test_async_return(asyncOnce);
+ yield test_async_throw(asyncOnce);
+
+ yield test_async_once();
+ yield test_async_invoke();
+ do_test_finished();
+ }).then(null, error => {
+ do_throw(error);
+ });
+}
+
+// Test that arguments are correctly passed through to the async function.
+function test_async_args(async) {
+ let obj = {
+ method: async(function* (a, b) {
+ do_check_eq(this, obj);
+ do_check_eq(a, "foo");
+ do_check_eq(b, "bar");
+ })
+ };
+
+ return obj.method("foo", "bar");
+}
+
+// Test that the return value from the async function is resolution value of
+// the promise.
+function test_async_return(async) {
+ let obj = {
+ method: async(function* (a, b) {
+ return a + b;
+ })
+ };
+
+ return obj.method("foo", "bar").then(ret => {
+ do_check_eq(ret, "foobar");
+ });
+}
+
+// Test that the throwing from an async function rejects the promise.
+function test_async_throw(async) {
+ let obj = {
+ method: async(function* () {
+ throw "boom";
+ })
+ };
+
+ return obj.method().then(null, error => {
+ do_check_eq(error, "boom");
+ });
+}
+
+// Test that asyncOnce only runs the async function once per instance and
+// returns the same promise for that instance.
+function test_async_once() {
+ let counter = 0;
+
+ function Foo() {}
+ Foo.prototype = {
+ ran: false,
+ method: asyncOnce(function* () {
+ yield Promise.resolve();
+ if (this.ran) {
+ do_throw("asyncOnce function unexpectedly ran twice on the same object");
+ }
+ this.ran = true;
+ return counter++;
+ })
+ };
+
+ let foo1 = new Foo();
+ let foo2 = new Foo();
+ let p1 = foo1.method();
+ let p2 = foo2.method();
+
+ do_check_neq(p1, p2);
+
+ let p3 = foo1.method();
+ do_check_eq(p1, p3);
+ do_check_false(foo1.ran);
+
+ let p4 = foo2.method();
+ do_check_eq(p2, p4);
+ do_check_false(foo2.ran);
+
+ return p1.then(ret => {
+ do_check_true(foo1.ran);
+ do_check_eq(ret, 0);
+ return p2;
+ }).then(ret => {
+ do_check_true(foo2.ran);
+ do_check_eq(ret, 1);
+ });
+}
+
+// Test invoke and call.
+function test_async_invoke() {
+ return Task.spawn(function* () {
+ function func(a, b, expectedThis, callback) {
+ "use strict";
+ do_check_eq(a, "foo");
+ do_check_eq(b, "bar");
+ do_check_eq(this, expectedThis);
+ callback(a + b);
+ }
+
+ // Test call.
+ let callResult = yield promiseCall(func, "foo", "bar", undefined);
+ do_check_eq(callResult, "foobar");
+
+
+ // Test invoke.
+ let obj = { method: func };
+ let invokeResult = yield promiseInvoke(obj, obj.method, "foo", "bar", obj);
+ do_check_eq(invokeResult, "foobar");
+
+
+ // Test passing multiple values to the callback.
+ function multipleResults(callback) {
+ callback("foo", "bar");
+ }
+
+ let results = yield promiseCall(multipleResults);
+ do_check_eq(results.length, 2);
+ do_check_eq(results[0], "foo");
+ do_check_eq(results[1], "bar");
+
+
+ // Test throwing from the function.
+ function thrower() {
+ throw "boom";
+ }
+
+ yield promiseCall(thrower).then(null, error => {
+ do_check_eq(error, "boom");
+ });
+ });
+}
diff --git a/devtools/shared/tests/unit/test_console_filtering.js b/devtools/shared/tests/unit/test_console_filtering.js
new file mode 100644
index 000000000..431e2b234
--- /dev/null
+++ b/devtools/shared/tests/unit/test_console_filtering.js
@@ -0,0 +1,133 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { console, ConsoleAPI } = require("resource://gre/modules/Console.jsm");
+const { ConsoleAPIListener } = require("devtools/server/actors/utils/webconsole-utils");
+const Services = require("Services");
+
+var seenMessages = 0;
+var seenTypes = 0;
+
+var callback = {
+ onConsoleAPICall: function (aMessage) {
+ if (aMessage.consoleID && aMessage.consoleID == "addon/foo") {
+ do_check_eq(aMessage.level, "warn");
+ do_check_eq(aMessage.arguments[0], "Warning from foo");
+ seenTypes |= 1;
+ } else if (aMessage.originAttributes &&
+ aMessage.originAttributes.addonId == "bar") {
+ do_check_eq(aMessage.level, "error");
+ do_check_eq(aMessage.arguments[0], "Error from bar");
+ seenTypes |= 2;
+ } else {
+ do_check_eq(aMessage.level, "log");
+ do_check_eq(aMessage.arguments[0], "Hello from default console");
+ seenTypes |= 4;
+ }
+ seenMessages++;
+ }
+};
+
+function createFakeAddonWindow({addonId} = {}) {
+ let baseURI = Services.io.newURI("about:blank", null, null);
+ let originAttributes = {addonId};
+ let principal = Services.scriptSecurityManager
+ .createCodebasePrincipal(baseURI, originAttributes);
+ let chromeWebNav = Services.appShell.createWindowlessBrowser(true);
+ let docShell = chromeWebNav.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDocShell);
+ docShell.createAboutBlankContentViewer(principal);
+ let addonWindow = docShell.contentViewer.DOMDocument.defaultView;
+
+ return {addonWindow, chromeWebNav};
+}
+
+/**
+ * Tests that the consoleID property of the ConsoleAPI options gets passed
+ * through to console messages.
+ */
+function run_test() {
+ // console1 Test Console.jsm messages tagged by the Addon SDK
+ // are still filtered correctly.
+ let console1 = new ConsoleAPI({
+ consoleID: "addon/foo"
+ });
+
+ // console2 - WebExtension page's console messages tagged
+ // by 'originAttributes.addonId' are filtered correctly.
+ let {addonWindow, chromeWebNav} = createFakeAddonWindow({addonId: "bar"});
+ let console2 = addonWindow.console;
+
+ // console - Plain console object (messages are tagged with window ids
+ // and originAttributes, but the addonId will be empty).
+ console.log("Hello from default console");
+
+ console1.warn("Warning from foo");
+ console2.error("Error from bar");
+
+ let listener = new ConsoleAPIListener(null, callback);
+ listener.init();
+ let messages = listener.getCachedMessages();
+
+ seenTypes = 0;
+ seenMessages = 0;
+ messages.forEach(callback.onConsoleAPICall);
+ do_check_eq(seenMessages, 3);
+ do_check_eq(seenTypes, 7);
+
+ seenTypes = 0;
+ seenMessages = 0;
+ console.log("Hello from default console");
+ console1.warn("Warning from foo");
+ console2.error("Error from bar");
+ do_check_eq(seenMessages, 3);
+ do_check_eq(seenTypes, 7);
+
+ listener.destroy();
+
+ listener = new ConsoleAPIListener(null, callback, {addonId: "foo"});
+ listener.init();
+ messages = listener.getCachedMessages();
+
+ seenTypes = 0;
+ seenMessages = 0;
+ messages.forEach(callback.onConsoleAPICall);
+ do_check_eq(seenMessages, 2);
+ do_check_eq(seenTypes, 1);
+
+ seenTypes = 0;
+ seenMessages = 0;
+ console.log("Hello from default console");
+ console1.warn("Warning from foo");
+ console2.error("Error from bar");
+ do_check_eq(seenMessages, 1);
+ do_check_eq(seenTypes, 1);
+
+ listener.destroy();
+
+ listener = new ConsoleAPIListener(null, callback, {addonId: "bar"});
+ listener.init();
+ messages = listener.getCachedMessages();
+
+ seenTypes = 0;
+ seenMessages = 0;
+ messages.forEach(callback.onConsoleAPICall);
+ do_check_eq(seenMessages, 3);
+ do_check_eq(seenTypes, 2);
+
+ seenTypes = 0;
+ seenMessages = 0;
+ console.log("Hello from default console");
+ console1.warn("Warning from foo");
+ console2.error("Error from bar");
+
+ do_check_eq(seenMessages, 1);
+ do_check_eq(seenTypes, 2);
+
+ listener.destroy();
+
+ // Close the addon window's chromeWebNav.
+ chromeWebNav.close();
+}
diff --git a/devtools/shared/tests/unit/test_css-properties-db.js b/devtools/shared/tests/unit/test_css-properties-db.js
new file mode 100644
index 000000000..108650a3e
--- /dev/null
+++ b/devtools/shared/tests/unit/test_css-properties-db.js
@@ -0,0 +1,136 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that the devtool's client-side CSS properties database is in sync with the values
+ * on the platform (in Nightly only). If they are not, then `mach devtools-css-db` needs
+ * to be run to make everything up to date. Nightly, aurora, beta, and release may have
+ * different CSS properties and values. These are based on preferences and compiler flags.
+ *
+ * This test broke uplifts as the database needed to be regenerated every uplift. The
+ * combination of compiler flags and preferences means that it's too difficult to
+ * statically determine which properties are enabled between Firefox releases.
+ *
+ * Because of these difficulties, the database only needs to be up to date with Nightly.
+ * It is a fallback that is only used if the remote debugging protocol doesn't support
+ * providing a CSS database, so it's ok if the provided properties don't exactly match
+ * the inspected target in this particular case.
+ */
+
+"use strict";
+
+const DOMUtils = Components.classes["@mozilla.org/inspector/dom-utils;1"]
+ .getService(Components.interfaces.inIDOMUtils);
+
+const {PSEUDO_ELEMENTS, CSS_PROPERTIES} = require("devtools/shared/css/generated/properties-db");
+const {generateCssProperties} = require("devtools/server/actors/css-properties");
+
+function run_test() {
+ const propertiesErrorMessage = "If this assertion fails, then the client side CSS " +
+ "properties list in devtools is out of sync with the " +
+ "CSS properties on the platform. To fix this " +
+ "assertion run `mach devtools-css-db` to re-generate " +
+ "the client side properties.";
+
+ // Check that the platform and client match for pseudo elements.
+ deepEqual(PSEUDO_ELEMENTS, DOMUtils.getCSSPseudoElementNames(), `The pseudo elements ` +
+ `match on the client and platform. ${propertiesErrorMessage}`);
+
+ /**
+ * Check that the platform and client match for the details on their CSS properties.
+ * Enumerate each property to aid in debugging.
+ */
+ const platformProperties = generateCssProperties();
+
+ for (let propertyName in CSS_PROPERTIES) {
+ const platformProperty = platformProperties[propertyName];
+ const clientProperty = CSS_PROPERTIES[propertyName];
+ const deepEqual = isJsonDeepEqual(platformProperty, clientProperty);
+
+ if (deepEqual) {
+ ok(true, `The static database and platform match for "${propertyName}".`);
+ } else {
+ const prefMessage = `The static database and platform do not match ` +
+ `for "${propertyName}".`;
+ if (getPreference(propertyName) === false) {
+ ok(true, `${prefMessage} However, there is a preference for disabling this ` +
+ `property on the current build.`);
+ } else {
+ ok(false, `${prefMessage} ${propertiesErrorMessage}`);
+ }
+ }
+ }
+
+ /**
+ * Check that the list of properties on the platform and client are the same. If
+ * they are not, check that there may be preferences that are disabling them on the
+ * target platform.
+ */
+ const mismatches = getKeyMismatches(platformProperties, CSS_PROPERTIES)
+ // Filter out OS-specific properties.
+ .filter(name => name && name.indexOf("-moz-osx-") === -1);
+
+ if (mismatches.length === 0) {
+ ok(true, "No client and platform CSS property database mismatches were found.");
+ }
+
+ mismatches.forEach(propertyName => {
+ ok(false, `The static database and platform do not agree on the property ` +
+ `"${propertyName}" ${propertiesErrorMessage}`);
+ });
+}
+
+/**
+ * Check JSON-serializable objects for deep equality.
+ */
+function isJsonDeepEqual(a, b) {
+ // Handle primitives.
+ if (a === b) {
+ return true;
+ }
+
+ // Handle arrays.
+ if (Array.isArray(a) && Array.isArray(b)) {
+ if (a.length !== b.length) {
+ return false;
+ }
+ for (let i = 0; i < a.length; i++) {
+ if (!isJsonDeepEqual(a[i], b[i])) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ // Handle objects
+ if (typeof a === "object" && typeof b === "object") {
+ for (let key in a) {
+ if (!isJsonDeepEqual(a[key], b[key])) {
+ return false;
+ }
+ }
+
+ return Object.keys(a).length === Object.keys(b).length;
+ }
+
+ // Not something handled by these cases, therefore not equal.
+ return false;
+}
+
+/**
+ * Take the keys of two objects, and return the ones that don't match.
+ *
+ * @param {Object} a
+ * @param {Object} b
+ * @return {Array} keys
+ */
+function getKeyMismatches(a, b) {
+ const aNames = Object.keys(a);
+ const bNames = Object.keys(b);
+ const aMismatches = aNames.filter(key => !bNames.includes(key));
+ const bMismatches = bNames.filter(key => {
+ return !aNames.includes(key) && !aMismatches.includes(key);
+ });
+
+ return aMismatches.concat(bMismatches);
+}
diff --git a/devtools/shared/tests/unit/test_csslexer.js b/devtools/shared/tests/unit/test_csslexer.js
new file mode 100644
index 000000000..35855640b
--- /dev/null
+++ b/devtools/shared/tests/unit/test_csslexer.js
@@ -0,0 +1,242 @@
+/* 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/.
+ */
+
+// This file is a copy of layout/style/test/test_csslexer.js, modified
+// to use both our pure-JS lexer and the DOMUtils lexer for
+// cross-checking.
+
+"use strict";
+
+const jsLexer = require("devtools/shared/css/lexer");
+const domutils = Components.classes["@mozilla.org/inspector/dom-utils;1"]
+ .getService(Components.interfaces.inIDOMUtils);
+
+// An object that acts like a CSSLexer but verifies that the DOM lexer
+// and the JS lexer do the same thing.
+function DoubleLexer(input) {
+ do_print("DoubleLexer input: " + input);
+ this.domLexer = domutils.getCSSLexer(input);
+ this.jsLexer = jsLexer.getCSSLexer(input);
+}
+
+DoubleLexer.prototype = {
+ checkState: function () {
+ equal(this.domLexer.lineNumber, this.jsLexer.lineNumber,
+ "check line number");
+ equal(this.domLexer.columnNumber, this.jsLexer.columnNumber,
+ "check column number");
+ },
+
+ get lineNumber() {
+ return this.domLexer.lineNumber;
+ },
+
+ get columnNumber() {
+ return this.domLexer.columnNumber;
+ },
+
+ performEOFFixup: function (inputString, preserveBackslash) {
+ let d = this.domLexer.performEOFFixup(inputString, preserveBackslash);
+ let j = this.jsLexer.performEOFFixup(inputString, preserveBackslash);
+
+ equal(d, j);
+ return d;
+ },
+
+ mungeNumber: function (token) {
+ if (token && (token.tokenType === "number" ||
+ token.tokenType === "percentage") &&
+ !token.isInteger) {
+ // The JS lexer does its computations in double, but the
+ // platform lexer does its computations in float. Account for
+ // this discrepancy in a way that's sufficient for this test.
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1163047
+ token.number = parseFloat(token.number.toPrecision(8));
+ }
+ },
+
+ nextToken: function () {
+ // Check state both before and after.
+ this.checkState();
+
+ let d = this.domLexer.nextToken();
+ let j = this.jsLexer.nextToken();
+
+ this.mungeNumber(d);
+ this.mungeNumber(j);
+
+ deepEqual(d, j);
+
+ this.checkState();
+
+ return d;
+ }
+};
+
+function test_lexer(cssText, tokenTypes) {
+ let lexer = new DoubleLexer(cssText);
+ let reconstructed = "";
+ let lastTokenEnd = 0;
+ let i = 0;
+ while (true) {
+ let token = lexer.nextToken();
+ if (!token) {
+ break;
+ }
+ let combined = token.tokenType;
+ if (token.text) {
+ combined += ":" + token.text;
+ }
+ equal(combined, tokenTypes[i]);
+ ok(token.endOffset > token.startOffset);
+ equal(token.startOffset, lastTokenEnd);
+ lastTokenEnd = token.endOffset;
+ reconstructed += cssText.substring(token.startOffset, token.endOffset);
+ ++i;
+ }
+ // Ensure that we saw the correct number of tokens.
+ equal(i, tokenTypes.length);
+ // Ensure that the reported offsets cover all the text.
+ equal(reconstructed, cssText);
+}
+
+var LEX_TESTS = [
+ ["simple", ["ident:simple"]],
+ ["simple: { hi; }",
+ ["ident:simple", "symbol::",
+ "whitespace", "symbol:{",
+ "whitespace", "ident:hi",
+ "symbol:;", "whitespace",
+ "symbol:}"]],
+ ["/* whatever */", ["comment"]],
+ ["'string'", ["string:string"]],
+ ['"string"', ["string:string"]],
+ ["rgb(1,2,3)", ["function:rgb", "number",
+ "symbol:,", "number",
+ "symbol:,", "number",
+ "symbol:)"]],
+ ["@media", ["at:media"]],
+ ["#hibob", ["id:hibob"]],
+ ["#123", ["hash:123"]],
+ ["23px", ["dimension:px"]],
+ ["23%", ["percentage"]],
+ ["url(http://example.com)", ["url:http://example.com"]],
+ ["url('http://example.com')", ["url:http://example.com"]],
+ ["url( 'http://example.com' )",
+ ["url:http://example.com"]],
+ // In CSS Level 3, this is an ordinary URL, not a BAD_URL.
+ ["url(http://example.com", ["url:http://example.com"]],
+ // See bug 1153981 to understand why this gets a SYMBOL token.
+ ["url(http://example.com @", ["bad_url:http://example.com", "symbol:@"]],
+ ["quo\\ting", ["ident:quoting"]],
+ ["'bad string\n", ["bad_string:bad string", "whitespace"]],
+ ["~=", ["includes"]],
+ ["|=", ["dashmatch"]],
+ ["^=", ["beginsmatch"]],
+ ["$=", ["endsmatch"]],
+ ["*=", ["containsmatch"]],
+
+ // URANGE may be on the way out, and it isn't used by devutils, so
+ // let's skip it.
+
+ ["<!-- html comment -->", ["htmlcomment", "whitespace", "ident:html",
+ "whitespace", "ident:comment", "whitespace",
+ "htmlcomment"]],
+
+ // earlier versions of CSS had "bad comment" tokens, but in level 3,
+ // unterminated comments are just comments.
+ ["/* bad comment", ["comment"]]
+];
+
+function test_lexer_linecol(cssText, locations) {
+ let lexer = new DoubleLexer(cssText);
+ let i = 0;
+ while (true) {
+ let token = lexer.nextToken();
+ let startLine = lexer.lineNumber;
+ let startColumn = lexer.columnNumber;
+
+ // We do this in a bit of a funny way so that we can also test the
+ // location of the EOF.
+ let combined = ":" + startLine + ":" + startColumn;
+ if (token) {
+ combined = token.tokenType + combined;
+ }
+
+ equal(combined, locations[i]);
+ ++i;
+
+ if (!token) {
+ break;
+ }
+ }
+ // Ensure that we saw the correct number of tokens.
+ equal(i, locations.length);
+}
+
+function test_lexer_eofchar(cssText, argText, expectedAppend,
+ expectedNoAppend) {
+ let lexer = new DoubleLexer(cssText);
+ while (lexer.nextToken()) {
+ // Nothing.
+ }
+
+ do_print("EOF char test, input = " + cssText);
+
+ let result = lexer.performEOFFixup(argText, true);
+ equal(result, expectedAppend);
+
+ result = lexer.performEOFFixup(argText, false);
+ equal(result, expectedNoAppend);
+}
+
+var LINECOL_TESTS = [
+ ["simple", ["ident:0:0", ":0:6"]],
+ ["\n stuff", ["whitespace:0:0", "ident:1:4", ":1:9"]],
+ ['"string with \\\nnewline" \r\n', ["string:0:0", "whitespace:1:8",
+ ":2:0"]]
+];
+
+var EOFCHAR_TESTS = [
+ ["hello", "hello"],
+ ["hello \\", "hello \\\\", "hello \\\uFFFD"],
+ ["'hello", "'hello'"],
+ ["\"hello", "\"hello\""],
+ ["'hello\\", "'hello\\\\'", "'hello'"],
+ ["\"hello\\", "\"hello\\\\\"", "\"hello\""],
+ ["/*hello", "/*hello*/"],
+ ["/*hello*", "/*hello*/"],
+ ["/*hello\\", "/*hello\\*/"],
+ ["url(hello", "url(hello)"],
+ ["url('hello", "url('hello')"],
+ ["url(\"hello", "url(\"hello\")"],
+ ["url(hello\\", "url(hello\\\\)", "url(hello\\\uFFFD)"],
+ ["url('hello\\", "url('hello\\\\')", "url('hello')"],
+ ["url(\"hello\\", "url(\"hello\\\\\")", "url(\"hello\")"],
+];
+
+function run_test() {
+ let text, result;
+ for ([text, result] of LEX_TESTS) {
+ test_lexer(text, result);
+ }
+
+ for ([text, result] of LINECOL_TESTS) {
+ test_lexer_linecol(text, result);
+ }
+
+ let expectedAppend, expectedNoAppend;
+ for ([text, expectedAppend, expectedNoAppend] of EOFCHAR_TESTS) {
+ if (!expectedNoAppend) {
+ expectedNoAppend = expectedAppend;
+ }
+ test_lexer_eofchar(text, text, expectedAppend, expectedNoAppend);
+ }
+
+ // Ensure that passing a different inputString to performEOFFixup
+ // doesn't cause an assertion trying to strip a backslash from the
+ // end of an empty string.
+ test_lexer_eofchar("'\\", "", "\\'", "'");
+}
diff --git a/devtools/shared/tests/unit/test_defer.js b/devtools/shared/tests/unit/test_defer.js
new file mode 100644
index 000000000..c0babe961
--- /dev/null
+++ b/devtools/shared/tests/unit/test_defer.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const defer = require("devtools/shared/defer");
+
+function testResolve() {
+ const deferred = defer();
+ deferred.resolve("success");
+ return deferred.promise;
+}
+
+function testReject() {
+ const deferred = defer();
+ deferred.reject("error");
+ return deferred.promise;
+}
+
+add_task(function* () {
+ const success = yield testResolve();
+ equal(success, "success");
+
+ let error;
+ try {
+ yield testReject();
+ } catch (e) {
+ error = e;
+ }
+
+ equal(error, "error");
+});
diff --git a/devtools/shared/tests/unit/test_defineLazyPrototypeGetter.js b/devtools/shared/tests/unit/test_defineLazyPrototypeGetter.js
new file mode 100644
index 000000000..d729e7473
--- /dev/null
+++ b/devtools/shared/tests/unit/test_defineLazyPrototypeGetter.js
@@ -0,0 +1,68 @@
+/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test DevToolsUtils.defineLazyPrototypeGetter
+
+function Class() {}
+DevToolsUtils.defineLazyPrototypeGetter(Class.prototype, "foo", () => []);
+
+
+function run_test() {
+ test_prototype_attributes();
+ test_instance_attributes();
+ test_multiple_instances();
+ test_callback_receiver();
+}
+
+function test_prototype_attributes() {
+ // Check that the prototype has a getter property with expected attributes.
+ let descriptor = Object.getOwnPropertyDescriptor(Class.prototype, "foo");
+ do_check_eq(typeof descriptor.get, "function");
+ do_check_eq(descriptor.set, undefined);
+ do_check_eq(descriptor.enumerable, false);
+ do_check_eq(descriptor.configurable, true);
+}
+
+function test_instance_attributes() {
+ // Instances should not have an own property until the lazy getter has been
+ // activated.
+ let instance = new Class();
+ do_check_false(instance.hasOwnProperty("foo"));
+ instance.foo;
+ do_check_true(instance.hasOwnProperty("foo"));
+
+ // Check that the instance has an own property with the expecred value and
+ // attributes after the lazy getter is activated.
+ let descriptor = Object.getOwnPropertyDescriptor(instance, "foo");
+ do_check_true(descriptor.value instanceof Array);
+ do_check_eq(descriptor.writable, true);
+ do_check_eq(descriptor.enumerable, false);
+ do_check_eq(descriptor.configurable, true);
+}
+
+function test_multiple_instances() {
+ let instance1 = new Class();
+ let instance2 = new Class();
+ let foo1 = instance1.foo;
+ let foo2 = instance2.foo;
+ // Check that the lazy getter returns the expected type of value.
+ do_check_true(foo1 instanceof Array);
+ do_check_true(foo2 instanceof Array);
+ // Make sure the lazy getter runs once and only once per instance.
+ do_check_eq(instance1.foo, foo1);
+ do_check_eq(instance2.foo, foo2);
+ // Make sure each instance gets its own unique value.
+ do_check_neq(foo1, foo2);
+}
+
+function test_callback_receiver() {
+ function Foo() {}
+ DevToolsUtils.defineLazyPrototypeGetter(Foo.prototype, "foo", function () {
+ return this;
+ });
+
+ // Check that the |this| value in the callback is the instance itself.
+ let instance = new Foo();
+ do_check_eq(instance.foo, instance);
+}
diff --git a/devtools/shared/tests/unit/test_executeSoon.js b/devtools/shared/tests/unit/test_executeSoon.js
new file mode 100644
index 000000000..6154a3e67
--- /dev/null
+++ b/devtools/shared/tests/unit/test_executeSoon.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Client request stacks should span the entire process from before making the
+ * request to handling the reply from the server. The server frames are not
+ * included, nor can they be in most cases, since the server can be a remote
+ * device.
+ */
+
+var { executeSoon } = require("devtools/shared/DevToolsUtils");
+var promise = require("promise");
+var defer = require("devtools/shared/defer");
+var Services = require("Services");
+
+var asyncStackEnabled =
+ Services.prefs.getBoolPref("javascript.options.asyncstack");
+
+do_register_cleanup(() => {
+ Services.prefs.setBoolPref("javascript.options.asyncstack",
+ asyncStackEnabled);
+});
+
+add_task(function* () {
+ Services.prefs.setBoolPref("javascript.options.asyncstack", true);
+
+ yield waitForTick();
+
+ let stack = Components.stack;
+ while (stack) {
+ do_print(stack.name);
+ if (stack.name == "waitForTick") {
+ // Reached back to outer function before executeSoon
+ ok(true, "Complete stack");
+ return;
+ }
+ stack = stack.asyncCaller || stack.caller;
+ }
+ ok(false, "Incomplete stack");
+});
+
+function waitForTick() {
+ let deferred = defer();
+ executeSoon(deferred.resolve);
+ return deferred.promise;
+}
diff --git a/devtools/shared/tests/unit/test_fetch-bom.js b/devtools/shared/tests/unit/test_fetch-bom.js
new file mode 100644
index 000000000..7f299b1ce
--- /dev/null
+++ b/devtools/shared/tests/unit/test_fetch-bom.js
@@ -0,0 +1,76 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Tests for DevToolsUtils.fetch BOM detection.
+
+const CC = Components.Constructor;
+
+const { HttpServer } = Cu.import("resource://testing-common/httpd.js", {});
+const BinaryOutputStream = CC("@mozilla.org/binaryoutputstream;1",
+ "nsIBinaryOutputStream", "setOutputStream");
+
+function write8(bos) {
+ bos.write8(0xef);
+ bos.write8(0xbb);
+ bos.write8(0xbf);
+ bos.write8(0x68);
+ bos.write8(0xc4);
+ bos.write8(0xb1);
+}
+
+function write16be(bos) {
+ bos.write8(0xfe);
+ bos.write8(0xff);
+ bos.write8(0x00);
+ bos.write8(0x68);
+ bos.write8(0x01);
+ bos.write8(0x31);
+}
+
+function write16le(bos) {
+ bos.write8(0xff);
+ bos.write8(0xfe);
+ bos.write8(0x68);
+ bos.write8(0x00);
+ bos.write8(0x31);
+ bos.write8(0x01);
+}
+
+function getHandler(writer) {
+ return function (request, response) {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+
+ let bos = new BinaryOutputStream(response.bodyOutputStream);
+ writer(bos);
+ };
+}
+
+const server = new HttpServer();
+server.registerDirectory("/", do_get_cwd());
+server.registerPathHandler("/u8", getHandler(write8));
+server.registerPathHandler("/u16be", getHandler(write16be));
+server.registerPathHandler("/u16le", getHandler(write16le));
+server.start(-1);
+
+const port = server.identity.primaryPort;
+const serverURL = "http://localhost:" + port;
+
+do_register_cleanup(() => {
+ return new Promise(resolve => server.stop(resolve));
+});
+
+add_task(function* () {
+ yield test_one(serverURL + "/u8", "UTF-8");
+ yield test_one(serverURL + "/u16be", "UTF-16BE");
+ yield test_one(serverURL + "/u16le", "UTF-16LE");
+});
+
+function* test_one(url, encoding) {
+ // Be sure to set the encoding to something that will yield an
+ // invalid result if BOM sniffing is not done.
+ yield DevToolsUtils.fetch(url, { charset: "ISO-8859-1" }).then(({content}) => {
+ do_check_eq(content, "hı", "The content looks correct for " + encoding);
+ });
+}
diff --git a/devtools/shared/tests/unit/test_fetch-chrome.js b/devtools/shared/tests/unit/test_fetch-chrome.js
new file mode 100644
index 000000000..441d4c7ba
--- /dev/null
+++ b/devtools/shared/tests/unit/test_fetch-chrome.js
@@ -0,0 +1,31 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Tests for DevToolsUtils.fetch on chrome:// URI's.
+
+const URL_FOUND = "chrome://devtools-shared/locale/debugger.properties";
+const URL_NOT_FOUND = "chrome://this/is/not/here.js";
+
+/**
+ * Test that non-existent files are handled correctly.
+ */
+add_task(function* test_missing() {
+ yield DevToolsUtils.fetch(URL_NOT_FOUND).then(result => {
+ do_print(result);
+ ok(false, "fetch resolved unexpectedly for non-existent chrome:// URI");
+ }, () => {
+ ok(true, "fetch rejected as the chrome:// URI was non-existent.");
+ });
+});
+
+/**
+ * Tests that existing files are handled correctly.
+ */
+add_task(function* test_normal() {
+ yield DevToolsUtils.fetch(URL_FOUND).then(result => {
+ notDeepEqual(result.content, "",
+ "chrome:// URI seems to be read correctly.");
+ });
+});
diff --git a/devtools/shared/tests/unit/test_fetch-file.js b/devtools/shared/tests/unit/test_fetch-file.js
new file mode 100644
index 000000000..d9d712322
--- /dev/null
+++ b/devtools/shared/tests/unit/test_fetch-file.js
@@ -0,0 +1,104 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Tests for DevToolsUtils.fetch on file:// URI's.
+
+const { FileUtils } = Cu.import("resource://gre/modules/FileUtils.jsm", {});
+const { OS } = Cu.import("resource://gre/modules/osfile.jsm", {});
+
+const TEST_CONTENT = "aéd";
+
+// The TEST_CONTENT encoded as UTF-8.
+const UTF8_TEST_BUFFER = new Uint8Array([0x61, 0xc3, 0xa9, 0x64]);
+
+// The TEST_CONTENT encoded as ISO 8859-1.
+const ISO_8859_1_BUFFER = new Uint8Array([0x61, 0xe9, 0x64]);
+
+/**
+ * Tests that URLs with arrows pointing to an actual source are handled properly
+ * (bug 808960). For example 'resource://gre/modules/XPIProvider.jsm ->
+ * file://l10n.js' should load 'file://l10n.js'.
+ */
+add_task(function* test_arrow_urls() {
+ let { path } = createTemporaryFile(".js");
+ let url = "resource://gre/modules/XPIProvider.jsm -> file://" + path;
+
+ yield OS.File.writeAtomic(path, TEST_CONTENT, { encoding: "utf-8" });
+ let { content } = yield DevToolsUtils.fetch(url);
+
+ deepEqual(content, TEST_CONTENT, "The file contents were correctly read.");
+});
+
+/**
+ * Tests that empty files are read correctly.
+ */
+add_task(function* test_empty() {
+ let { path } = createTemporaryFile();
+ let { content } = yield DevToolsUtils.fetch("file://" + path);
+ deepEqual(content, "", "The empty file was read correctly.");
+});
+
+/**
+ * Tests that UTF-8 encoded files are correctly read.
+ */
+add_task(function* test_encoding_utf8() {
+ let { path } = createTemporaryFile();
+ yield OS.File.writeAtomic(path, UTF8_TEST_BUFFER);
+
+ let { content } = yield DevToolsUtils.fetch(path);
+ deepEqual(content, TEST_CONTENT,
+ "The UTF-8 encoded file was correctly read.");
+});
+
+/**
+ * Tests that ISO 8859-1 (Latin-1) encoded files are correctly read.
+ */
+add_task(function* test_encoding_iso_8859_1() {
+ let { path } = createTemporaryFile();
+ yield OS.File.writeAtomic(path, ISO_8859_1_BUFFER);
+
+ let { content } = yield DevToolsUtils.fetch(path);
+ deepEqual(content, TEST_CONTENT,
+ "The ISO 8859-1 encoded file was correctly read.");
+});
+
+/**
+ * Test that non-existent files are handled correctly.
+ */
+add_task(function* test_missing() {
+ yield DevToolsUtils.fetch("file:///file/not/found.right").then(result => {
+ do_print(result);
+ ok(false, "Fetch resolved unexpectedly when the file was not found.");
+ }, () => {
+ ok(true, "Fetch rejected as expected because the file was not found.");
+ });
+});
+
+/**
+ * Test that URLs without file:// scheme work.
+ */
+add_task(function* test_schemeless_files() {
+ let { path } = createTemporaryFile();
+
+ yield OS.File.writeAtomic(path, TEST_CONTENT, { encoding: "utf-8" });
+
+ let { content } = yield DevToolsUtils.fetch(path);
+ deepEqual(content, TEST_CONTENT, "The content was correct.");
+});
+
+/**
+ * Creates a temporary file that is removed after the test completes.
+ */
+function createTemporaryFile(extension) {
+ let name = "test_fetch-file-" + Math.random() + (extension || "");
+ let file = FileUtils.getFile("TmpD", [name]);
+ file.create(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("0755", 8));
+
+ do_register_cleanup(() => {
+ file.remove(false);
+ });
+
+ return file;
+}
diff --git a/devtools/shared/tests/unit/test_fetch-http.js b/devtools/shared/tests/unit/test_fetch-http.js
new file mode 100644
index 000000000..028fa33ee
--- /dev/null
+++ b/devtools/shared/tests/unit/test_fetch-http.js
@@ -0,0 +1,61 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Tests for DevToolsUtils.fetch on http:// URI's.
+
+const { HttpServer } = Cu.import("resource://testing-common/httpd.js", {});
+
+const server = new HttpServer();
+server.registerDirectory("/", do_get_cwd());
+server.registerPathHandler("/cached.json", cacheRequestHandler);
+server.start(-1);
+
+const port = server.identity.primaryPort;
+const serverURL = "http://localhost:" + port;
+const CACHED_URL = serverURL + "/cached.json";
+const NORMAL_URL = serverURL + "/test_fetch-http.js";
+
+function cacheRequestHandler(request, response) {
+ do_print("Got request for " + request.path);
+ response.setHeader("Cache-Control", "max-age=10000", false);
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "application/json", false);
+
+ let body = "[" + Math.random() + "]";
+ response.bodyOutputStream.write(body, body.length);
+}
+
+do_register_cleanup(() => {
+ return new Promise(resolve => server.stop(resolve));
+});
+
+add_task(function* test_normal() {
+ yield DevToolsUtils.fetch(NORMAL_URL).then(({content}) => {
+ ok(content.includes("The content looks correct."),
+ "The content looks correct.");
+ });
+});
+
+add_task(function* test_caching() {
+ let initialContent = null;
+
+ do_print("Performing the first request.");
+ yield DevToolsUtils.fetch(CACHED_URL).then(({content}) => {
+ do_print("Got the first response: " + content);
+ initialContent = content;
+ });
+
+ do_print("Performing another request, expecting to get cached response.");
+ yield DevToolsUtils.fetch(CACHED_URL).then(({content}) => {
+ deepEqual(content, initialContent, "The content was loaded from cache.");
+ });
+
+ do_print("Performing a third request with cache bypassed.");
+ let opts = { loadFromCache: false };
+ yield DevToolsUtils.fetch(CACHED_URL, opts).then(({content}) => {
+ notDeepEqual(content, initialContent,
+ "The URL wasn't loaded from cache with loadFromCache: false.");
+ });
+});
diff --git a/devtools/shared/tests/unit/test_fetch-resource.js b/devtools/shared/tests/unit/test_fetch-resource.js
new file mode 100644
index 000000000..45e79cfa1
--- /dev/null
+++ b/devtools/shared/tests/unit/test_fetch-resource.js
@@ -0,0 +1,31 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Tests for DevToolsUtils.fetch on resource:// URI's.
+
+const URL_FOUND = "resource://devtools/shared/DevToolsUtils.js";
+const URL_NOT_FOUND = "resource://devtools/this/is/not/here.js";
+
+/**
+ * Test that non-existent files are handled correctly.
+ */
+add_task(function* test_missing() {
+ yield DevToolsUtils.fetch(URL_NOT_FOUND).then(result => {
+ do_print(result);
+ ok(false, "fetch resolved unexpectedly for non-existent resource:// URI");
+ }, () => {
+ ok(true, "fetch rejected as the resource:// URI was non-existent.");
+ });
+});
+
+/**
+ * Tests that existing files are handled correctly.
+ */
+add_task(function* test_normal() {
+ yield DevToolsUtils.fetch(URL_FOUND).then(result => {
+ notDeepEqual(result.content, "",
+ "resource:// URI seems to be read correctly.");
+ });
+});
diff --git a/devtools/shared/tests/unit/test_flatten.js b/devtools/shared/tests/unit/test_flatten.js
new file mode 100644
index 000000000..f5a186770
--- /dev/null
+++ b/devtools/shared/tests/unit/test_flatten.js
@@ -0,0 +1,24 @@
+/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test ThreadSafeDevToolsUtils.flatten
+
+function run_test() {
+ const { flatten } = DevToolsUtils;
+
+ const flat = flatten([["a", "b", "c"],
+ ["d", "e", "f"],
+ ["g", "h", "i"]]);
+
+ equal(flat.length, 9);
+ equal(flat[0], "a");
+ equal(flat[1], "b");
+ equal(flat[2], "c");
+ equal(flat[3], "d");
+ equal(flat[4], "e");
+ equal(flat[5], "f");
+ equal(flat[6], "g");
+ equal(flat[7], "h");
+ equal(flat[8], "i");
+}
diff --git a/devtools/shared/tests/unit/test_indentation.js b/devtools/shared/tests/unit/test_indentation.js
new file mode 100644
index 000000000..f7e4403d4
--- /dev/null
+++ b/devtools/shared/tests/unit/test_indentation.js
@@ -0,0 +1,133 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const Services = require("Services");
+const {
+ EXPAND_TAB,
+ TAB_SIZE,
+ DETECT_INDENT,
+ getIndentationFromPrefs,
+ getIndentationFromIteration,
+ getIndentationFromString,
+} = require("devtools/shared/indentation");
+
+function test_indent_from_prefs() {
+ Services.prefs.setBoolPref(DETECT_INDENT, true);
+ equal(getIndentationFromPrefs(), false,
+ "getIndentationFromPrefs returning false");
+
+ Services.prefs.setIntPref(TAB_SIZE, 73);
+ Services.prefs.setBoolPref(EXPAND_TAB, false);
+ Services.prefs.setBoolPref(DETECT_INDENT, false);
+ deepEqual(getIndentationFromPrefs(), {indentUnit: 73, indentWithTabs: true},
+ "getIndentationFromPrefs basic test");
+}
+
+const TESTS = [
+ {
+ desc: "two spaces",
+ input: [
+ "/*",
+ " * tricky comment block",
+ " */",
+ "div {",
+ " color: red;",
+ " background: blue;",
+ "}",
+ " ",
+ "span {",
+ " padding-left: 10px;",
+ "}"
+ ],
+ expected: {indentUnit: 2, indentWithTabs: false}
+ },
+ {
+ desc: "four spaces",
+ input: [
+ "var obj = {",
+ " addNumbers: function() {",
+ " var x = 5;",
+ " var y = 18;",
+ " return x + y;",
+ " },",
+ " ",
+ " /*",
+ " * Do some stuff to two numbers",
+ " * ",
+ " * @param x",
+ " * @param y",
+ " * ",
+ " * @return the result of doing stuff",
+ " */",
+ " subtractNumbers: function(x, y) {",
+ " var x += 7;",
+ " var y += 18;",
+ " var result = x - y;",
+ " result %= 2;",
+ " }",
+ "}"
+ ],
+ expected: {indentUnit: 4, indentWithTabs: false}
+ },
+ {
+ desc: "tabs",
+ input: [
+ "/*",
+ " * tricky comment block",
+ " */",
+ "div {",
+ "\tcolor: red;",
+ "\tbackground: blue;",
+ "}",
+ "",
+ "span {",
+ "\tpadding-left: 10px;",
+ "}"
+ ],
+ expected: {indentUnit: 2, indentWithTabs: true}
+ },
+ {
+ desc: "no indent",
+ input: [
+ "var x = 0;",
+ " // stray thing",
+ "var y = 9;",
+ " ",
+ ""
+ ],
+ expected: {indentUnit: 2, indentWithTabs: false}
+ },
+];
+
+function test_indent_detection() {
+ Services.prefs.setIntPref(TAB_SIZE, 2);
+ Services.prefs.setBoolPref(EXPAND_TAB, true);
+ Services.prefs.setBoolPref(DETECT_INDENT, true);
+
+ for (let test of TESTS) {
+ let iterFn = function (start, end, callback) {
+ test.input.slice(start, end).forEach(callback);
+ };
+
+ deepEqual(getIndentationFromIteration(iterFn), test.expected,
+ "test getIndentationFromIteration " + test.desc);
+ }
+
+ for (let test of TESTS) {
+ deepEqual(getIndentationFromString(test.input.join("\n")), test.expected,
+ "test getIndentationFromString " + test.desc);
+ }
+}
+
+function run_test() {
+ try {
+ test_indent_from_prefs();
+ test_indent_detection();
+ } finally {
+ Services.prefs.clearUserPref(TAB_SIZE);
+ Services.prefs.clearUserPref(EXPAND_TAB);
+ Services.prefs.clearUserPref(DETECT_INDENT);
+ }
+}
diff --git a/devtools/shared/tests/unit/test_independent_loaders.js b/devtools/shared/tests/unit/test_independent_loaders.js
new file mode 100644
index 000000000..20abed27d
--- /dev/null
+++ b/devtools/shared/tests/unit/test_independent_loaders.js
@@ -0,0 +1,20 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Ensure that each instance of the Dev Tools loader contains its own loader
+ * instance, and also returns unique objects. This ensures there is no sharing
+ * in place between loaders.
+ */
+function run_test() {
+ let loader1 = new DevToolsLoader();
+ let loader2 = new DevToolsLoader();
+
+ let indent1 = loader1.require("devtools/shared/indentation");
+ let indent2 = loader2.require("devtools/shared/indentation");
+
+ do_check_true(indent1 !== indent2);
+
+ do_check_true(loader1._provider !== loader2._provider);
+ do_check_true(loader1._provider.loader !== loader2._provider.loader);
+}
diff --git a/devtools/shared/tests/unit/test_invisible_loader.js b/devtools/shared/tests/unit/test_invisible_loader.js
new file mode 100644
index 000000000..96a68abc4
--- /dev/null
+++ b/devtools/shared/tests/unit/test_invisible_loader.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { addDebuggerToGlobal } = Cu.import("resource://gre/modules/jsdebugger.jsm", {});
+addDebuggerToGlobal(this);
+
+/**
+ * Ensure that sandboxes created via the Dev Tools loader respect the
+ * invisibleToDebugger flag.
+ */
+function run_test() {
+ visible_loader();
+ invisible_loader();
+}
+
+function visible_loader() {
+ let loader = new DevToolsLoader();
+ loader.invisibleToDebugger = false;
+ loader.require("devtools/shared/indentation");
+
+ let dbg = new Debugger();
+ let sandbox = loader._provider.loader.sharedGlobalSandbox;
+
+ try {
+ dbg.addDebuggee(sandbox);
+ do_check_true(true);
+ } catch (e) {
+ do_throw("debugger could not add visible value");
+ }
+
+ // Check that for common loader used for tabs, promise modules is Promise.jsm
+ // Which is required to support unhandled promises rejection in mochitests
+ const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
+ do_check_eq(loader.require("promise"), promise);
+}
+
+function invisible_loader() {
+ let loader = new DevToolsLoader();
+ loader.invisibleToDebugger = true;
+ loader.require("devtools/shared/indentation");
+
+ let dbg = new Debugger();
+ let sandbox = loader._provider.loader.sharedGlobalSandbox;
+
+ try {
+ dbg.addDebuggee(sandbox);
+ do_throw("debugger added invisible value");
+ } catch (e) {
+ do_check_true(true);
+ }
+
+ // But for browser toolbox loader, promise is loaded as a regular modules out
+ // of Promise-backend.js, that to be invisible to the debugger and not step
+ // into it.
+ const promise = loader.require("promise");
+ const promiseModule = loader._provider.loader.modules["resource://gre/modules/Promise-backend.js"];
+ do_check_eq(promise, promiseModule.exports);
+}
diff --git a/devtools/shared/tests/unit/test_isSet.js b/devtools/shared/tests/unit/test_isSet.js
new file mode 100644
index 000000000..f85d6ae3c
--- /dev/null
+++ b/devtools/shared/tests/unit/test_isSet.js
@@ -0,0 +1,25 @@
+/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test ThreadSafeDevToolsUtils.isSet
+
+function run_test() {
+ const { isSet } = DevToolsUtils;
+
+ equal(isSet(new Set()), true);
+ equal(isSet(new Map()), false);
+ equal(isSet({}), false);
+ equal(isSet("I swear I'm a Set"), false);
+ equal(isSet(5), false);
+
+ const systemPrincipal = Cc["@mozilla.org/systemprincipal;1"]
+ .createInstance(Ci.nsIPrincipal);
+ const sandbox = new Cu.Sandbox(systemPrincipal);
+
+ equal(isSet(Cu.evalInSandbox("new Set()", sandbox)), true);
+ equal(isSet(Cu.evalInSandbox("new Map()", sandbox)), false);
+ equal(isSet(Cu.evalInSandbox("({})", sandbox)), false);
+ equal(isSet(Cu.evalInSandbox("'I swear I\\'m a Set'", sandbox)), false);
+ equal(isSet(Cu.evalInSandbox("5", sandbox)), false);
+}
diff --git a/devtools/shared/tests/unit/test_pluralForm-english.js b/devtools/shared/tests/unit/test_pluralForm-english.js
new file mode 100644
index 000000000..67cd3b712
--- /dev/null
+++ b/devtools/shared/tests/unit/test_pluralForm-english.js
@@ -0,0 +1,29 @@
+/* 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";
+
+/**
+ * This unit test makes sure the plural form for Irish Gaeilge is working by
+ * using the makeGetter method instead of using the default language (by
+ * development), English.
+ */
+
+const {PluralForm} = require("devtools/shared/plural-form");
+
+function run_test() {
+ // English has 2 plural forms
+ do_check_eq(2, PluralForm.numForms());
+
+ // Make sure for good inputs, things work as expected
+ for (let num = 0; num <= 200; num++) {
+ do_check_eq(num == 1 ? "word" : "words", PluralForm.get(num, "word;words"));
+ }
+
+ // Not having enough plural forms defaults to the first form
+ do_check_eq("word", PluralForm.get(2, "word"));
+
+ // Empty forms defaults to the first form
+ do_check_eq("word", PluralForm.get(2, "word;"));
+}
diff --git a/devtools/shared/tests/unit/test_pluralForm-makeGetter.js b/devtools/shared/tests/unit/test_pluralForm-makeGetter.js
new file mode 100644
index 000000000..a9d4928b1
--- /dev/null
+++ b/devtools/shared/tests/unit/test_pluralForm-makeGetter.js
@@ -0,0 +1,38 @@
+/* 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";
+
+/**
+ * This unit test makes sure the plural form for Irish Gaeilge is working by
+ * using the makeGetter method instead of using the default language (by
+ * development), English.
+ */
+
+const {PluralForm} = require("devtools/shared/plural-form");
+
+function run_test() {
+ // Irish is plural rule #11
+ let [get, numForms] = PluralForm.makeGetter(11);
+
+ // Irish has 5 plural forms
+ do_check_eq(5, numForms());
+
+ // I don't really know Irish, so I'll stick in some dummy text
+ let words = "is 1;is 2;is 3-6;is 7-10;everything else";
+
+ let test = function (text, low, high) {
+ for (let num = low; num <= high; num++) {
+ do_check_eq(text, get(num, words));
+ }
+ };
+
+ // Make sure for good inputs, things work as expected
+ test("everything else", 0, 0);
+ test("is 1", 1, 1);
+ test("is 2", 2, 2);
+ test("is 3-6", 3, 6);
+ test("is 7-10", 7, 10);
+ test("everything else", 11, 200);
+}
diff --git a/devtools/shared/tests/unit/test_prettifyCSS.js b/devtools/shared/tests/unit/test_prettifyCSS.js
new file mode 100644
index 000000000..8415c9b83
--- /dev/null
+++ b/devtools/shared/tests/unit/test_prettifyCSS.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test prettifyCSS.
+
+"use strict";
+
+const {prettifyCSS} = require("devtools/shared/inspector/css-logic");
+
+const TESTS = [
+ { name: "simple test",
+ input: "div { font-family:'Arial Black', Arial, sans-serif; }",
+ expected: [
+ "div {",
+ "\tfont-family:'Arial Black', Arial, sans-serif;",
+ "}"
+ ]
+ },
+
+ { name: "whitespace before open brace",
+ input: "div{}",
+ expected: [
+ "div {",
+ "}"
+ ]
+ },
+
+ { name: "minified with trailing newline",
+ input: "\nbody{background:white;}div{font-size:4em;color:red}span{color:green;}\n",
+ expected: [
+ "body {",
+ "\tbackground:white;",
+ "}",
+ "div {",
+ "\tfont-size:4em;",
+ "\tcolor:red",
+ "}",
+ "span {",
+ "\tcolor:green;",
+ "}"
+ ]
+ },
+
+ { name: "leading whitespace",
+ input: "\n div{color: red;}",
+ expected: [
+ "div {",
+ "\tcolor: red;",
+ "}"
+ ]
+ },
+];
+
+function run_test() {
+ // Note that prettifyCSS.LINE_SEPARATOR is computed lazily, so we
+ // ensure it is set.
+ prettifyCSS("");
+
+ for (let test of TESTS) {
+ do_print(test.name);
+
+ let input = test.input.split("\n").join(prettifyCSS.LINE_SEPARATOR);
+ let output = prettifyCSS(input);
+ let expected = test.expected.join(prettifyCSS.LINE_SEPARATOR) +
+ prettifyCSS.LINE_SEPARATOR;
+ equal(output, expected, test.name);
+ }
+}
diff --git a/devtools/shared/tests/unit/test_require.js b/devtools/shared/tests/unit/test_require.js
new file mode 100644
index 000000000..005e56656
--- /dev/null
+++ b/devtools/shared/tests/unit/test_require.js
@@ -0,0 +1,20 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test require
+
+// Ensure that DevtoolsLoader.require doesn't spawn multiple
+// loader/modules when early cached
+function testBug1091706() {
+ let loader = new DevToolsLoader();
+ let require = loader.require;
+
+ let indent1 = require("devtools/shared/indentation");
+ let indent2 = require("devtools/shared/indentation");
+
+ do_check_true(indent1 === indent2);
+}
+
+function run_test() {
+ testBug1091706();
+}
diff --git a/devtools/shared/tests/unit/test_require_lazy.js b/devtools/shared/tests/unit/test_require_lazy.js
new file mode 100644
index 000000000..8ef5a7d23
--- /dev/null
+++ b/devtools/shared/tests/unit/test_require_lazy.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test devtools.lazyRequireGetter
+
+function run_test() {
+ const name = "asyncUtils";
+ const path = "devtools/shared/async-utils";
+ const o = {};
+ devtools.lazyRequireGetter(o, name, path);
+ const asyncUtils = require(path);
+ // XXX: do_check_eq only works on primitive types, so we have this
+ // do_check_true of an equality expression.
+ do_check_true(o.asyncUtils === asyncUtils);
+
+ // A non-main loader should get a new object via |lazyRequireGetter|, just
+ // as it would via a direct |require|.
+ const o2 = {};
+ let loader = new DevToolsLoader();
+
+ // We have to init the loader by loading any module before lazyRequireGetter is available
+ loader.require("devtools/shared/DevToolsUtils");
+
+ loader.lazyRequireGetter(o2, name, path);
+ do_check_true(o2.asyncUtils !== asyncUtils);
+
+ // A module required via a non-main loader that then uses |lazyRequireGetter|
+ // should also get the same object from that non-main loader.
+ let exposeLoader = loader.require("xpcshell-test/exposeLoader");
+ const o3 = exposeLoader.exerciseLazyRequire(name, path);
+ do_check_true(o3.asyncUtils === o2.asyncUtils);
+}
diff --git a/devtools/shared/tests/unit/test_require_raw.js b/devtools/shared/tests/unit/test_require_raw.js
new file mode 100644
index 000000000..5bec82b55
--- /dev/null
+++ b/devtools/shared/tests/unit/test_require_raw.js
@@ -0,0 +1,19 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test require using "raw!".
+
+function run_test() {
+ let loader = new DevToolsLoader();
+ let require = loader.require;
+
+ let variableFileContents = require("raw!devtools/client/themes/variables.css");
+ ok(variableFileContents.length > 0, "raw browserRequire worked");
+
+ let propertiesFileContents = require("raw!devtools/client/locales/shared.properties");
+ ok(propertiesFileContents.length > 0, "unprefixed properties raw require worked");
+
+ let chromePropertiesFileContents =
+ require("raw!chrome://devtools/locale/shared.properties");
+ ok(chromePropertiesFileContents.length > 0, "prefixed properties raw require worked");
+}
diff --git a/devtools/shared/tests/unit/test_safeErrorString.js b/devtools/shared/tests/unit/test_safeErrorString.js
new file mode 100644
index 000000000..9892f34d1
--- /dev/null
+++ b/devtools/shared/tests/unit/test_safeErrorString.js
@@ -0,0 +1,58 @@
+/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test DevToolsUtils.safeErrorString
+
+function run_test() {
+ test_with_error();
+ test_with_tricky_error();
+ test_with_string();
+ test_with_thrower();
+ test_with_psychotic();
+}
+
+function test_with_error() {
+ let s = DevToolsUtils.safeErrorString(new Error("foo bar"));
+ // Got the message.
+ do_check_true(s.includes("foo bar"));
+ // Got the stack.
+ do_check_true(s.includes("test_with_error"));
+ do_check_true(s.includes("test_safeErrorString.js"));
+ // Got the lineNumber and columnNumber.
+ do_check_true(s.includes("Line"));
+ do_check_true(s.includes("column"));
+}
+
+function test_with_tricky_error() {
+ let e = new Error("batman");
+ e.stack = { toString: Object.create(null) };
+ let s = DevToolsUtils.safeErrorString(e);
+ // Still got the message, despite a bad stack property.
+ do_check_true(s.includes("batman"));
+}
+
+function test_with_string() {
+ let s = DevToolsUtils.safeErrorString("not really an error");
+ // Still get the message.
+ do_check_true(s.includes("not really an error"));
+}
+
+function test_with_thrower() {
+ let s = DevToolsUtils.safeErrorString({
+ toString: () => {
+ throw new Error("Muahahaha");
+ }
+ });
+ // Still don't fail, get string back.
+ do_check_eq(typeof s, "string");
+}
+
+function test_with_psychotic() {
+ let s = DevToolsUtils.safeErrorString({
+ toString: () => Object.create(null)
+ });
+ // Still get a string out, and no exceptions thrown
+ do_check_eq(typeof s, "string");
+ do_check_eq(s, "[object Object]");
+}
diff --git a/devtools/shared/tests/unit/test_stack.js b/devtools/shared/tests/unit/test_stack.js
new file mode 100644
index 000000000..ef747c83f
--- /dev/null
+++ b/devtools/shared/tests/unit/test_stack.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test stack.js.
+
+function run_test() {
+ let loader = new DevToolsLoader();
+ let require = loader.require;
+
+ const {StackFrameCache} = require("devtools/server/actors/utils/stack");
+
+ let cache = new StackFrameCache();
+ cache.initFrames();
+ let baseFrame = {
+ line: 23,
+ column: 77,
+ source: "nowhere",
+ functionDisplayName: "nobody",
+ parent: null,
+ asyncParent: null,
+ asyncCause: null
+ };
+ cache.addFrame(baseFrame);
+
+ let event = cache.makeEvent();
+ do_check_eq(event[0], null);
+ do_check_eq(event[1].functionDisplayName, "nobody");
+ do_check_eq(event.length, 2);
+
+ cache.addFrame({
+ line: 24,
+ column: 78,
+ source: "nowhere",
+ functionDisplayName: "still nobody",
+ parent: null,
+ asyncParent: baseFrame,
+ asyncCause: "async"
+ });
+
+ event = cache.makeEvent();
+ do_check_eq(event[0].functionDisplayName, "still nobody");
+ do_check_eq(event[0].parent, 0);
+ do_check_eq(event[0].asyncParent, 1);
+ do_check_eq(event.length, 1);
+}
diff --git a/devtools/shared/tests/unit/xpcshell.ini b/devtools/shared/tests/unit/xpcshell.ini
new file mode 100644
index 000000000..d0323c813
--- /dev/null
+++ b/devtools/shared/tests/unit/xpcshell.ini
@@ -0,0 +1,40 @@
+[DEFAULT]
+tags = devtools
+head = head_devtools.js
+tail =
+firefox-appdir = browser
+skip-if = toolkit == 'android'
+support-files =
+ exposeLoader.js
+
+[test_assert.js]
+[test_csslexer.js]
+[test_css-properties-db.js]
+# This test only enforces that the CSS database is up to date with nightly. The DB is
+# only used when inspecting a target that doesn't support the getCSSDatabase actor.
+# CSS properties are behind compile-time flags, and there is no automatic rebuild
+# process for uplifts, so this test breaks on uplift.
+run-if = nightly_build
+[test_fetch-bom.js]
+[test_fetch-chrome.js]
+[test_fetch-file.js]
+[test_fetch-http.js]
+[test_fetch-resource.js]
+[test_flatten.js]
+[test_indentation.js]
+[test_independent_loaders.js]
+[test_invisible_loader.js]
+[test_isSet.js]
+[test_safeErrorString.js]
+[test_defineLazyPrototypeGetter.js]
+[test_async-utils.js]
+[test_console_filtering.js]
+[test_pluralForm-english.js]
+[test_pluralForm-makeGetter.js]
+[test_prettifyCSS.js]
+[test_require_lazy.js]
+[test_require_raw.js]
+[test_require.js]
+[test_stack.js]
+[test_defer.js]
+[test_executeSoon.js]