summaryrefslogtreecommitdiffstats
path: root/devtools/shared/tests
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/shared/tests')
-rw-r--r--devtools/shared/tests/browser/browser.ini10
-rw-r--r--devtools/shared/tests/browser/browser_async_storage.js76
-rw-r--r--devtools/shared/tests/browser/browser_l10n_localizeMarkup.js94
-rw-r--r--devtools/shared/tests/chrome/chrome.ini8
-rw-r--r--devtools/shared/tests/chrome/test_css-logic-findCssSelector.html115
-rw-r--r--devtools/shared/tests/chrome/test_css-logic-getCssPath.html106
-rw-r--r--devtools/shared/tests/chrome/test_css-logic-getXPath.html95
-rw-r--r--devtools/shared/tests/xpcshell/.eslintrc.js6
-rw-r--r--devtools/shared/tests/xpcshell/exposeLoader.js10
-rw-r--r--devtools/shared/tests/xpcshell/head_devtools.js66
-rw-r--r--devtools/shared/tests/xpcshell/test_assert.js42
-rw-r--r--devtools/shared/tests/xpcshell/test_console_filtering.js156
-rw-r--r--devtools/shared/tests/xpcshell/test_css-properties-db.js180
-rw-r--r--devtools/shared/tests/xpcshell/test_csslexer.js203
-rw-r--r--devtools/shared/tests/xpcshell/test_debugger_client.js69
-rw-r--r--devtools/shared/tests/xpcshell/test_defer.js32
-rw-r--r--devtools/shared/tests/xpcshell/test_defineLazyPrototypeGetter.js68
-rw-r--r--devtools/shared/tests/xpcshell/test_eventemitter_abort_controller.js181
-rw-r--r--devtools/shared/tests/xpcshell/test_eventemitter_basic.js345
-rw-r--r--devtools/shared/tests/xpcshell/test_eventemitter_destroy.js32
-rw-r--r--devtools/shared/tests/xpcshell/test_eventemitter_static.js378
-rw-r--r--devtools/shared/tests/xpcshell/test_executeSoon.js35
-rw-r--r--devtools/shared/tests/xpcshell/test_fetch-bom.js80
-rw-r--r--devtools/shared/tests/xpcshell/test_fetch-chrome.js36
-rw-r--r--devtools/shared/tests/xpcshell/test_fetch-file.js113
-rw-r--r--devtools/shared/tests/xpcshell/test_fetch-http.js67
-rw-r--r--devtools/shared/tests/xpcshell/test_fetch-resource.js43
-rw-r--r--devtools/shared/tests/xpcshell/test_flatten.js27
-rw-r--r--devtools/shared/tests/xpcshell/test_indentation.js150
-rw-r--r--devtools/shared/tests/xpcshell/test_independent_loaders.js22
-rw-r--r--devtools/shared/tests/xpcshell/test_invisible_loader.js80
-rw-r--r--devtools/shared/tests/xpcshell/test_isSet.js35
-rw-r--r--devtools/shared/tests/xpcshell/test_loader.js72
-rw-r--r--devtools/shared/tests/xpcshell/test_natural-sort.js911
-rw-r--r--devtools/shared/tests/xpcshell/test_pluralForm-english.js32
-rw-r--r--devtools/shared/tests/xpcshell/test_pluralForm-makeGetter.js38
-rw-r--r--devtools/shared/tests/xpcshell/test_prettifyCSS.js172
-rw-r--r--devtools/shared/tests/xpcshell/test_require.js100
-rw-r--r--devtools/shared/tests/xpcshell/test_require_lazy.js38
-rw-r--r--devtools/shared/tests/xpcshell/test_require_raw.js26
-rw-r--r--devtools/shared/tests/xpcshell/test_safeErrorString.js59
-rw-r--r--devtools/shared/tests/xpcshell/test_sprintfjs.js120
-rw-r--r--devtools/shared/tests/xpcshell/test_stack.js49
-rw-r--r--devtools/shared/tests/xpcshell/throwing-module-1.js7
-rw-r--r--devtools/shared/tests/xpcshell/throwing-module-2.js8
-rw-r--r--devtools/shared/tests/xpcshell/xpcshell.ini48
46 files changed, 4640 insertions, 0 deletions
diff --git a/devtools/shared/tests/browser/browser.ini b/devtools/shared/tests/browser/browser.ini
new file mode 100644
index 0000000000..1536980a52
--- /dev/null
+++ b/devtools/shared/tests/browser/browser.ini
@@ -0,0 +1,10 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ !/devtools/client/shared/test/shared-head.js
+ !/devtools/client/shared/test/telemetry-test-helpers.js
+ ../../../server/tests/browser/head.js
+
+[browser_async_storage.js]
+[browser_l10n_localizeMarkup.js]
diff --git a/devtools/shared/tests/browser/browser_async_storage.js b/devtools/shared/tests/browser/browser_async_storage.js
new file mode 100644
index 0000000000..87a1ef169f
--- /dev/null
+++ b/devtools/shared/tests/browser/browser_async_storage.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the basic functionality of async-storage.
+// Adapted from https://github.com/mozilla-b2g/gaia/blob/f09993563fb5fec4393eb71816ce76cb00463190/apps/sharedtest/test/unit/async_storage_test.js.
+
+const asyncStorage = require("resource://devtools/shared/async-storage.js");
+add_task(async function () {
+ is(typeof asyncStorage.length, "function", "API exists.");
+ is(typeof asyncStorage.key, "function", "API exists.");
+ is(typeof asyncStorage.getItem, "function", "API exists.");
+ is(typeof asyncStorage.setItem, "function", "API exists.");
+ is(typeof asyncStorage.removeItem, "function", "API exists.");
+ is(typeof asyncStorage.clear, "function", "API exists.");
+});
+
+add_task(async function () {
+ await asyncStorage.setItem("foo", "bar");
+ let value = await asyncStorage.getItem("foo");
+ is(value, "bar", "value is correct");
+ await asyncStorage.setItem("foo", "overwritten");
+ value = await asyncStorage.getItem("foo");
+ is(value, "overwritten", "value is correct");
+ await asyncStorage.removeItem("foo");
+ value = await asyncStorage.getItem("foo");
+ is(value, null, "value is correct");
+});
+
+add_task(async function () {
+ const object = {
+ x: 1,
+ y: "foo",
+ z: true,
+ };
+
+ await asyncStorage.setItem("myobj", object);
+ let value = await asyncStorage.getItem("myobj");
+ is(object.x, value.x, "value is correct");
+ is(object.y, value.y, "value is correct");
+ is(object.z, value.z, "value is correct");
+ await asyncStorage.removeItem("myobj");
+ value = await asyncStorage.getItem("myobj");
+ is(value, null, "value is correct");
+});
+
+add_task(async function () {
+ await asyncStorage.clear();
+ let len = await asyncStorage.length();
+ is(len, 0, "length is correct");
+ await asyncStorage.setItem("key1", "value1");
+ len = await asyncStorage.length();
+ is(len, 1, "length is correct");
+ await asyncStorage.setItem("key2", "value2");
+ len = await asyncStorage.length();
+ is(len, 2, "length is correct");
+ await asyncStorage.setItem("key3", "value3");
+ len = await asyncStorage.length();
+ is(len, 3, "length is correct");
+
+ let key = await asyncStorage.key(0);
+ is(key, "key1", "key is correct");
+ key = await asyncStorage.key(1);
+ is(key, "key2", "key is correct");
+ key = await asyncStorage.key(2);
+ is(key, "key3", "key is correct");
+ key = await asyncStorage.key(3);
+ is(key, null, "key is correct");
+ await asyncStorage.clear();
+ key = await asyncStorage.key(0);
+ is(key, null, "key is correct");
+
+ len = await asyncStorage.length();
+ is(len, 0, "length is correct");
+});
diff --git a/devtools/shared/tests/browser/browser_l10n_localizeMarkup.js b/devtools/shared/tests/browser/browser_l10n_localizeMarkup.js
new file mode 100644
index 0000000000..9c4118b572
--- /dev/null
+++ b/devtools/shared/tests/browser/browser_l10n_localizeMarkup.js
@@ -0,0 +1,94 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the markup localization works properly.
+
+const {
+ localizeMarkup,
+ LocalizationHelper,
+} = require("resource://devtools/shared/l10n.js");
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+
+add_task(async function () {
+ info("Check that the strings used for this test are still valid");
+ const STARTUP_L10N = new LocalizationHelper(
+ "devtools/client/locales/startup.properties"
+ );
+ const TOOLBOX_L10N = new LocalizationHelper(
+ "devtools/client/locales/toolbox.properties"
+ );
+ const str1 = STARTUP_L10N.getStr("inspector.label");
+ const str2 = STARTUP_L10N.getStr("inspector.accesskey");
+ const str3 = TOOLBOX_L10N.getStr("toolbox.defaultTitle");
+ ok(
+ str1 && str2 && str3,
+ "If this failed, strings should be updated in the test"
+ );
+
+ info("Create the test markup");
+ const div = document.createElementNS(HTML_NS, "div");
+ div.setAttribute(
+ "data-localization-bundle",
+ "devtools/client/locales/startup.properties"
+ );
+ const div0 = document.createElementNS(HTML_NS, "div");
+ div0.setAttribute("id", "d0");
+ div0.setAttribute("data-localization", "content=inspector.someInvalidKey");
+ div.appendChild(div0);
+ const div1 = document.createElementNS(HTML_NS, "div");
+ div1.setAttribute("id", "d1");
+ div1.setAttribute("data-localization", "content=inspector.label");
+ div.appendChild(div1);
+ div1.append("Text will disappear");
+ const div2 = document.createElementNS(HTML_NS, "div");
+ div2.setAttribute("id", "d2");
+ div2.setAttribute(
+ "data-localization",
+ "content=inspector.label;title=inspector.accesskey"
+ );
+ div.appendChild(div2);
+ const div3 = document.createElementNS(HTML_NS, "div");
+ div3.setAttribute("id", "d3");
+ div3.setAttribute(
+ "data-localization",
+ "content=inspector.label;title=inspector.accesskey"
+ );
+ div.appendChild(div3);
+ const div4 = document.createElementNS(HTML_NS, "div");
+ div4.setAttribute("id", "d4");
+ div4.setAttribute("data-localization", "aria-label=inspector.label");
+ div.appendChild(div4);
+ div4.append("Some content");
+ const toolboxDiv = document.createElementNS(HTML_NS, "div");
+ toolboxDiv.setAttribute(
+ "data-localization-bundle",
+ "devtools/client/locales/toolbox.properties"
+ );
+ div.appendChild(toolboxDiv);
+ const div5 = document.createElementNS(HTML_NS, "div");
+ div5.setAttribute("id", "d5");
+ div5.setAttribute("data-localization", "content=toolbox.defaultTitle");
+ toolboxDiv.appendChild(div5);
+
+ info("Use localization helper to localize the test markup");
+ localizeMarkup(div);
+
+ is(div1.innerHTML, str1, "The content of #d1 is localized");
+ is(div2.innerHTML, str1, "The content of #d2 is localized");
+ is(div2.getAttribute("title"), str2, "The title of #d2 is localized");
+ is(div3.innerHTML, str1, "The content of #d3 is localized");
+ is(div3.getAttribute("title"), str2, "The title of #d3 is localized");
+ is(div4.innerHTML, "Some content", "The content of #d4 is not replaced");
+ is(
+ div4.getAttribute("aria-label"),
+ str1,
+ "The aria-label of #d4 is localized"
+ );
+ is(
+ div5.innerHTML,
+ str3,
+ "The content of #d5 is localized with another bundle"
+ );
+});
diff --git a/devtools/shared/tests/chrome/chrome.ini b/devtools/shared/tests/chrome/chrome.ini
new file mode 100644
index 0000000000..026b575d33
--- /dev/null
+++ b/devtools/shared/tests/chrome/chrome.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+tags = devtools
+skip-if = os == 'android'
+
+[test_css-logic-findCssSelector.html]
+[test_css-logic-getCssPath.html]
+[test_css-logic-getXPath.html]
+skip-if = os == 'linux' && debug # Bug 1205739
diff --git a/devtools/shared/tests/chrome/test_css-logic-findCssSelector.html b/devtools/shared/tests/chrome/test_css-logic-findCssSelector.html
new file mode 100644
index 0000000000..b7d364664b
--- /dev/null
+++ b/devtools/shared/tests/chrome/test_css-logic-findCssSelector.html
@@ -0,0 +1,115 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for CSS logic helper </title>
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript">
+"use strict";
+
+const { require } = ChromeUtils.importESModule("resource://devtools/shared/loader/Loader.sys.mjs");
+const { findCssSelector } = require("devtools/shared/inspector/css-logic");
+
+var _tests = [];
+function addTest(test) {
+ _tests.push(test);
+}
+
+function runNextTest() {
+ if (!_tests.length) {
+ SimpleTest.finish();
+ return;
+ }
+ _tests.shift()();
+}
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+ runNextTest();
+};
+
+addTest(function findAllCssSelectors() {
+ const nodes = document.querySelectorAll("*");
+ for (let i = 0; i < nodes.length; i++) {
+ const selector = findCssSelector(nodes[i]);
+ const matches = document.querySelectorAll(selector);
+
+ is(matches.length, 1, "There is a single match: " + selector);
+ is(matches[0], nodes[i], "The selector matches the correct node: " + selector);
+ }
+
+ runNextTest();
+});
+
+addTest(function findCssSelectorNotContainedInDocument() {
+ const unattached = document.createElement("div");
+ unattached.id = "unattached";
+ is(findCssSelector(unattached), "", "Unattached node returns empty string");
+
+ const unattachedChild = document.createElement("div");
+ unattached.appendChild(unattachedChild);
+ is(findCssSelector(unattachedChild), "", "Unattached child returns empty string");
+
+ const unattachedBody = document.createElement("body");
+ is(findCssSelector(unattachedBody), "", "Unattached body returns empty string");
+
+ runNextTest();
+});
+
+addTest(function findCssSelectorBasic() {
+ const data = [
+ "#one",
+ "#" + CSS.escape("2"),
+ ".three",
+ "." + CSS.escape("4"),
+ "#find-css-selector > div:nth-child(5)",
+ "#find-css-selector > p:nth-child(6)",
+ ".seven",
+ ".eight",
+ ".nine",
+ ".ten",
+ "div.sameclass:nth-child(11)",
+ "div.sameclass:nth-child(12)",
+ "div.sameclass:nth-child(13)",
+ "#" + CSS.escape("!, \", #, $, %, &, ', (, ), *, +, ,, -, ., /, :, ;, <, =, >, ?, @, [, \\, ], ^, `, {, |, }, ~"),
+ ];
+
+ const container = document.querySelector("#find-css-selector");
+ is(container.children.length, data.length, "Container has correct number of children.");
+
+ for (let i = 0; i < data.length; i++) {
+ const node = container.children[i];
+ is(findCssSelector(node), data[i], "matched id for index " + (i - 1));
+ }
+
+ runNextTest();
+});
+ </script>
+</head>
+<body>
+ <div id="find-css-selector">
+ <div id="one"></div> <!-- Basic ID -->
+ <div id="2"></div> <!-- Escaped ID -->
+ <div class="three"></div> <!-- Basic Class -->
+ <div class="4"></div> <!-- Escaped Class -->
+ <div attr="5"></div> <!-- Only an attribute -->
+ <p></p> <!-- Nothing unique -->
+ <div class="seven seven"></div> <!-- Two classes with same name -->
+ <div class="eight eight2"></div> <!-- Two classes with different names -->
+
+ <!-- Two elements with the same id - should not use ID -->
+ <div class="nine" id="nine-and-ten"></div>
+ <div class="ten" id="nine-and-ten"></div>
+
+ <!-- Three elements with the same id - should use class and nth-child instead -->
+ <div class="sameclass" id="11-12-13"></div>
+ <div class="sameclass" id="11-12-13"></div>
+ <div class="sameclass" id="11-12-13"></div>
+
+ <!-- Special characters -->
+ <div id="!, &quot;, #, $, %, &amp;, ', (, ), *, +, ,, -, ., /, :, ;, <, =, >, ?, @, [, \, ], ^, `, {, |, }, ~"></div>
+ </div>
+</body>
+</html>
diff --git a/devtools/shared/tests/chrome/test_css-logic-getCssPath.html b/devtools/shared/tests/chrome/test_css-logic-getCssPath.html
new file mode 100644
index 0000000000..333c9e0fdf
--- /dev/null
+++ b/devtools/shared/tests/chrome/test_css-logic-getCssPath.html
@@ -0,0 +1,106 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1323700
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1323700</title>
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript">
+"use strict";
+
+const { require } = ChromeUtils.importESModule("resource://devtools/shared/loader/Loader.sys.mjs");
+const CssLogic = require("devtools/shared/inspector/css-logic");
+
+var _tests = [];
+function addTest(test) {
+ _tests.push(test);
+}
+
+function runNextTest() {
+ if (!_tests.length) {
+ SimpleTest.finish()
+ return;
+ }
+ _tests.shift()();
+}
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+ runNextTest();
+}
+
+addTest(function getCssPathForUnattachedElement() {
+ const unattached = document.createElement("div");
+ unattached.id = "unattached";
+ is(CssLogic.getCssPath(unattached), "", "Unattached node returns empty string");
+
+ const unattachedChild = document.createElement("div");
+ unattached.appendChild(unattachedChild);
+ is(CssLogic.getCssPath(unattachedChild), "", "Unattached child returns empty string");
+
+ const unattachedBody = document.createElement("body");
+ is(CssLogic.getCssPath(unattachedBody), "", "Unattached body returns empty string");
+
+ runNextTest();
+});
+
+addTest(function cssPathHasOneStepForEachAncestor() {
+ for (const el of [...document.querySelectorAll('*')]) {
+ const splitPath = CssLogic.getCssPath(el).split(" ");
+
+ let expectedNbOfParts = 0;
+ let parent = el.parentNode;
+ while (parent) {
+ expectedNbOfParts ++;
+ parent = parent.parentNode;
+ }
+
+ is(splitPath.length, expectedNbOfParts, "There are enough parts in the full path");
+ }
+
+ runNextTest();
+});
+
+addTest(function getCssPath() {
+ const data = [{
+ selector: "#id",
+ path: "html body div div div.class div#id"
+ }, {
+ selector: "html",
+ path: "html"
+ }, {
+ selector: "body",
+ path: "html body"
+ }, {
+ selector: ".c1.c2.c3",
+ path: "html body span.c1.c2.c3"
+ }, {
+ selector: "#i",
+ path: "html body span#i.c1.c2"
+ }];
+
+ for (const {selector, path} of data) {
+ const node = document.querySelector(selector);
+ is (CssLogic.getCssPath(node), path, `Full css path is correct for ${selector}`);
+ }
+
+ runNextTest();
+});
+ </script>
+</head>
+<body>
+ <div>
+ <div>
+ <div class="class">
+ <div id="id"></div>
+ </div>
+ </div>
+ </div>
+ <span class="c1 c2 c3"></span>
+ <span id="i" class="c1 c2"></span>
+</body>
+</html>
diff --git a/devtools/shared/tests/chrome/test_css-logic-getXPath.html b/devtools/shared/tests/chrome/test_css-logic-getXPath.html
new file mode 100644
index 0000000000..469e188cf0
--- /dev/null
+++ b/devtools/shared/tests/chrome/test_css-logic-getXPath.html
@@ -0,0 +1,95 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=987877
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 987877</title>
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript">
+"use strict";
+
+const { require } = ChromeUtils.importESModule("resource://devtools/shared/loader/Loader.sys.mjs");
+const CssLogic = require("devtools/shared/inspector/css-logic");
+
+const _tests = [];
+function addTest(test) {
+ _tests.push(test);
+}
+
+function runNextTest() {
+ if (!_tests.length) {
+ SimpleTest.finish();
+ return;
+ }
+ _tests.shift()();
+}
+
+window.onload = function () {
+ SimpleTest.waitForExplicitFinish();
+ runNextTest();
+};
+
+addTest(function getXPathForUnattachedElement() {
+ const unattached = document.createElement("div");
+ unattached.id = "unattached";
+ is(CssLogic.getXPath(unattached), "", "Unattached node returns empty string");
+
+ const unattachedChild = document.createElement("div");
+ unattached.appendChild(unattachedChild);
+ is(CssLogic.getXPath(unattachedChild), "", "Unattached child returns empty string");
+
+ const unattachedBody = document.createElement("body");
+ is(CssLogic.getXPath(unattachedBody), "", "Unattached body returns empty string");
+
+ runNextTest();
+});
+
+addTest(function getXPath() {
+ const data = [{
+ // Target elements that have an ID get a short XPath.
+ selector: "#i-have-an-id",
+ path: "//*[@id=\"i-have-an-id\"]"
+ }, {
+ selector: "html",
+ path: "/html"
+ }, {
+ selector: "body",
+ path: "/html/body"
+ }, {
+ selector: "body > div:nth-child(2) > div > div:nth-child(4)",
+ path: "/html/body/div[2]/div/div[4]"
+ }, {
+ // XPath should support namespace.
+ selector: "namespace\\:body",
+ path: "/html/body/namespace:test/namespace:body"
+ }];
+
+ for (const {selector, path} of data) {
+ const node = document.querySelector(selector);
+ is(CssLogic.getXPath(node), path, `Full css path is correct for ${selector}`);
+ }
+
+ runNextTest();
+});
+ </script>
+</head>
+<body>
+ <div id="i-have-an-id">find me</div>
+ <div>
+ <div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div>me too!</div>
+ </div>
+ </div>
+ <namespace:test>
+ <namespace:header></namespace:header>
+ <namespace:body>and me</namespace:body>
+ </namespace:test>
+</body>
+</html>
diff --git a/devtools/shared/tests/xpcshell/.eslintrc.js b/devtools/shared/tests/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..cc1ed286cc
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/.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/xpcshell/exposeLoader.js b/devtools/shared/tests/xpcshell/exposeLoader.js
new file mode 100644
index 0000000000..7c8acdd759
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/exposeLoader.js
@@ -0,0 +1,10 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+exports.exerciseLazyRequire = (name, path) => {
+ const o = {};
+ loader.lazyRequireGetter(o, name, path);
+ return o;
+};
diff --git a/devtools/shared/tests/xpcshell/head_devtools.js b/devtools/shared/tests/xpcshell/head_devtools.js
new file mode 100644
index 0000000000..cee2218a2a
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/head_devtools.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* exported DevToolsUtils, DevToolsLoader */
+
+"use strict";
+
+const { require, DevToolsLoader } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+);
+const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
+
+Services.prefs.setBoolPref("devtools.testing", true);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.testing");
+});
+
+// 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;
+
+// XXX This listener is broken, see bug 1456634, for now turn off no-undef here,
+// this needs turning back on!
+/* eslint-disable no-undef */
+var listener = {
+ observe(message) {
+ let string;
+ try {
+ message.QueryInterface(Ci.nsIScriptError);
+ dump(
+ message.sourceName +
+ ":" +
+ message.lineNumber +
+ ": " +
+ scriptErrorFlagsToKind(message.flags) +
+ ": " +
+ message.errorMessage +
+ "\n"
+ );
+ string = message.errorMessage;
+ } catch (ex) {
+ // Be a little paranoid with message, as the whole goal here is to lose
+ // no information.
+ try {
+ string = "" + message.message;
+ } catch (e) {
+ string = "<error converting error message to string>";
+ }
+ }
+
+ // Make sure we exit all nested event loops so that the test can finish.
+ while (DevToolsServer.xpcInspector.eventLoopNestLevel > 0) {
+ DevToolsServer.xpcInspector.exitNestedEventLoop();
+ }
+
+ if (!ALLOW_CONSOLE_ERRORS) {
+ do_throw("head_devtools.js got console message: " + string + "\n");
+ }
+ },
+};
+/* eslint-enable no-undef */
+
+Services.console.registerListener(listener);
diff --git a/devtools/shared/tests/xpcshell/test_assert.js b/devtools/shared/tests/xpcshell/test_assert.js
new file mode 100644
index 0000000000..45ae9eb1a2
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_assert.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test DevToolsUtils.assert
+
+ALLOW_CONSOLE_ERRORS = true;
+
+function run_test() {
+ 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/xpcshell/test_console_filtering.js b/devtools/shared/tests/xpcshell/test_console_filtering.js
new file mode 100644
index 0000000000..74d4795895
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_console_filtering.js
@@ -0,0 +1,156 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { console, ConsoleAPI } = ChromeUtils.importESModule(
+ "resource://gre/modules/Console.sys.mjs"
+);
+const {
+ ConsoleAPIListener,
+} = require("resource://devtools/server/actors/webconsole/listeners/console-api.js");
+
+var seenMessages = 0;
+var seenTypes = 0;
+
+var onConsoleAPICall = function (message) {
+ if (message.consoleID && message.consoleID == "addon/foo") {
+ Assert.equal(message.level, "warn");
+ Assert.equal(message.arguments[0], "Warning from foo");
+ seenTypes |= 1;
+ } else if (message.addonId == "bar") {
+ Assert.equal(message.level, "error");
+ Assert.equal(message.arguments[0], "Error from bar");
+ seenTypes |= 2;
+ } else {
+ Assert.equal(message.level, "log");
+ Assert.equal(message.arguments[0], "Hello from default console");
+ seenTypes |= 4;
+ }
+ seenMessages++;
+};
+
+let policy;
+registerCleanupFunction(() => {
+ policy.active = false;
+});
+
+function createFakeAddonWindow({ addonId } = {}) {
+ const uuidGen = Services.uuid;
+ const uuid = uuidGen.generateUUID().number.slice(1, -1);
+
+ if (policy) {
+ policy.active = false;
+ }
+ /* globals MatchPatternSet, WebExtensionPolicy */
+ policy = new WebExtensionPolicy({
+ id: addonId,
+ mozExtensionHostname: uuid,
+ baseURL: "file:///",
+ allowedOrigins: new MatchPatternSet([]),
+ localizeCallback() {},
+ });
+ policy.active = true;
+
+ const baseURI = Services.io.newURI(`moz-extension://${uuid}/`);
+ const principal = Services.scriptSecurityManager.createContentPrincipal(
+ baseURI,
+ {}
+ );
+ const chromeWebNav = Services.appShell.createWindowlessBrowser(true);
+ const { docShell } = chromeWebNav;
+ docShell.createAboutBlankContentViewer(principal, principal);
+ const 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.sys.mjs messages tagged by the Addon SDK
+ // are still filtered correctly.
+ const console1 = new ConsoleAPI({
+ consoleID: "addon/foo",
+ });
+
+ // console2 - WebExtension page's console messages tagged
+ // by 'originAttributes.addonId' are filtered correctly.
+ const { addonWindow, chromeWebNav } = createFakeAddonWindow({
+ addonId: "bar",
+ });
+ const 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, onConsoleAPICall);
+ listener.init();
+ let messages = listener.getCachedMessages();
+
+ seenTypes = 0;
+ seenMessages = 0;
+ messages.forEach(onConsoleAPICall);
+ Assert.equal(seenMessages, 3);
+ Assert.equal(seenTypes, 7);
+
+ seenTypes = 0;
+ seenMessages = 0;
+ console.log("Hello from default console");
+ console1.warn("Warning from foo");
+ console2.error("Error from bar");
+ Assert.equal(seenMessages, 3);
+ Assert.equal(seenTypes, 7);
+
+ listener.destroy();
+
+ listener = new ConsoleAPIListener(null, onConsoleAPICall, { addonId: "foo" });
+ listener.init();
+ messages = listener.getCachedMessages();
+
+ seenTypes = 0;
+ seenMessages = 0;
+ messages.forEach(onConsoleAPICall);
+ Assert.equal(seenMessages, 2);
+ Assert.equal(seenTypes, 1);
+
+ seenTypes = 0;
+ seenMessages = 0;
+ console.log("Hello from default console");
+ console1.warn("Warning from foo");
+ console2.error("Error from bar");
+ Assert.equal(seenMessages, 1);
+ Assert.equal(seenTypes, 1);
+
+ listener.destroy();
+
+ listener = new ConsoleAPIListener(null, onConsoleAPICall, { addonId: "bar" });
+ listener.init();
+ messages = listener.getCachedMessages();
+
+ seenTypes = 0;
+ seenMessages = 0;
+ messages.forEach(onConsoleAPICall);
+ Assert.equal(seenMessages, 3);
+ Assert.equal(seenTypes, 2);
+
+ seenTypes = 0;
+ seenMessages = 0;
+ console.log("Hello from default console");
+ console1.warn("Warning from foo");
+ console2.error("Error from bar");
+
+ Assert.equal(seenMessages, 1);
+ Assert.equal(seenTypes, 2);
+
+ listener.destroy();
+
+ // Close the addon window's chromeWebNav.
+ chromeWebNav.close();
+}
diff --git a/devtools/shared/tests/xpcshell/test_css-properties-db.js b/devtools/shared/tests/xpcshell/test_css-properties-db.js
new file mode 100644
index 0000000000..78ed8fd70b
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_css-properties-db.js
@@ -0,0 +1,180 @@
+/* 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 {
+ PSEUDO_ELEMENTS,
+ CSS_PROPERTIES,
+} = require("resource://devtools/shared/css/generated/properties-db.js");
+const PREFERENCES = InspectorUtils.getCSSPropertyPrefs();
+const {
+ generateCssProperties,
+} = require("resource://devtools/server/actors/css-properties.js");
+const { Preferences } = ChromeUtils.importESModule(
+ "resource://gre/modules/Preferences.sys.mjs"
+);
+
+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,
+ InspectorUtils.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. Sometimes these properties don't
+ * completely agree due to differences in preferences. Check the currently set
+ * preference for that property to see if it's enabled.
+ */
+ const platformProperties = generateCssProperties();
+
+ for (const 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 {
+ ok(
+ false,
+ `The static database and platform do not match for ` +
+ `
+ "${propertyName}". ${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.includes("-moz-osx-"));
+
+ if (mismatches.length === 0) {
+ ok(
+ true,
+ "No client and platform CSS property database mismatches were found."
+ );
+ }
+
+ mismatches.forEach(propertyName => {
+ if (getPreference(propertyName) === false) {
+ ok(
+ true,
+ `The static database and platform do not agree on the property ` +
+ `"${propertyName}" This is ok because it is currently disabled through ` +
+ `a preference.`
+ );
+ } else {
+ 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 (const 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);
+}
+
+/**
+ * Get the preference value of whether this property is enabled. Returns an empty string
+ * if no preference exists.
+ *
+ * @param {String} propertyName
+ * @return {Boolean|undefined}
+ */
+function getPreference(propertyName) {
+ const preference = PREFERENCES.find(({ name, pref }) => {
+ return name === propertyName && !!pref;
+ });
+
+ if (preference) {
+ return Preferences.get(preference.pref);
+ }
+ return undefined;
+}
diff --git a/devtools/shared/tests/xpcshell/test_csslexer.js b/devtools/shared/tests/xpcshell/test_csslexer.js
new file mode 100644
index 0000000000..7200fcc419
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_csslexer.js
@@ -0,0 +1,203 @@
+/* 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 jsLexer = require("resource://devtools/shared/css/lexer.js");
+
+function test_lexer(cssText, tokenTypes) {
+ const lexer = jsLexer.getCSSLexer(cssText);
+ let reconstructed = "";
+ let lastTokenEnd = 0;
+ let i = 0;
+ while (true) {
+ const 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"]],
+ ["url(http://example.com @", ["bad_url:http://example.com"]],
+ ["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) {
+ const lexer = jsLexer.getCSSLexer(cssText);
+ let i = 0;
+ while (true) {
+ const token = lexer.nextToken();
+ const startLine = lexer.lineNumber;
+ const 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
+) {
+ const lexer = jsLexer.getCSSLexer(cssText);
+ while (lexer.nextToken()) {
+ // Nothing.
+ }
+
+ info("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/xpcshell/test_debugger_client.js b/devtools/shared/tests/xpcshell/test_debugger_client.js
new file mode 100644
index 0000000000..d908ddfb27
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_debugger_client.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// DevToolsClient tests
+
+const {
+ DevToolsServer,
+} = require("resource://devtools/server/devtools-server.js");
+const {
+ DevToolsClient,
+} = require("resource://devtools/client/devtools-client.js");
+
+add_task(async function () {
+ await testCloseLoops();
+ await fakeTransportShutdown();
+});
+
+function createClient() {
+ DevToolsServer.init();
+ DevToolsServer.registerAllActors();
+ const client = new DevToolsClient(DevToolsServer.connectPipe());
+ return client;
+}
+
+// Ensure that closing the client while it is closing doesn't loop
+async function testCloseLoops() {
+ const client = createClient();
+ await client.connect();
+
+ await new Promise(resolve => {
+ let called = false;
+ client.on("closed", async () => {
+ dump(">> CLOSED\n");
+ if (called) {
+ ok(
+ false,
+ "Calling client.close from closed event listener introduce loops"
+ );
+ return;
+ }
+ called = true;
+ await client.close();
+ resolve();
+ });
+ client.close();
+ });
+}
+
+// Check that, if we fake a transport shutdown (like if a device is unplugged)
+// the client is automatically closed, and we can still call client.close.
+async function fakeTransportShutdown() {
+ const client = createClient();
+ await client.connect();
+
+ await new Promise(resolve => {
+ const onClosed = async function () {
+ client.off("closed", onClosed);
+ ok(true, "Client emitted 'closed' event");
+ resolve();
+ };
+ client.on("closed", onClosed);
+ client.transport.close();
+ });
+
+ await client.close();
+ ok(true, "client.close() successfully resolves");
+}
diff --git a/devtools/shared/tests/xpcshell/test_defer.js b/devtools/shared/tests/xpcshell/test_defer.js
new file mode 100644
index 0000000000..3ac0f28bd7
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/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("resource://devtools/shared/defer.js");
+
+function testResolve() {
+ const deferred = defer();
+ deferred.resolve("success");
+ return deferred.promise;
+}
+
+function testReject() {
+ const deferred = defer();
+ deferred.reject("error");
+ return deferred.promise;
+}
+
+add_task(async function () {
+ const success = await testResolve();
+ equal(success, "success");
+
+ let error;
+ try {
+ await testReject();
+ } catch (e) {
+ error = e;
+ }
+
+ equal(error, "error");
+});
diff --git a/devtools/shared/tests/xpcshell/test_defineLazyPrototypeGetter.js b/devtools/shared/tests/xpcshell/test_defineLazyPrototypeGetter.js
new file mode 100644
index 0000000000..2f53b377de
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_defineLazyPrototypeGetter.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// 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.
+ const descriptor = Object.getOwnPropertyDescriptor(Class.prototype, "foo");
+ Assert.equal(typeof descriptor.get, "function");
+ Assert.equal(descriptor.set, undefined);
+ Assert.equal(descriptor.enumerable, false);
+ Assert.equal(descriptor.configurable, true);
+}
+
+function test_instance_attributes() {
+ // Instances should not have an own property until the lazy getter has been
+ // activated.
+ const instance = new Class();
+ Assert.ok(!instance.hasOwnProperty("foo"));
+ instance.foo;
+ Assert.ok(instance.hasOwnProperty("foo"));
+
+ // Check that the instance has an own property with the expecred value and
+ // attributes after the lazy getter is activated.
+ const descriptor = Object.getOwnPropertyDescriptor(instance, "foo");
+ Assert.ok(descriptor.value instanceof Array);
+ Assert.equal(descriptor.writable, true);
+ Assert.equal(descriptor.enumerable, false);
+ Assert.equal(descriptor.configurable, true);
+}
+
+function test_multiple_instances() {
+ const instance1 = new Class();
+ const instance2 = new Class();
+ const foo1 = instance1.foo;
+ const foo2 = instance2.foo;
+ // Check that the lazy getter returns the expected type of value.
+ Assert.ok(foo1 instanceof Array);
+ Assert.ok(foo2 instanceof Array);
+ // Make sure the lazy getter runs once and only once per instance.
+ Assert.equal(instance1.foo, foo1);
+ Assert.equal(instance2.foo, foo2);
+ // Make sure each instance gets its own unique value.
+ Assert.notEqual(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.
+ const instance = new Foo();
+ Assert.equal(instance.foo, instance);
+}
diff --git a/devtools/shared/tests/xpcshell/test_eventemitter_abort_controller.js b/devtools/shared/tests/xpcshell/test_eventemitter_abort_controller.js
new file mode 100644
index 0000000000..9a35ce1f98
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_eventemitter_abort_controller.js
@@ -0,0 +1,181 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const EventEmitter = require("resource://devtools/shared/event-emitter.js");
+
+add_task(function testAbortSingleListener() {
+ // Test a simple case with AbortController
+ info("Create an EventEmitter");
+ const emitter = new EventEmitter();
+ const abortController = new AbortController();
+ const { signal } = abortController;
+
+ info("Setup an event listener on test-event, controlled by an AbortSignal");
+ let eventsReceived = 0;
+ emitter.on("test-event", () => eventsReceived++, { signal });
+
+ info("Emit test-event");
+ emitter.emit("test-event");
+ equal(eventsReceived, 1, "We received one event, as expected");
+
+ info("Abort the AbortController…");
+ abortController.abort();
+ info("… and emit test-event again");
+ emitter.emit("test-event");
+ equal(eventsReceived, 1, "We didn't receive new event after aborting");
+});
+
+add_task(function testAbortSingleListenerOnce() {
+ // Test a simple case with AbortController and once
+ info("Create an EventEmitter");
+ const emitter = new EventEmitter();
+ const abortController = new AbortController();
+ const { signal } = abortController;
+
+ info("Setup an event listener on test-event, controlled by an AbortSignal");
+ let eventReceived = false;
+ emitter.once(
+ "test-event",
+ () => {
+ eventReceived = true;
+ },
+ { signal }
+ );
+
+ info("Abort the AbortController…");
+ abortController.abort();
+ info("… and emit test-event");
+ emitter.emit("test-event");
+ equal(eventReceived, false, "We didn't receive the event after aborting");
+});
+
+add_task(function testAbortMultipleListener() {
+ // Test aborting multiple event listeners with one call to abort
+ info("Create an EventEmitter");
+ const emitter = new EventEmitter();
+ const abortController = new AbortController();
+ const { signal } = abortController;
+
+ info("Setup 3 event listeners controlled by an AbortSignal");
+ let eventsReceived = 0;
+ emitter.on("test-event", () => eventsReceived++, { signal });
+ emitter.on("test-event", () => eventsReceived++, { signal });
+ emitter.on("other-test-event", () => eventsReceived++, { signal });
+
+ info("Emit test-event and other-test-event");
+ emitter.emit("test-event");
+ emitter.emit("other-test-event");
+ equal(eventsReceived, 3, "We received 3 events, as expected");
+
+ info("Abort the AbortController…");
+ abortController.abort();
+ info("… and emit events again");
+ emitter.emit("test-event");
+ emitter.emit("other-test-event");
+ equal(eventsReceived, 3, "We didn't receive new event after aborting");
+});
+
+add_task(function testAbortMultipleEmitter() {
+ // Test aborting multiple event listeners on different emitters with one call to abort
+ info("Create 2 EventEmitter");
+ const emitter1 = new EventEmitter();
+ const emitter2 = new EventEmitter();
+ const abortController = new AbortController();
+ const { signal } = abortController;
+
+ info("Setup 2 event listeners on test-event, controlled by an AbortSignal");
+ let eventsReceived = 0;
+ emitter1.on("test-event", () => eventsReceived++, { signal });
+ emitter2.on("other-test-event", () => eventsReceived++, { signal });
+
+ info("Emit test-event and other-test-event");
+ emitter1.emit("test-event");
+ emitter2.emit("other-test-event");
+ equal(eventsReceived, 2, "We received 2 events, as expected");
+
+ info("Abort the AbortController…");
+ abortController.abort();
+ info("… and emit events again");
+ emitter1.emit("test-event");
+ emitter2.emit("other-test-event");
+ equal(eventsReceived, 2, "We didn't receive new event after aborting");
+});
+
+add_task(function testAbortBeforeEmitting() {
+ // Check that aborting before emitting does unregister the event listener
+ info("Create an EventEmitter");
+ const emitter = new EventEmitter();
+ const abortController = new AbortController();
+ const { signal } = abortController;
+
+ info("Setup an event listener on test-event, controlled by an AbortSignal");
+ let eventsReceived = 0;
+ emitter.on("test-event", () => eventsReceived++, { signal });
+
+ info("Abort the AbortController…");
+ abortController.abort();
+
+ info("… and emit test-event");
+ emitter.emit("test-event");
+ equal(eventsReceived, 0, "We didn't receive any event");
+});
+
+add_task(function testAbortBeforeSettingListener() {
+ // Check that aborting before creating the event listener won't register it
+ info("Create an EventEmitter");
+ const emitter = new EventEmitter();
+
+ info("Create an AbortController and abort it immediately");
+ const abortController = new AbortController();
+ const { signal } = abortController;
+ abortController.abort();
+
+ info(
+ "Setup an event listener on test-event, controlled by the aborted AbortSignal"
+ );
+ let eventsReceived = 0;
+ const off = emitter.on("test-event", () => eventsReceived++, { signal });
+
+ info("Emit test-event");
+ emitter.emit("test-event");
+ equal(eventsReceived, 0, "We didn't receive any event");
+
+ equal(typeof off, "function", "emitter.on still returned a function");
+ // check that calling off does not throw
+ off();
+});
+
+add_task(function testAbortAfterEventListenerIsRemoved() {
+ // Check that aborting after there's no more event listener does not throw
+ info("Create an EventEmitter");
+ const emitter = new EventEmitter();
+
+ const abortController = new AbortController();
+ const { signal } = abortController;
+
+ info(
+ "Setup an event listener on test-event, controlled by the aborted AbortSignal"
+ );
+ let eventsReceived = 0;
+ const off = emitter.on("test-event", () => eventsReceived++, { signal });
+
+ info("Emit test-event");
+ emitter.emit("test-event");
+ equal(eventsReceived, 1, "We received the expected event");
+
+ info("Remove the event listener with the function returned by `on`");
+ off();
+
+ info("Emit test-event a second time");
+ emitter.emit("test-event");
+ equal(
+ eventsReceived,
+ 1,
+ "We didn't receive new event after removing the event listener"
+ );
+
+ info("Abort to check it doesn't throw");
+ abortController.abort();
+});
diff --git a/devtools/shared/tests/xpcshell/test_eventemitter_basic.js b/devtools/shared/tests/xpcshell/test_eventemitter_basic.js
new file mode 100644
index 0000000000..0592598fdf
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_eventemitter_basic.js
@@ -0,0 +1,345 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {
+ ConsoleAPIListener,
+} = require("resource://devtools/server/actors/webconsole/listeners/console-api.js");
+const EventEmitter = require("resource://devtools/shared/event-emitter.js");
+const hasMethod = (target, method) =>
+ method in target && typeof target[method] === "function";
+
+/**
+ * Each method of this object is a test; tests can be synchronous or asynchronous:
+ *
+ * 1. Plain functions are synchronous tests.
+ * 2. methods with `async` keyword are asynchronous tests.
+ * 3. methods with `done` as argument are asynchronous tests (`done` needs to be called to
+ * finish the test).
+ */
+const TESTS = {
+ testEventEmitterCreation() {
+ const emitter = getEventEmitter();
+ const isAnEmitter = emitter instanceof EventEmitter;
+
+ ok(emitter, "We have an event emitter");
+ ok(
+ hasMethod(emitter, "on") &&
+ hasMethod(emitter, "off") &&
+ hasMethod(emitter, "once") &&
+ hasMethod(emitter, "count") &&
+ !hasMethod(emitter, "decorate"),
+ `Event Emitter ${
+ isAnEmitter ? "instance" : "mixin"
+ } has the expected methods.`
+ );
+ },
+
+ testEmittingEvents(done) {
+ const emitter = getEventEmitter();
+
+ let beenHere1 = false;
+ let beenHere2 = false;
+
+ function next(str1, str2) {
+ equal(str1, "abc", "Argument 1 is correct");
+ equal(str2, "def", "Argument 2 is correct");
+
+ ok(!beenHere1, "first time in next callback");
+ beenHere1 = true;
+
+ emitter.off("next", next);
+
+ emitter.emit("next");
+
+ emitter.once("onlyonce", onlyOnce);
+
+ emitter.emit("onlyonce");
+ emitter.emit("onlyonce");
+ }
+
+ function onlyOnce() {
+ ok(!beenHere2, '"once" listener has been called once');
+ beenHere2 = true;
+ emitter.emit("onlyonce");
+
+ done();
+ }
+
+ emitter.on("next", next);
+ emitter.emit("next", "abc", "def");
+ },
+
+ testThrowingExceptionInListener(done) {
+ const emitter = getEventEmitter();
+ const listener = new ConsoleAPIListener(null, message => {
+ equal(message.level, "error");
+ const [arg] = message.arguments;
+ equal(arg.message, "foo");
+ equal(arg.stack, "bar");
+ listener.destroy();
+ done();
+ });
+
+ listener.init();
+
+ function throwListener() {
+ emitter.off("throw-exception");
+ const err = new Error("foo");
+ err.stack = "bar";
+ throw err;
+ }
+
+ emitter.on("throw-exception", throwListener);
+ emitter.emit("throw-exception");
+ },
+
+ testKillItWhileEmitting(done) {
+ const emitter = getEventEmitter();
+
+ const c1 = () => ok(true, "c1 called");
+ const c2 = () => {
+ ok(true, "c2 called");
+ emitter.off("tick", c3);
+ };
+ const c3 = () => ok(false, "c3 should not be called");
+ const c4 = () => {
+ ok(true, "c4 called");
+ done();
+ };
+
+ emitter.on("tick", c1);
+ emitter.on("tick", c2);
+ emitter.on("tick", c3);
+ emitter.on("tick", c4);
+
+ emitter.emit("tick");
+ },
+
+ testOffAfterOnce() {
+ const emitter = getEventEmitter();
+
+ let enteredC1 = false;
+ const c1 = () => (enteredC1 = true);
+
+ emitter.once("oao", c1);
+ emitter.off("oao", c1);
+
+ emitter.emit("oao");
+
+ ok(!enteredC1, "c1 should not be called");
+ },
+
+ testPromise() {
+ const emitter = getEventEmitter();
+ const p = emitter.once("thing");
+
+ // Check that the promise is only resolved once event though we
+ // emit("thing") more than once
+ let firstCallbackCalled = false;
+ const check1 = p.then(arg => {
+ equal(firstCallbackCalled, false, "first callback called only once");
+ firstCallbackCalled = true;
+ equal(arg, "happened", "correct arg in promise");
+ return "rval from c1";
+ });
+
+ emitter.emit("thing", "happened", "ignored");
+
+ // Check that the promise is resolved asynchronously
+ let secondCallbackCalled = false;
+ const check2 = p.then(arg => {
+ ok(true, "second callback called");
+ equal(arg, "happened", "correct arg in promise");
+ secondCallbackCalled = true;
+ equal(arg, "happened", "correct arg in promise (a second time)");
+ return "rval from c2";
+ });
+
+ // Shouldn't call any of the above listeners
+ emitter.emit("thing", "trashinate");
+
+ // Check that we can still separate events with different names
+ // and that it works with no parameters
+ const pfoo = emitter.once("foo");
+ const pbar = emitter.once("bar");
+
+ const check3 = pfoo.then(arg => {
+ ok(arg === undefined, "no arg for foo event");
+ return "rval from c3";
+ });
+
+ pbar.then(() => {
+ ok(false, "pbar should not be called");
+ });
+
+ emitter.emit("foo");
+
+ equal(secondCallbackCalled, false, "second callback not called yet");
+
+ return Promise.all([check1, check2, check3]).then(args => {
+ equal(args[0], "rval from c1", "callback 1 done good");
+ equal(args[1], "rval from c2", "callback 2 done good");
+ equal(args[2], "rval from c3", "callback 3 done good");
+ });
+ },
+
+ testClearEvents() {
+ const emitter = getEventEmitter();
+
+ const received = [];
+ const listener = (...args) => received.push(args);
+
+ emitter.on("a", listener);
+ emitter.on("b", listener);
+ emitter.on("c", listener);
+
+ emitter.emit("a", 1);
+ emitter.emit("b", 1);
+ emitter.emit("c", 1);
+
+ equal(received.length, 3, "the listener was triggered three times");
+
+ emitter.clearEvents();
+ emitter.emit("a", 1);
+ emitter.emit("b", 1);
+ emitter.emit("c", 1);
+ equal(received.length, 3, "the listener was not called after clearEvents");
+ },
+
+ testOnReturn() {
+ const emitter = getEventEmitter();
+
+ let called = false;
+ const removeOnTest = emitter.on("test", () => {
+ called = true;
+ });
+
+ equal(typeof removeOnTest, "function", "`on` returns a function");
+ removeOnTest();
+
+ emitter.emit("test");
+ equal(called, false, "event listener wasn't called");
+ },
+
+ async testEmitAsync() {
+ const emitter = getEventEmitter();
+
+ let resolve1, resolve2;
+ emitter.once("test", async () => {
+ return new Promise(r => {
+ resolve1 = r;
+ });
+ });
+
+ // Adding a listener which doesn't return a promise should trigger a console warning.
+ emitter.once("test", () => {});
+
+ emitter.once("test", async () => {
+ return new Promise(r => {
+ resolve2 = r;
+ });
+ });
+
+ info("Emit an event and wait for all listener resolutions");
+ const onConsoleWarning = onConsoleWarningLogged(
+ "Listener for event 'test' did not return a promise."
+ );
+ const onEmitted = emitter.emitAsync("test");
+ let resolved = false;
+ onEmitted.then(() => {
+ info("emitAsync just resolved");
+ resolved = true;
+ });
+
+ info("Waiting for warning message about the second listener");
+ await onConsoleWarning;
+
+ // Spin the event loop, to ensure that emitAsync did not resolved too early
+ await new Promise(r => Services.tm.dispatchToMainThread(r));
+
+ ok(resolve1, "event listener has been called");
+ ok(!resolved, "but emitAsync hasn't resolved yet");
+
+ info("Resolve the first listener function");
+ resolve1();
+ ok(!resolved, "emitAsync isn't resolved until all listener resolve");
+
+ info("Resolve the second listener function");
+ resolve2();
+
+ // emitAsync is only resolved in the next event loop
+ await new Promise(r => Services.tm.dispatchToMainThread(r));
+ ok(resolved, "once we resolve all the listeners, emitAsync is resolved");
+ },
+
+ testCount() {
+ const emitter = getEventEmitter();
+
+ equal(emitter.count("foo"), 0, "no listeners for 'foo' events");
+ emitter.on("foo", () => {});
+ equal(emitter.count("foo"), 1, "listener registered");
+ emitter.on("foo", () => {});
+ equal(emitter.count("foo"), 2, "another listener registered");
+ emitter.off("foo");
+ equal(emitter.count("foo"), 0, "listeners unregistered");
+ },
+};
+
+// Wait for the next call to console.warn which includes
+// the text passed as argument
+function onConsoleWarningLogged(warningMessage) {
+ return new Promise(resolve => {
+ const ConsoleAPIStorage = Cc[
+ "@mozilla.org/consoleAPI-storage;1"
+ ].getService(Ci.nsIConsoleAPIStorage);
+
+ const observer = subject => {
+ // This is the first argument passed to console.warn()
+ const message = subject.wrappedJSObject.arguments[0];
+ if (message.includes(warningMessage)) {
+ ConsoleAPIStorage.removeLogEventListener(observer);
+ resolve();
+ }
+ };
+
+ ConsoleAPIStorage.addLogEventListener(
+ observer,
+ Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal)
+ );
+ });
+}
+
+/**
+ * Create a runnable tests based on the tests descriptor given.
+ *
+ * @param {Object} tests
+ * The tests descriptor object, contains the tests to run.
+ */
+const runnable = tests =>
+ async function () {
+ for (const name of Object.keys(tests)) {
+ info(name);
+ if (tests[name].length === 1) {
+ await new Promise(resolve => tests[name](resolve));
+ } else {
+ await tests[name]();
+ }
+ }
+ };
+
+// We want to run the same tests for both an instance of `EventEmitter` and an object
+// decorate with EventEmitter; therefore we create two strategies (`createNewEmitter` and
+// `decorateObject`) and a factory (`getEventEmitter`), where the factory is the actual
+// function used in the tests.
+
+const createNewEmitter = () => new EventEmitter();
+const decorateObject = () => EventEmitter.decorate({});
+
+// First iteration of the tests with a new instance of `EventEmitter`.
+let getEventEmitter = createNewEmitter;
+add_task(runnable(TESTS));
+// Second iteration of the tests with an object decorate using `EventEmitter`
+add_task(() => (getEventEmitter = decorateObject));
+add_task(runnable(TESTS));
diff --git a/devtools/shared/tests/xpcshell/test_eventemitter_destroy.js b/devtools/shared/tests/xpcshell/test_eventemitter_destroy.js
new file mode 100644
index 0000000000..715dd1c466
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_eventemitter_destroy.js
@@ -0,0 +1,32 @@
+/* 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";
+
+add_task(function () {
+ const { DevToolsLoader, require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+
+ // Force-load the module once in the global loader to avoid Bug 1622718.
+ require("resource://devtools/shared/event-emitter.js");
+
+ const emitterRef = (function () {
+ const loader = new DevToolsLoader();
+
+ const ref = Cu.getWeakReference(
+ loader.require("resource://devtools/shared/event-emitter.js")
+ );
+
+ loader.destroy();
+ return ref;
+ })();
+
+ Cu.forceGC();
+ Cu.forceCC();
+ Cu.forceGC();
+ Cu.forceCC();
+
+ Assert.ok(!emitterRef.get(), "weakref has been cleared by gc");
+});
diff --git a/devtools/shared/tests/xpcshell/test_eventemitter_static.js b/devtools/shared/tests/xpcshell/test_eventemitter_static.js
new file mode 100644
index 0000000000..9b17a7612f
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_eventemitter_static.js
@@ -0,0 +1,378 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {
+ ConsoleAPIListener,
+} = require("resource://devtools/server/actors/webconsole/listeners/console-api.js");
+const {
+ on,
+ once,
+ off,
+ emit,
+ count,
+ handler,
+} = require("resource://devtools/shared/event-emitter.js");
+
+const pass = message => ok(true, message);
+const fail = message => ok(false, message);
+
+/**
+ * Each method of this object is a test; tests can be synchronous or asynchronous:
+ *
+ * 1. Plain method are synchronous tests.
+ * 2. methods with `async` keyword are asynchronous tests.
+ * 3. methods with `done` as argument are asynchronous tests (`done` needs to be called to
+ * complete the test).
+ */
+const TESTS = {
+ testAddListener() {
+ const events = [{ name: "event#1" }, "event#2"];
+ const target = { name: "target" };
+
+ on(target, "message", function (message) {
+ equal(this, target, "this is a target object");
+ equal(message, events.shift(), "message is emitted event");
+ });
+
+ emit(target, "message", events[0]);
+ emit(target, "message", events[0]);
+ },
+
+ testListenerIsUniquePerType() {
+ const actual = [];
+ const target = {};
+ listener = () => actual.push(1);
+
+ on(target, "message", listener);
+ on(target, "message", listener);
+ on(target, "message", listener);
+ on(target, "foo", listener);
+ on(target, "foo", listener);
+
+ emit(target, "message");
+ deepEqual([1], actual, "only one message listener added");
+
+ emit(target, "foo");
+ deepEqual([1, 1], actual, "same listener added for other event");
+ },
+
+ testEventTypeMatters() {
+ const target = { name: "target" };
+ on(target, "message", () => fail("no event is expected"));
+ on(target, "done", () => pass("event is emitted"));
+
+ emit(target, "foo");
+ emit(target, "done");
+ },
+
+ testAllArgumentsArePassed() {
+ const foo = { name: "foo" },
+ bar = "bar";
+ const target = { name: "target" };
+
+ on(target, "message", (a, b) => {
+ equal(a, foo, "first argument passed");
+ equal(b, bar, "second argument passed");
+ });
+
+ emit(target, "message", foo, bar);
+ },
+
+ testNoSideEffectsInEmit() {
+ const target = { name: "target" };
+
+ on(target, "message", () => {
+ pass("first listener is called");
+
+ on(target, "message", () => fail("second listener is called"));
+ });
+ emit(target, "message");
+ },
+
+ testCanRemoveNextListener() {
+ const target = { name: "target" };
+
+ on(target, "data", () => {
+ pass("first listener called");
+ off(target, "data", fail);
+ });
+ on(target, "data", fail);
+
+ emit(target, "data", "Listener should be removed");
+ },
+
+ testOrderOfPropagation() {
+ const actual = [];
+ const target = { name: "target" };
+
+ on(target, "message", () => actual.push(1));
+ on(target, "message", () => actual.push(2));
+ on(target, "message", () => actual.push(3));
+ emit(target, "message");
+
+ deepEqual([1, 2, 3], actual, "called in order they were added");
+ },
+
+ testRemoveListener() {
+ const target = { name: "target" };
+ const actual = [];
+
+ on(target, "message", function listener() {
+ actual.push(1);
+ on(target, "message", () => {
+ off(target, "message", listener);
+ actual.push(2);
+ });
+ });
+
+ emit(target, "message");
+ deepEqual([1], actual, "first listener called");
+
+ emit(target, "message");
+ deepEqual([1, 1, 2], actual, "second listener called");
+
+ emit(target, "message");
+ deepEqual([1, 1, 2, 2, 2], actual, "first listener removed");
+ },
+
+ testRemoveAllListenersForType() {
+ const actual = [];
+ const target = { name: "target" };
+
+ on(target, "message", () => actual.push(1));
+ on(target, "message", () => actual.push(2));
+ on(target, "message", () => actual.push(3));
+ on(target, "bar", () => actual.push("b"));
+ off(target, "message");
+
+ emit(target, "message");
+ emit(target, "bar");
+
+ deepEqual(["b"], actual, "all message listeners were removed");
+ },
+
+ testRemoveAllListeners() {
+ const actual = [];
+ const target = { name: "target" };
+
+ on(target, "message", () => actual.push(1));
+ on(target, "message", () => actual.push(2));
+ on(target, "message", () => actual.push(3));
+ on(target, "bar", () => actual.push("b"));
+
+ off(target);
+
+ emit(target, "message");
+ emit(target, "bar");
+
+ deepEqual([], actual, "all listeners events were removed");
+ },
+
+ testFalsyArgumentsAreFine() {
+ let type, listener;
+ const target = { name: "target" },
+ actual = [];
+ on(target, "bar", () => actual.push(0));
+
+ off(target, "bar", listener);
+ emit(target, "bar");
+ deepEqual([0], actual, "3rd bad arg will keep listener");
+
+ off(target, type);
+ emit(target, "bar");
+ deepEqual([0, 0], actual, "2nd bad arg will keep listener");
+
+ off(target, type, listener);
+ emit(target, "bar");
+ deepEqual([0, 0, 0], actual, "2nd & 3rd bad args will keep listener");
+ },
+
+ testUnhandledExceptions(done) {
+ const listener = new ConsoleAPIListener(null, message => {
+ equal(message.level, "error", "Got the first exception");
+ equal(
+ message.arguments[0].message,
+ "Boom!",
+ "unhandled exception is logged"
+ );
+
+ listener.destroy();
+ done();
+ });
+
+ listener.init();
+
+ const target = {};
+
+ on(target, "message", () => {
+ throw Error("Boom!");
+ });
+
+ emit(target, "message");
+ },
+
+ testCount() {
+ const target = { name: "target" };
+
+ equal(count(target, "foo"), 0, "no listeners for 'foo' events");
+ on(target, "foo", () => {});
+ equal(count(target, "foo"), 1, "listener registered");
+ on(target, "foo", () => {});
+ equal(count(target, "foo"), 2, "another listener registered");
+ off(target);
+ equal(count(target, "foo"), 0, "listeners unregistered");
+ },
+
+ async testOnce() {
+ const target = { name: "target" };
+ const called = false;
+
+ const pFoo = once(target, "foo", function (value) {
+ ok(!called, "listener called only once");
+ equal(value, "bar", "correct argument was passed");
+ equal(this, target, "the contextual object is correct");
+ });
+ const pDone = once(target, "done");
+
+ emit(target, "foo", "bar");
+ emit(target, "foo", "baz");
+ emit(target, "done", "");
+
+ await Promise.all([pFoo, pDone]);
+ },
+
+ testRemovingOnce(done) {
+ const target = { name: "target" };
+
+ once(target, "foo", fail);
+ once(target, "done", done);
+
+ off(target, "foo", fail);
+
+ emit(target, "foo", "listener was called");
+ emit(target, "done", "");
+ },
+
+ testAddListenerWithHandlerMethod() {
+ const target = { name: "target" };
+ const actual = [];
+ const listener = function (...args) {
+ equal(
+ this,
+ target,
+ "the contextual object is correct for function listener"
+ );
+ deepEqual(args, [10, 20, 30], "arguments are properly passed");
+ };
+
+ const object = {
+ name: "target",
+ [handler](type, ...rest) {
+ actual.push(type);
+ equal(
+ this,
+ object,
+ "the contextual object is correct for object listener"
+ );
+ deepEqual(rest, [10, 20, 30], "arguments are properly passed");
+ },
+ };
+
+ on(target, "foo", listener);
+ on(target, "bar", object);
+ on(target, "baz", object);
+
+ emit(target, "foo", 10, 20, 30);
+ emit(target, "bar", 10, 20, 30);
+ emit(target, "baz", 10, 20, 30);
+
+ deepEqual(
+ actual,
+ ["bar", "baz"],
+ "object's listener called in the expected order"
+ );
+ },
+
+ testRemoveListenerWithHandlerMethod() {
+ const target = {};
+ const actual = [];
+
+ const object = {
+ [handler](type) {
+ actual.push(1);
+ on(target, "message", () => {
+ off(target, "message", object);
+ actual.push(2);
+ });
+ },
+ };
+
+ on(target, "message", object);
+
+ emit(target, "message");
+ deepEqual([1], actual, "first listener called");
+
+ emit(target, "message");
+ deepEqual([1, 1, 2], actual, "second listener called");
+
+ emit(target, "message");
+ deepEqual([1, 1, 2, 2, 2], actual, "first listener removed");
+ },
+
+ async testOnceListenerWithHandlerMethod() {
+ const target = { name: "target" };
+ const called = false;
+
+ const object = {
+ [handler](type, value) {
+ ok(!called, "listener called only once");
+ equal(type, "foo", "event type is properly passed");
+ equal(value, "bar", "correct argument was passed");
+ equal(
+ this,
+ object,
+ "the contextual object is correct for object listener"
+ );
+ },
+ };
+
+ const pFoo = once(target, "foo", object);
+
+ const pDone = once(target, "done");
+
+ emit(target, "foo", "bar");
+ emit(target, "foo", "baz");
+ emit(target, "done", "");
+
+ await Promise.all([pFoo, pDone]);
+ },
+
+ testCallingOffWithMoreThan3Args() {
+ const target = { name: "target" };
+ on(target, "data", fail);
+ off(target, "data", fail, undefined);
+ emit(target, "data", "Listener should be removed");
+ },
+};
+
+/**
+ * Create a runnable tests based on the tests descriptor given.
+ *
+ * @param {Object} tests
+ * The tests descriptor object, contains the tests to run.
+ */
+const runnable = tests =>
+ async function () {
+ for (const name of Object.keys(tests)) {
+ info(name);
+ if (tests[name].length === 1) {
+ await new Promise(resolve => tests[name](resolve));
+ } else {
+ await tests[name]();
+ }
+ }
+ };
+
+add_task(runnable(TESTS));
diff --git a/devtools/shared/tests/xpcshell/test_executeSoon.js b/devtools/shared/tests/xpcshell/test_executeSoon.js
new file mode 100644
index 0000000000..acb60360a1
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_executeSoon.js
@@ -0,0 +1,35 @@
+/* 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("resource://devtools/shared/DevToolsUtils.js");
+
+add_task(async function () {
+ await waitForTick();
+
+ let stack = Components.stack;
+ while (stack) {
+ info(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() {
+ return new Promise(resolve => {
+ executeSoon(resolve);
+ });
+}
diff --git a/devtools/shared/tests/xpcshell/test_fetch-bom.js b/devtools/shared/tests/xpcshell/test_fetch-bom.js
new file mode 100644
index 0000000000..3275c9fcd0
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_fetch-bom.js
@@ -0,0 +1,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 { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+const BinaryOutputStream = Components.Constructor(
+ "@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");
+
+ const 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_get_profile();
+
+registerCleanupFunction(() => {
+ return new Promise(resolve => server.stop(resolve));
+});
+
+add_task(async function () {
+ await test_one(serverURL + "/u8", "UTF-8");
+ await test_one(serverURL + "/u16be", "UTF-16BE");
+ await test_one(serverURL + "/u16le", "UTF-16LE");
+});
+
+async 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.
+ await DevToolsUtils.fetch(url, { charset: "ISO-8859-1" }).then(
+ ({ content }) => {
+ Assert.equal(content, "hı", "The content looks correct for " + encoding);
+ }
+ );
+}
diff --git a/devtools/shared/tests/xpcshell/test_fetch-chrome.js b/devtools/shared/tests/xpcshell/test_fetch-chrome.js
new file mode 100644
index 0000000000..38021d49c9
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_fetch-chrome.js
@@ -0,0 +1,36 @@
+/* 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(async function test_missing() {
+ await DevToolsUtils.fetch(URL_NOT_FOUND).then(
+ result => {
+ info(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(async function test_normal() {
+ await DevToolsUtils.fetch(URL_FOUND).then(result => {
+ notDeepEqual(
+ result.content,
+ "",
+ "chrome:// URI seems to be read correctly."
+ );
+ });
+});
diff --git a/devtools/shared/tests/xpcshell/test_fetch-file.js b/devtools/shared/tests/xpcshell/test_fetch-file.js
new file mode 100644
index 0000000000..8b2602a9c4
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_fetch-file.js
@@ -0,0 +1,113 @@
+/* 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 } = ChromeUtils.importESModule(
+ "resource://gre/modules/FileUtils.sys.mjs"
+);
+
+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(async function test_arrow_urls() {
+ const { path } = createTemporaryFile(".js");
+ const url = "resource://gre/modules/XPIProvider.jsm -> file://" + path;
+
+ await IOUtils.writeUTF8(path, TEST_CONTENT);
+ const { content } = await DevToolsUtils.fetch(url);
+
+ deepEqual(content, TEST_CONTENT, "The file contents were correctly read.");
+});
+
+/**
+ * Tests that empty files are read correctly.
+ */
+add_task(async function test_empty() {
+ const { path } = createTemporaryFile();
+ const { content } = await DevToolsUtils.fetch("file://" + path);
+ deepEqual(content, "", "The empty file was read correctly.");
+});
+
+/**
+ * Tests that UTF-8 encoded files are correctly read.
+ */
+add_task(async function test_encoding_utf8() {
+ const { path } = createTemporaryFile();
+ await IOUtils.write(path, UTF8_TEST_BUFFER);
+
+ const { content } = await 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(async function test_encoding_iso_8859_1() {
+ const { path } = createTemporaryFile();
+ await IOUtils.write(path, ISO_8859_1_BUFFER);
+
+ const { content } = await 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(async function test_missing() {
+ await DevToolsUtils.fetch("file:///file/not/found.right").then(
+ result => {
+ info(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(async function test_schemeless_files() {
+ const { path } = createTemporaryFile();
+
+ await IOUtils.writeUTF8(path, TEST_CONTENT);
+
+ const { content } = await 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) {
+ const name = "test_fetch-file-" + Math.random() + (extension || "");
+ const file = FileUtils.getFile("TmpD", [name]);
+ file.create(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("0755", 8));
+
+ registerCleanupFunction(() => {
+ file.remove(false);
+ });
+
+ return file;
+}
diff --git a/devtools/shared/tests/xpcshell/test_fetch-http.js b/devtools/shared/tests/xpcshell/test_fetch-http.js
new file mode 100644
index 0000000000..eec2b18b83
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_fetch-http.js
@@ -0,0 +1,67 @@
+/* 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 } = ChromeUtils.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) {
+ info("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);
+
+ const body = "[" + Math.random() + "]";
+ response.bodyOutputStream.write(body, body.length);
+}
+
+do_get_profile();
+
+registerCleanupFunction(() => {
+ return new Promise(resolve => server.stop(resolve));
+});
+
+add_task(async function test_normal() {
+ await DevToolsUtils.fetch(NORMAL_URL).then(({ content }) => {
+ ok(
+ content.includes("The content looks correct."),
+ "The content looks correct."
+ );
+ });
+});
+
+add_task(async function test_caching() {
+ let initialContent = null;
+
+ info("Performing the first request.");
+ await DevToolsUtils.fetch(CACHED_URL).then(({ content }) => {
+ info("Got the first response: " + content);
+ initialContent = content;
+ });
+
+ info("Performing another request, expecting to get cached response.");
+ await DevToolsUtils.fetch(CACHED_URL).then(({ content }) => {
+ deepEqual(content, initialContent, "The content was loaded from cache.");
+ });
+
+ info("Performing a third request with cache bypassed.");
+ const opts = { loadFromCache: false };
+ await 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/xpcshell/test_fetch-resource.js b/devtools/shared/tests/xpcshell/test_fetch-resource.js
new file mode 100644
index 0000000000..a320d63151
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_fetch-resource.js
@@ -0,0 +1,43 @@
+/* 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";
+
+// Disable `xpc::IsInAutomation()` so we don't crash when accessing a
+// nonexistent resource URI.
+Services.prefs.setBoolPref(
+ "security.turn_off_all_security_so_that_viruses_can_take_over_this_computer",
+ false
+);
+
+/**
+ * Test that non-existent files are handled correctly.
+ */
+add_task(async function test_missing() {
+ await DevToolsUtils.fetch(URL_NOT_FOUND).then(
+ result => {
+ info(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(async function test_normal() {
+ await DevToolsUtils.fetch(URL_FOUND).then(result => {
+ notDeepEqual(
+ result.content,
+ "",
+ "resource:// URI seems to be read correctly."
+ );
+ });
+});
diff --git a/devtools/shared/tests/xpcshell/test_flatten.js b/devtools/shared/tests/xpcshell/test_flatten.js
new file mode 100644
index 0000000000..2e2a80a9d5
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_flatten.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// 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/xpcshell/test_indentation.js b/devtools/shared/tests/xpcshell/test_indentation.js
new file mode 100644
index 0000000000..7ed84c6a87
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_indentation.js
@@ -0,0 +1,150 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {
+ EXPAND_TAB,
+ TAB_SIZE,
+ DETECT_INDENT,
+ getTabPrefs,
+ getIndentationFromPrefs,
+ getIndentationFromIteration,
+ getIndentationFromString,
+} = require("resource://devtools/shared/indentation.js");
+
+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(
+ getTabPrefs(),
+ { indentUnit: 73, indentWithTabs: true },
+ "getTabPrefs basic test"
+ );
+ 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 (const test of TESTS) {
+ const iterFn = function (start, end, callback) {
+ test.input.slice(start, end).forEach(callback);
+ };
+
+ deepEqual(
+ getIndentationFromIteration(iterFn),
+ test.expected,
+ "test getIndentationFromIteration " + test.desc
+ );
+ }
+
+ for (const 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/xpcshell/test_independent_loaders.js b/devtools/shared/tests/xpcshell/test_independent_loaders.js
new file mode 100644
index 0000000000..ee8771db25
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_independent_loaders.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * 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() {
+ const loader1 = new DevToolsLoader();
+ const loader2 = new DevToolsLoader();
+
+ const indent1 = loader1.require("resource://devtools/shared/indentation.js");
+ const indent2 = loader2.require("resource://devtools/shared/indentation.js");
+
+ Assert.ok(indent1 !== indent2);
+
+ Assert.ok(loader1.loader !== loader2.loader);
+ Assert.ok(loader1.id !== loader2.id);
+}
diff --git a/devtools/shared/tests/xpcshell/test_invisible_loader.js b/devtools/shared/tests/xpcshell/test_invisible_loader.js
new file mode 100644
index 0000000000..d83efd9c2a
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_invisible_loader.js
@@ -0,0 +1,80 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { addDebuggerToGlobal } = ChromeUtils.importESModule(
+ "resource://gre/modules/jsdebugger.sys.mjs"
+);
+addDebuggerToGlobal(globalThis);
+
+/**
+ * Ensure that sandboxes created via the Dev Tools loader respect the
+ * invisibleToDebugger flag.
+ */
+function run_test() {
+ visible_loader();
+ invisible_loader();
+ // TODO: invisibleToDebugger should be deprecated in favor of
+ // useDistinctSystemPrincipalLoader, but we might move out from the loader
+ // to using only standard imports instead.
+ distinct_system_principal_loader();
+}
+
+function visible_loader() {
+ const loader = new DevToolsLoader({
+ invisibleToDebugger: false,
+ });
+ loader.require("resource://devtools/shared/indentation.js");
+
+ const dbg = new Debugger();
+ const sandbox = loader.loader.sharedGlobal;
+
+ try {
+ dbg.addDebuggee(sandbox);
+ Assert.ok(true);
+ } catch (e) {
+ do_throw("debugger could not add visible value");
+ }
+}
+
+function invisible_loader() {
+ const loader = new DevToolsLoader({
+ invisibleToDebugger: true,
+ });
+ loader.require("resource://devtools/shared/indentation.js");
+
+ const dbg = new Debugger();
+ const sandbox = loader.loader.sharedGlobal;
+
+ try {
+ dbg.addDebuggee(sandbox);
+ do_throw("debugger added invisible value");
+ } catch (e) {
+ Assert.ok(true);
+ }
+}
+
+function distinct_system_principal_loader() {
+ const {
+ useDistinctSystemPrincipalLoader,
+ releaseDistinctSystemPrincipalLoader,
+ } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs"
+ );
+
+ const requester = {};
+ const loader = useDistinctSystemPrincipalLoader(requester);
+ loader.require("resource://devtools/shared/indentation.js");
+
+ const dbg = new Debugger();
+ const sandbox = loader.loader.sharedGlobal;
+
+ try {
+ dbg.addDebuggee(sandbox);
+ do_throw("debugger added invisible value");
+ } catch (e) {
+ Assert.ok(true);
+ }
+ releaseDistinctSystemPrincipalLoader(requester);
+}
diff --git a/devtools/shared/tests/xpcshell/test_isSet.js b/devtools/shared/tests/xpcshell/test_isSet.js
new file mode 100644
index 0000000000..73ccb6fe6a
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_isSet.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test ThreadSafeDevToolsUtils.isSet
+
+function run_test() {
+ Services.prefs.setBoolPref(
+ "security.allow_parent_unrestricted_js_loads",
+ true
+ );
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.allow_parent_unrestricted_js_loads");
+ });
+
+ 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/xpcshell/test_loader.js b/devtools/shared/tests/xpcshell/test_loader.js
new file mode 100644
index 0000000000..195a364bc6
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_loader.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {
+ useDistinctSystemPrincipalLoader,
+ releaseDistinctSystemPrincipalLoader,
+} = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs"
+);
+
+function run_test() {
+ const requester = {},
+ requester2 = {};
+
+ const loader = useDistinctSystemPrincipalLoader(requester);
+
+ // The DevTools dedicated global forces invisibleToDebugger on its realm at
+ // https://searchfox.org/mozilla-central/rev/12a18f7e112a4dcf88d8441d439b84144bfbe9a3/js/xpconnect/loader/mozJSModuleLoader.cpp#591-593
+ // but this is not observable directly.
+ const DevToolsSpecialGlobal = Cu.getGlobalForObject(
+ ChromeUtils.importESModule(
+ "resource://devtools/shared/DevToolsInfaillibleUtils.sys.mjs",
+ { loadInDevToolsLoader: true }
+ )
+ );
+
+ const regularLoader = new DevToolsLoader();
+ ok(
+ DevToolsSpecialGlobal !== regularLoader.loader.sharedGlobal,
+ "The regular loader is not using the special DevTools global"
+ );
+
+ info("Assert the key difference with the other regular loaders:");
+ ok(
+ DevToolsSpecialGlobal === loader.loader.sharedGlobal,
+ "The system principal loader is using the special DevTools global"
+ );
+
+ ok(loader.loader, "Loader is not destroyed before calling release");
+
+ info("Now assert the precise behavior of release");
+ releaseDistinctSystemPrincipalLoader({});
+ ok(
+ loader.loader,
+ "Loader is still not destroyed after calling release with another requester"
+ );
+
+ releaseDistinctSystemPrincipalLoader(requester);
+ ok(
+ !loader.loader,
+ "Loader is destroyed after calling release with the right requester"
+ );
+
+ info("Now test the behavior with two concurrent usages");
+ const loader2 = useDistinctSystemPrincipalLoader(requester);
+ Assert.notEqual(loader, loader2, "We get a new loader instance");
+ ok(
+ DevToolsSpecialGlobal === loader2.loader.sharedGlobal,
+ "The new system principal loader is also using the special DevTools global"
+ );
+
+ const loader3 = useDistinctSystemPrincipalLoader(requester2);
+ Assert.equal(loader2, loader3, "The two loader last loaders are shared");
+
+ releaseDistinctSystemPrincipalLoader(requester);
+ ok(loader2.loader, "Loader isn't destroy on the first call to destroy");
+
+ releaseDistinctSystemPrincipalLoader(requester2);
+ ok(!loader2.loader, "Loader is destroyed on the second call to destroy");
+}
diff --git a/devtools/shared/tests/xpcshell/test_natural-sort.js b/devtools/shared/tests/xpcshell/test_natural-sort.js
new file mode 100644
index 0000000000..dea60cf16f
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_natural-sort.js
@@ -0,0 +1,911 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const l10n = new Localization(["devtools/client/storage.ftl"], true);
+const sessionString = l10n.formatValueSync("storage-expires-session");
+const {
+ naturalSortCaseSensitive,
+ naturalSortCaseInsensitive,
+} = require("resource://devtools/shared/natural-sort.js");
+
+function run_test() {
+ test("different values types", function () {
+ runTest(["a", 1], [1, "a"], "number always comes first");
+ runTest(
+ ["1", 1],
+ ["1", 1],
+ "number vs numeric string - should remain unchanged (error in chrome)"
+ );
+ runTest(
+ ["02", 3, 2, "01"],
+ ["01", "02", 2, 3],
+ "padding numeric string vs number"
+ );
+ });
+
+ test("datetime", function () {
+ runTest(
+ ["10/12/2008", "10/11/2008", "10/11/2007", "10/12/2007"],
+ ["10/11/2007", "10/12/2007", "10/11/2008", "10/12/2008"],
+ "similar dates"
+ );
+ runTest(
+ ["01/01/2008", "01/10/2008", "01/01/1992", "01/01/1991"],
+ ["01/01/1991", "01/01/1992", "01/01/2008", "01/10/2008"],
+ "similar dates"
+ );
+ runTest(
+ [
+ "Wed Jan 01 2010 00:00:00 GMT-0800 (Pacific Standard Time)",
+ "Thu Dec 31 2009 00:00:00 GMT-0800 (Pacific Standard Time)",
+ "Wed Jan 01 2010 00:00:00 GMT-0500 (Eastern Standard Time)",
+ ],
+ [
+ "Thu Dec 31 2009 00:00:00 GMT-0800 (Pacific Standard Time)",
+ "Wed Jan 01 2010 00:00:00 GMT-0500 (Eastern Standard Time)",
+ "Wed Jan 01 2010 00:00:00 GMT-0800 (Pacific Standard Time)",
+ ],
+ "javascript toString(), different timezones"
+ );
+ runTest(
+ [
+ "Saturday, July 3, 2010",
+ "Monday, August 2, 2010",
+ "Monday, May 3, 2010",
+ ],
+ [
+ "Monday, May 3, 2010",
+ "Saturday, July 3, 2010",
+ "Monday, August 2, 2010",
+ ],
+ "Date.toString(), Date.toLocaleString()"
+ );
+ runTest(
+ [
+ "Mon, 15 Jun 2009 20:45:30 GMT",
+ "Mon, 3 May 2010 17:45:30 GMT",
+ "Mon, 15 Jun 2009 17:45:30 GMT",
+ ],
+ [
+ "Mon, 15 Jun 2009 17:45:30 GMT",
+ "Mon, 15 Jun 2009 20:45:30 GMT",
+ "Mon, 3 May 2010 17:45:30 GMT",
+ ],
+ "Date.toUTCString()"
+ );
+ runTest(
+ [
+ "Saturday, July 3, 2010 1:45 PM",
+ "Saturday, July 3, 2010 1:45 AM",
+ "Monday, August 2, 2010 1:45 PM",
+ "Monday, May 3, 2010 1:45 PM",
+ ],
+ [
+ "Monday, May 3, 2010 1:45 PM",
+ "Saturday, July 3, 2010 1:45 AM",
+ "Saturday, July 3, 2010 1:45 PM",
+ "Monday, August 2, 2010 1:45 PM",
+ ],
+ ""
+ );
+ runTest(
+ [
+ "Saturday, July 3, 2010 1:45:30 PM",
+ "Saturday, July 3, 2010 1:45:29 PM",
+ "Monday, August 2, 2010 1:45:01 PM",
+ "Monday, May 3, 2010 1:45:00 PM",
+ ],
+ [
+ "Monday, May 3, 2010 1:45:00 PM",
+ "Saturday, July 3, 2010 1:45:29 PM",
+ "Saturday, July 3, 2010 1:45:30 PM",
+ "Monday, August 2, 2010 1:45:01 PM",
+ ],
+ ""
+ );
+ runTest(
+ ["2/15/2009 1:45 PM", "1/15/2009 1:45 PM", "2/15/2009 1:45 AM"],
+ ["1/15/2009 1:45 PM", "2/15/2009 1:45 AM", "2/15/2009 1:45 PM"],
+ ""
+ );
+ runTest(
+ [
+ "2010-06-15T13:45:30",
+ "2009-06-15T13:45:30",
+ "2009-06-15T01:45:30.2",
+ "2009-01-15T01:45:30",
+ ],
+ [
+ "2009-01-15T01:45:30",
+ "2009-06-15T01:45:30.2",
+ "2009-06-15T13:45:30",
+ "2010-06-15T13:45:30",
+ ],
+ "ISO8601 Dates"
+ );
+ runTest(
+ ["2010-06-15 13:45:30", "2009-06-15 13:45:30", "2009-01-15 01:45:30"],
+ ["2009-01-15 01:45:30", "2009-06-15 13:45:30", "2010-06-15 13:45:30"],
+ "ISO8601-ish YYYY-MM-DDThh:mm:ss - which does not parse into a Date instance"
+ );
+ runTest(
+ [
+ "Mon, 15 Jun 2009 20:45:30 GMT",
+ "Mon, 15 Jun 2009 20:45:30 PDT",
+ "Mon, 15 Jun 2009 20:45:30 EST",
+ ],
+ [
+ "Mon, 15 Jun 2009 20:45:30 GMT",
+ "Mon, 15 Jun 2009 20:45:30 EST",
+ "Mon, 15 Jun 2009 20:45:30 PDT",
+ ],
+ "RFC1123 testing different timezones"
+ );
+ runTest(
+ ["1245098730000", "14330728000", "1245098728000"],
+ ["14330728000", "1245098728000", "1245098730000"],
+ "unix epoch, Date.getTime()"
+ );
+ runTest(
+ [
+ new Date("2001-01-10"),
+ "2015-01-01",
+ new Date("2001-01-01"),
+ "1998-01-01",
+ ],
+ [
+ "1998-01-01",
+ new Date("2001-01-01"),
+ new Date("2001-01-10"),
+ "2015-01-01",
+ ],
+ "mixed Date types"
+ );
+ runTest(
+ [
+ "Tue, 29 Jun 2021 11:31:17 GMT",
+ "Sun, 14 Jun 2009 11:11:15 GMT",
+ sessionString,
+ "Mon, 15 Jun 2009 20:45:30 GMT",
+ ],
+ [
+ sessionString,
+ "Sun, 14 Jun 2009 11:11:15 GMT",
+ "Mon, 15 Jun 2009 20:45:30 GMT",
+ "Tue, 29 Jun 2021 11:31:17 GMT",
+ ],
+ `"${sessionString}" amongst date strings`
+ );
+ runTest(
+ [
+ "Wed, 04 Sep 2024 09:11:44 GMT",
+ sessionString,
+ "Tue, 06 Sep 2022 09:11:44 GMT",
+ sessionString,
+ "Mon, 05 Sep 2022 09:12:41 GMT",
+ ],
+ [
+ sessionString,
+ sessionString,
+ "Mon, 05 Sep 2022 09:12:41 GMT",
+ "Tue, 06 Sep 2022 09:11:44 GMT",
+ "Wed, 04 Sep 2024 09:11:44 GMT",
+ ],
+ `"${sessionString}" amongst date strings (complex)`
+ );
+
+ runTest(
+ [
+ "Madras",
+ "Jalfrezi",
+ "Rogan Josh",
+ "Vindaloo",
+ "Tikka Masala",
+ sessionString,
+ "Masala",
+ "Korma",
+ ],
+ [
+ "Jalfrezi",
+ "Korma",
+ "Madras",
+ "Masala",
+ "Rogan Josh",
+ sessionString,
+ "Tikka Masala",
+ "Vindaloo",
+ ],
+ `"${sessionString}" amongst strings`
+ );
+ });
+
+ test("version number strings", function () {
+ runTest(
+ ["1.0.2", "1.0.1", "1.0.0", "1.0.9"],
+ ["1.0.0", "1.0.1", "1.0.2", "1.0.9"],
+ "close version numbers"
+ );
+ runTest(
+ ["1.1.100", "1.1.1", "1.1.10", "1.1.54"],
+ ["1.1.1", "1.1.10", "1.1.54", "1.1.100"],
+ "more version numbers"
+ );
+ runTest(
+ ["1.0.03", "1.0.003", "1.0.002", "1.0.0001"],
+ ["1.0.0001", "1.0.002", "1.0.003", "1.0.03"],
+ "multi-digit branch release"
+ );
+ runTest(
+ [
+ "1.1beta",
+ "1.1.2alpha3",
+ "1.0.2alpha3",
+ "1.0.2alpha1",
+ "1.0.1alpha4",
+ "2.1.2",
+ "2.1.1",
+ ],
+ [
+ "1.0.1alpha4",
+ "1.0.2alpha1",
+ "1.0.2alpha3",
+ "1.1.2alpha3",
+ "1.1beta",
+ "2.1.1",
+ "2.1.2",
+ ],
+ "close version numbers"
+ );
+ runTest(
+ [
+ "myrelease-1.1.3",
+ "myrelease-1.2.3",
+ "myrelease-1.1.4",
+ "myrelease-1.1.1",
+ "myrelease-1.0.5",
+ ],
+ [
+ "myrelease-1.0.5",
+ "myrelease-1.1.1",
+ "myrelease-1.1.3",
+ "myrelease-1.1.4",
+ "myrelease-1.2.3",
+ ],
+ "string first"
+ );
+ });
+
+ test("numerics", function () {
+ runTest(["10", 9, 2, "1", "4"], ["1", 2, "4", 9, "10"], "string vs number");
+ runTest(
+ ["0001", "002", "001"],
+ ["0001", "001", "002"],
+ "0 left-padded numbers"
+ );
+ runTest(
+ [2, 1, "1", "0001", "002", "02", "001"],
+ [1, "1", "0001", "001", 2, "002", "02"],
+ "0 left-padded numbers and regular numbers"
+ );
+ runTest(
+ ["10.0401", 10.022, 10.042, "10.021999"],
+ ["10.021999", 10.022, "10.0401", 10.042],
+ "decimal string vs decimal, different precision"
+ );
+ runTest(
+ ["10.04", 10.02, 10.03, "10.01"],
+ ["10.01", 10.02, 10.03, "10.04"],
+ "decimal string vs decimal, same precision"
+ );
+ runTest(
+ ["10.04f", "10.039F", "10.038d", "10.037D"],
+ ["10.037D", "10.038d", "10.039F", "10.04f"],
+ "float/decimal with 'F' or 'D' notation"
+ );
+ runTest(
+ ["10.004Z", "10.039T", "10.038ooo", "10.037g"],
+ ["10.004Z", "10.037g", "10.038ooo", "10.039T"],
+ "not foat/decimal notation"
+ );
+ runTest(
+ ["1.528535047e5", "1.528535047e7", "1.52e15", "1.528535047e3", "1.59e-3"],
+ ["1.59e-3", "1.528535047e3", "1.528535047e5", "1.528535047e7", "1.52e15"],
+ "scientific notation"
+ );
+ runTest(
+ ["-1", "-2", "4", "-3", "0", "-5"],
+ ["-5", "-3", "-2", "-1", "0", "4"],
+ "negative numbers as strings"
+ );
+ runTest(
+ [-1, "-2", 4, -3, "0", "-5"],
+ ["-5", -3, "-2", -1, "0", 4],
+ "negative numbers as strings - mixed input type, string + numeric"
+ );
+ runTest(
+ [-2.01, -2.1, 4.144, 4.1, -2.001, -5],
+ [-5, -2.1, -2.01, -2.001, 4.1, 4.144],
+ "negative floats - all numeric"
+ );
+ });
+
+ test("IP addresses", function () {
+ runTest(
+ [
+ "192.168.0.100",
+ "192.168.0.1",
+ "192.168.1.1",
+ "192.168.0.250",
+ "192.168.1.123",
+ "10.0.0.2",
+ "10.0.0.1",
+ ],
+ [
+ "10.0.0.1",
+ "10.0.0.2",
+ "192.168.0.1",
+ "192.168.0.100",
+ "192.168.0.250",
+ "192.168.1.1",
+ "192.168.1.123",
+ ]
+ );
+ });
+
+ test("filenames", function () {
+ runTest(
+ ["img12.png", "img10.png", "img2.png", "img1.png"],
+ ["img1.png", "img2.png", "img10.png", "img12.png"],
+ "simple image filenames"
+ );
+ runTest(
+ [
+ "car.mov",
+ "01alpha.sgi",
+ "001alpha.sgi",
+ "my.string_41299.tif",
+ "organic2.0001.sgi",
+ ],
+ [
+ "001alpha.sgi",
+ "01alpha.sgi",
+ "car.mov",
+ "my.string_41299.tif",
+ "organic2.0001.sgi",
+ ],
+ "complex filenames"
+ );
+ runTest(
+ [
+ "./system/kernel/js/01_ui.core.js",
+ "./system/kernel/js/00_jquery-1.3.2.js",
+ "./system/kernel/js/02_my.desktop.js",
+ ],
+ [
+ "./system/kernel/js/00_jquery-1.3.2.js",
+ "./system/kernel/js/01_ui.core.js",
+ "./system/kernel/js/02_my.desktop.js",
+ ],
+ "unix filenames"
+ );
+ });
+
+ test("space(s) as first character(s)", function () {
+ runTest(["alpha", " 1", " 3", " 2", 0], [0, " 1", " 2", " 3", "alpha"]);
+ });
+
+ test("empty strings and space character", function () {
+ runTest(
+ ["10023", "999", "", 2, 5.663, 5.6629],
+ ["", 2, 5.6629, 5.663, "999", "10023"]
+ );
+ runTest([0, "0", ""], [0, "0", ""]);
+ });
+
+ test("hex", function () {
+ runTest(["0xA", "0x9", "0x99"], ["0x9", "0xA", "0x99"], "real hex numbers");
+ runTest(
+ ["0xZZ", "0xVVV", "0xVEV", "0xUU"],
+ ["0xUU", "0xVEV", "0xVVV", "0xZZ"],
+ "fake hex numbers"
+ );
+ });
+
+ test("unicode", function () {
+ runTest(
+ ["\u0044", "\u0055", "\u0054", "\u0043"],
+ ["\u0043", "\u0044", "\u0054", "\u0055"],
+ "basic latin"
+ );
+ });
+
+ test("sparse array sort", function () {
+ const sarray = [3, 2];
+ const sarrayOutput = [1, 2, 3];
+
+ sarray[10] = 1;
+ for (let i = 0; i < 8; i++) {
+ sarrayOutput.push(undefined);
+ }
+ runTest(sarray, sarrayOutput, "simple sparse array");
+ });
+
+ test("case insensitive support", function () {
+ runTest(
+ ["A", "b", "C", "d", "E", "f"],
+ ["A", "b", "C", "d", "E", "f"],
+ "case sensitive pre-sorted array",
+ true
+ );
+ runTest(
+ ["A", "C", "E", "b", "d", "f"],
+ ["A", "b", "C", "d", "E", "f"],
+ "case sensitive un-sorted array",
+ true
+ );
+ runTest(
+ ["A", "C", "E", "b", "d", "f"],
+ ["A", "C", "E", "b", "d", "f"],
+ "case sensitive pre-sorted array"
+ );
+ runTest(
+ ["A", "b", "C", "d", "E", "f"],
+ ["A", "C", "E", "b", "d", "f"],
+ "case sensitive un-sorted array"
+ );
+ });
+
+ test("rosetta code natural sort small test set", function () {
+ runTest(
+ [
+ "ignore leading spaces: 2-2",
+ " ignore leading spaces: 2-1",
+ " ignore leading spaces: 2+0",
+ " ignore leading spaces: 2+1",
+ ],
+ [
+ " ignore leading spaces: 2+0",
+ " ignore leading spaces: 2+1",
+ " ignore leading spaces: 2-1",
+ "ignore leading spaces: 2-2",
+ ],
+ "Ignoring leading spaces"
+ );
+ runTest(
+ [
+ "ignore m.a.s spaces: 2-2",
+ "ignore m.a.s spaces: 2-1",
+ "ignore m.a.s spaces: 2+0",
+ "ignore m.a.s spaces: 2+1",
+ ],
+ [
+ "ignore m.a.s spaces: 2+0",
+ "ignore m.a.s spaces: 2+1",
+ "ignore m.a.s spaces: 2-1",
+ "ignore m.a.s spaces: 2-2",
+ ],
+ "Ignoring multiple adjacent spaces (m.a.s)"
+ );
+ runTest(
+ [
+ "Equiv. spaces: 3-3",
+ "Equiv.\rspaces: 3-2",
+ "Equiv.\x0cspaces: 3-1",
+ "Equiv.\x0bspaces: 3+0",
+ "Equiv.\nspaces: 3+1",
+ "Equiv.\tspaces: 3+2",
+ ],
+ [
+ "Equiv.\x0bspaces: 3+0",
+ "Equiv.\nspaces: 3+1",
+ "Equiv.\tspaces: 3+2",
+ "Equiv.\x0cspaces: 3-1",
+ "Equiv.\rspaces: 3-2",
+ "Equiv. spaces: 3-3",
+ ],
+ "Equivalent whitespace characters"
+ );
+ runTest(
+ [
+ "cASE INDEPENENT: 3-2",
+ "caSE INDEPENENT: 3-1",
+ "casE INDEPENENT: 3+0",
+ "case INDEPENENT: 3+1",
+ ],
+ [
+ "casE INDEPENENT: 3+0",
+ "case INDEPENENT: 3+1",
+ "caSE INDEPENENT: 3-1",
+ "cASE INDEPENENT: 3-2",
+ ],
+ "Case Indepenent sort (naturalSort.insensitive = true)",
+ true
+ );
+ runTest(
+ [
+ "foo100bar99baz0.txt",
+ "foo100bar10baz0.txt",
+ "foo1000bar99baz10.txt",
+ "foo1000bar99baz9.txt",
+ ],
+ [
+ "foo100bar10baz0.txt",
+ "foo100bar99baz0.txt",
+ "foo1000bar99baz9.txt",
+ "foo1000bar99baz10.txt",
+ ],
+ "Numeric fields as numerics"
+ );
+ runTest(
+ [
+ "The Wind in the Willows",
+ "The 40th step more",
+ "The 39 steps",
+ "Wanda",
+ ],
+ [
+ "The 39 steps",
+ "The 40th step more",
+ "The Wind in the Willows",
+ "Wanda",
+ ],
+ "Title sorts"
+ );
+ runTest(
+ [
+ "Equiv. \xfd accents: 2-2",
+ "Equiv. \xdd accents: 2-1",
+ "Equiv. y accents: 2+0",
+ "Equiv. Y accents: 2+1",
+ ],
+ [
+ "Equiv. y accents: 2+0",
+ "Equiv. Y accents: 2+1",
+ "Equiv. \xfd accents: 2-2",
+ "Equiv. \xdd accents: 2-1",
+ ],
+ "Equivalent accented characters (and case) (naturalSort.insensitive = true)",
+ true
+ );
+ // This is not a valuable unicode ordering test
+ // runTest(
+ // ['Start with an \u0292: 2-2', 'Start with an \u017f: 2-1', 'Start with an \xdf: 2+0', 'Start with an s: 2+1'],
+ // ['Start with an s: 2+1', 'Start with an \xdf: 2+0', 'Start with an \u017f: 2-1', 'Start with an \u0292: 2-2'],
+ // 'Character replacements');
+ });
+
+ test("contributed tests", function () {
+ runTest(
+ [
+ "T78",
+ "U17",
+ "U10",
+ "U12",
+ "U14",
+ "745",
+ "U7",
+ "485",
+ "S16",
+ "S2",
+ "S22",
+ "1081",
+ "S25",
+ "1055",
+ "779",
+ "776",
+ "771",
+ "44",
+ "4",
+ "87",
+ "1091",
+ "42",
+ "480",
+ "952",
+ "951",
+ "756",
+ "1000",
+ "824",
+ "770",
+ "666",
+ "633",
+ "619",
+ "1",
+ "991",
+ "77H",
+ "PIER-7",
+ "47",
+ "29",
+ "9",
+ "77L",
+ "433",
+ ],
+ [
+ "1",
+ "4",
+ "9",
+ "29",
+ "42",
+ "44",
+ "47",
+ "77H",
+ "77L",
+ "87",
+ "433",
+ "480",
+ "485",
+ "619",
+ "633",
+ "666",
+ "745",
+ "756",
+ "770",
+ "771",
+ "776",
+ "779",
+ "824",
+ "951",
+ "952",
+ "991",
+ "1000",
+ "1055",
+ "1081",
+ "1091",
+ "PIER-7",
+ "S2",
+ "S16",
+ "S22",
+ "S25",
+ "T78",
+ "U7",
+ "U10",
+ "U12",
+ "U14",
+ "U17",
+ ],
+ "contributed by Bob Zeiner (Chrome not stable sort)"
+ );
+ runTest(
+ [
+ "FSI stop, Position: 5",
+ "Mail Group stop, Position: 5",
+ "Mail Group stop, Position: 5",
+ "FSI stop, Position: 6",
+ "FSI stop, Position: 6",
+ "Newsstand stop, Position: 4",
+ "Newsstand stop, Position: 4",
+ "FSI stop, Position: 5",
+ ],
+ [
+ "FSI stop, Position: 5",
+ "FSI stop, Position: 5",
+ "FSI stop, Position: 6",
+ "FSI stop, Position: 6",
+ "Mail Group stop, Position: 5",
+ "Mail Group stop, Position: 5",
+ "Newsstand stop, Position: 4",
+ "Newsstand stop, Position: 4",
+ ],
+ "contributed by Scott"
+ );
+ runTest(
+ [2, 10, 1, "azd", undefined, "asd"],
+ [1, 2, 10, "asd", "azd", undefined],
+ "issue #2 - undefined support - jarvinen pekka"
+ );
+ runTest(
+ [undefined, undefined, undefined, 1, undefined],
+ [1, undefined, undefined, undefined],
+ "issue #2 - undefined support - jarvinen pekka"
+ );
+ runTest(
+ ["-1", "-2", "4", "-3", "0", "-5"],
+ ["-5", "-3", "-2", "-1", "0", "4"],
+ "issue #3 - invalid numeric string sorting - guilermo.dev"
+ );
+ // native sort implementations are not guaranteed to be stable (i.e. Chrome)
+ // runTest(
+ // ['9','11','22','99','A','aaaa','bbbb','Aaaa','aAaa','aa','AA','Aa','aA','BB','bB','aaA','AaA','aaa'],
+ // ['9', '11', '22', '99', 'A', 'aa', 'AA', 'Aa', 'aA', 'aaA', 'AaA', 'aaa', 'aaaa', 'Aaaa', 'aAaa', 'BB', 'bB', 'bbbb'],
+ // 'issue #5 - invalid sort order - Howie Schecter (naturalSort.insensitive = true)'m true);
+ runTest(
+ [
+ "9",
+ "11",
+ "22",
+ "99",
+ "A",
+ "aaaa",
+ "bbbb",
+ "Aaaa",
+ "aAaa",
+ "aa",
+ "AA",
+ "Aa",
+ "aA",
+ "BB",
+ "bB",
+ "aaA",
+ "AaA",
+ "aaa",
+ ],
+ [
+ "9",
+ "11",
+ "22",
+ "99",
+ "A",
+ "AA",
+ "Aa",
+ "AaA",
+ "Aaaa",
+ "BB",
+ "aA",
+ "aAaa",
+ "aa",
+ "aaA",
+ "aaa",
+ "aaaa",
+ "bB",
+ "bbbb",
+ ],
+ "issue #5 - invalid sort order - Howie Schecter (naturalSort.insensitive = false)"
+ );
+ runTest(
+ [
+ "5D",
+ "1A",
+ "2D",
+ "33A",
+ "5E",
+ "33K",
+ "33D",
+ "5S",
+ "2C",
+ "5C",
+ "5F",
+ "1D",
+ "2M",
+ ],
+ [
+ "1A",
+ "1D",
+ "2C",
+ "2D",
+ "2M",
+ "5C",
+ "5D",
+ "5E",
+ "5F",
+ "5S",
+ "33A",
+ "33D",
+ "33K",
+ ],
+ "alphanumeric - number first"
+ );
+ runTest(
+ ["img 99", "img199", "imga99", "imgz99"],
+ ["img 99", "img199", "imga99", "imgz99"],
+ "issue #16 - Sorting incorrect when there is a space - adrien-be"
+ );
+ runTest(
+ ["img199", "img 99", "imga99", "imgz 99", "imgb99", "imgz199"],
+ ["img 99", "img199", "imga99", "imgb99", "imgz 99", "imgz199"],
+ "issue #16 - expanded test"
+ );
+ runTest(
+ ["1", "02", "3"],
+ ["1", "02", "3"],
+ "issue #18 - Any zeros that precede a number messes up the sorting - menixator"
+ );
+ // strings are coerced as floats/ints if possible and sorted accordingly - e.g. they are not chunked
+ runTest(
+ ["1.100", "1.1", "1.10", "1.54"],
+ ["1.100", "1.1", "1.10", "1.54"],
+ "issue #13 - ['1.100', '1.10', '1.1', '1.54'] etc do not sort properly... - rubenstolk"
+ );
+ runTest(
+ ["v1.100", "v1.1", "v1.10", "v1.54"],
+ ["v1.1", "v1.10", "v1.54", "v1.100"],
+ "issue #13 - ['v1.100', 'v1.10', 'v1.1', 'v1.54'] etc do not sort properly... - rubenstolk (bypass float coercion)"
+ );
+ runTest(
+ [
+ "MySnmp 1234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567",
+ "MySnmp 4234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567",
+ "MySnmp 2234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567",
+ "MySnmp 3234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567",
+ ],
+ [
+ "MySnmp 1234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567",
+ "MySnmp 2234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567",
+ "MySnmp 3234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567",
+ "MySnmp 4234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567",
+ ],
+ "issue #14 - Very large numbers make sorting very slow - Mottie"
+ );
+ runTest(
+ ["bar.1-2", "bar.1"],
+ ["bar.1", "bar.1-2"],
+ "issue #21 - javascript error"
+ );
+ runTest(
+ ["SomeString", "SomeString 1"],
+ ["SomeString", "SomeString 1"],
+ "PR #19 - ['SomeString', 'SomeString 1'] bombing on 'undefined is not an object' - dannycochran"
+ );
+ runTest(
+ [
+ "Udet",
+ "\xDCbelacker",
+ "Uell",
+ "\xDClle",
+ "Ueve",
+ "\xDCxk\xFCll",
+ "Uffenbach",
+ ],
+ [
+ "\xDCbelacker",
+ "Udet",
+ "Uell",
+ "Ueve",
+ "Uffenbach",
+ "\xDClle",
+ "\xDCxk\xFCll",
+ ],
+ "issue #9 - Sorting umlauts characters \xC4, \xD6, \xDC - diogoalves"
+ );
+ runTest(
+ ["2.2 sec", "1.9 sec", "1.53 sec"],
+ ["1.53 sec", "1.9 sec", "2.2 sec"],
+ "https://github.com/overset/javascript-natural-sort/issues/13 - ['2.2 sec','1.9 sec','1.53 sec'] - padded by spaces - harisb"
+ );
+ runTest(
+ ["2.2sec", "1.9sec", "1.53sec"],
+ ["1.53sec", "1.9sec", "2.2sec"],
+ "https://github.com/overset/javascript-natural-sort/issues/13 - ['2.2sec','1.9sec','1.53sec'] - no padding - harisb"
+ );
+ });
+}
+
+function test(description, testFunc) {
+ info(description);
+ testFunc();
+}
+
+function runTest(testArray, expected, description, caseInsensitive = false) {
+ let actual = null;
+
+ if (caseInsensitive) {
+ actual = testArray.sort((a, b) =>
+ naturalSortCaseInsensitive(a, b, sessionString)
+ );
+ } else {
+ actual = testArray.sort((a, b) =>
+ naturalSortCaseSensitive(a, b, sessionString)
+ );
+ }
+
+ compareOptions(actual, expected, description);
+}
+
+// deepEqual() doesn't work well for testing arrays containing `undefined` so
+// we need to use a custom method.
+function compareOptions(actual, expected, description) {
+ let match = true;
+ for (let i = 0; i < actual.length; i++) {
+ if (actual[i] + "" !== expected[i] + "") {
+ ok(
+ false,
+ `${description}\nElement ${i} does not match:\n[${i}] ${actual[i]}\n[${i}] ${expected[i]}`
+ );
+ match = false;
+ break;
+ }
+ }
+ if (match) {
+ ok(true, description);
+ }
+}
diff --git a/devtools/shared/tests/xpcshell/test_pluralForm-english.js b/devtools/shared/tests/xpcshell/test_pluralForm-english.js
new file mode 100644
index 0000000000..2d2f29d47e
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_pluralForm-english.js
@@ -0,0 +1,32 @@
+/* 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("resource://devtools/shared/plural-form.js");
+
+function run_test() {
+ // English has 2 plural forms
+ Assert.equal(2, PluralForm.numForms());
+
+ // Make sure for good inputs, things work as expected
+ for (let num = 0; num <= 200; num++) {
+ Assert.equal(
+ num == 1 ? "word" : "words",
+ PluralForm.get(num, "word;words")
+ );
+ }
+
+ // Not having enough plural forms defaults to the first form
+ Assert.equal("word", PluralForm.get(2, "word"));
+
+ // Empty forms defaults to the first form
+ Assert.equal("word", PluralForm.get(2, "word;"));
+}
diff --git a/devtools/shared/tests/xpcshell/test_pluralForm-makeGetter.js b/devtools/shared/tests/xpcshell/test_pluralForm-makeGetter.js
new file mode 100644
index 0000000000..d9d1facca2
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/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("resource://devtools/shared/plural-form.js");
+
+function run_test() {
+ // Irish is plural rule #11
+ const [get, numForms] = PluralForm.makeGetter(11);
+
+ // Irish has 5 plural forms
+ Assert.equal(5, numForms());
+
+ // I don't really know Irish, so I'll stick in some dummy text
+ const words = "is 1;is 2;is 3-6;is 7-10;everything else";
+
+ const test = function (text, low, high) {
+ for (let num = low; num <= high; num++) {
+ Assert.equal(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/xpcshell/test_prettifyCSS.js b/devtools/shared/tests/xpcshell/test_prettifyCSS.js
new file mode 100644
index 0000000000..3739c07462
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_prettifyCSS.js
@@ -0,0 +1,172 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test prettifyCSS.
+
+"use strict";
+
+const {
+ prettifyCSS,
+} = require("resource://devtools/shared/inspector/css-logic.js");
+
+const EXPAND_TAB = "devtools.editor.expandtab";
+
+const TESTS_TAB_INDENT = [
+ {
+ name: "simple test. indent using tabs",
+ input: "div { font-family:'Arial Black', Arial, sans-serif; }",
+ expected: ["div {", "\tfont-family:'Arial Black', Arial, sans-serif;", "}"],
+ },
+
+ {
+ name: "whitespace before open brace. indent using tabs",
+ input: "div{}",
+ expected: ["div {", "}"],
+ },
+
+ {
+ name: "minified with trailing newline. indent using tabs",
+ 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. indent using tabs",
+ input: "\n div{color: red;}",
+ expected: ["div {", "\tcolor: red;", "}"],
+ },
+
+ {
+ name: "CSS with extra closing brace. indent using tabs",
+ input: "body{margin:0}} div{color:red}",
+ expected: ["body {", "\tmargin:0", "}", "}", "div {", "\tcolor:red", "}"],
+ },
+];
+
+const TESTS_SPACE_INDENT = [
+ {
+ name: "simple test. indent using spaces",
+ input: "div { font-family:'Arial Black', Arial, sans-serif; }",
+ expected: ["div {", " font-family:'Arial Black', Arial, sans-serif;", "}"],
+ },
+
+ {
+ name: "whitespace before open brace. indent using spaces",
+ input: "div{}",
+ expected: ["div {", "}"],
+ },
+
+ {
+ name: "minified with trailing newline. indent using spaces",
+ input:
+ "\nbody{background:white;}div{font-size:4em;color:red}span{color:green;}\n",
+ expected: [
+ "body {",
+ " background:white;",
+ "}",
+ "div {",
+ " font-size:4em;",
+ " color:red",
+ "}",
+ "span {",
+ " color:green;",
+ "}",
+ ],
+ },
+
+ {
+ name: "leading whitespace. indent using spaces",
+ input: "\n div{color: red;}",
+ expected: ["div {", " color: red;", "}"],
+ },
+
+ {
+ name: "CSS with extra closing brace. indent using spaces",
+ input: "body{margin:0}} div{color:red}",
+ expected: ["body {", " margin:0", "}", "}", "div {", " color:red", "}"],
+ },
+
+ {
+ name: "HTML comments with some whitespace padding",
+ input: " \n\n\t <!--\n\n\t body {color:red} \n\n--> \t\n",
+ expected: ["body {", " color:red", "}"],
+ },
+
+ {
+ name: "HTML comments without whitespace padding",
+ input: "<!--body {color:red}-->",
+ expected: ["body {", " color:red", "}"],
+ },
+
+ {
+ name: "Breaking after commas in selectors",
+ input:
+ "@media screen, print {div, span, input {color: red;}}" +
+ "div, div, input, pre, table {color: blue;}",
+ expected: [
+ "@media screen, print {",
+ " div,",
+ " span,",
+ " input {",
+ " color: red;",
+ " }",
+ "}",
+ "div,",
+ "div,",
+ "input,",
+ "pre,",
+ "table {",
+ " color: blue;",
+ "}",
+ ],
+ },
+
+ {
+ name: "Multiline comment in CSS",
+ input: "/*\n * comment\n */\n#example{display:grid;}",
+ expected: ["/*", " * comment", " */", "#example {", " display:grid;", "}"],
+ },
+];
+
+function run_test() {
+ // Note that prettifyCSS.LINE_SEPARATOR is computed lazily, so we
+ // ensure it is set.
+ prettifyCSS("");
+
+ Services.prefs.setBoolPref(EXPAND_TAB, true);
+ for (const test of TESTS_SPACE_INDENT) {
+ info(test.name);
+
+ const input = test.input.split("\n").join(prettifyCSS.LINE_SEPARATOR);
+ const { result: output } = prettifyCSS(input);
+ const expected =
+ test.expected.join(prettifyCSS.LINE_SEPARATOR) +
+ prettifyCSS.LINE_SEPARATOR;
+ equal(output, expected, test.name);
+ }
+
+ Services.prefs.setBoolPref(EXPAND_TAB, false);
+ for (const test of TESTS_TAB_INDENT) {
+ info(test.name);
+
+ const input = test.input.split("\n").join(prettifyCSS.LINE_SEPARATOR);
+ const { result: output } = prettifyCSS(input);
+ const expected =
+ test.expected.join(prettifyCSS.LINE_SEPARATOR) +
+ prettifyCSS.LINE_SEPARATOR;
+ equal(output, expected, test.name);
+ }
+ Services.prefs.clearUserPref(EXPAND_TAB);
+}
diff --git a/devtools/shared/tests/xpcshell/test_require.js b/devtools/shared/tests/xpcshell/test_require.js
new file mode 100644
index 0000000000..b94aca23e7
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_require.js
@@ -0,0 +1,100 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test require
+
+// Ensure that DevtoolsLoader.require doesn't spawn multiple
+// loader/modules when early cached
+function testBug1091706() {
+ const loader = new DevToolsLoader();
+ const require = loader.require;
+
+ const indent1 = require("resource://devtools/shared/indentation.js");
+ const indent2 = require("resource://devtools/shared/indentation.js");
+
+ Assert.ok(indent1 === indent2);
+}
+
+function testInvalidModule() {
+ const loader = new DevToolsLoader();
+ const require = loader.require;
+
+ try {
+ // This will result in an invalid URL with no scheme and mae loadSubScript
+ // throws "Error creating URI" error
+ require("foo");
+ Assert.ok(false, "require should throw");
+ } catch (error) {
+ Assert.equal(error.message, "Module `foo` is not found at foo.js");
+ Assert.ok(
+ error.stack.includes("testInvalidModule"),
+ "Exception's stack includes the test function"
+ );
+ }
+
+ try {
+ // But when using devtools prefix, the URL is going to be correct but the file
+ // doesn't exists, leading to "Error opening input stream (invalid filename?)" error
+ require("resource://devtools/foo.js");
+ Assert.ok(false, "require should throw");
+ } catch (error) {
+ Assert.equal(
+ error.message,
+ "Module `resource://devtools/foo.js` is not found at resource://devtools/foo.js"
+ );
+ Assert.ok(
+ error.stack.includes("testInvalidModule"),
+ "Exception's stack includes the test function"
+ );
+ }
+}
+
+function testThrowingModule() {
+ const loader = new DevToolsLoader();
+ const require = loader.require;
+
+ try {
+ // Require a test module that is throwing an Error object
+ require("xpcshell-test/throwing-module-1.js");
+ Assert.ok(false, "require should throw");
+ } catch (error) {
+ Assert.equal(error.message, "my-exception");
+ Assert.ok(
+ error.stack.includes("testThrowingModule"),
+ "Exception's stack includes the test function"
+ );
+ Assert.ok(
+ error.stack.includes("throwingMethod"),
+ "Exception's stack also includes the module function that throws"
+ );
+ }
+ try {
+ // Require a test module that is throwing a string
+ require("xpcshell-test/throwing-module-2.js");
+ Assert.ok(false, "require should throw");
+ } catch (error) {
+ Assert.equal(
+ error.message,
+ "Error while loading module `xpcshell-test/throwing-module-2.js` at " +
+ "resource://test/throwing-module-2.js:\nmy-exception"
+ );
+ Assert.ok(
+ error.stack.includes("testThrowingModule"),
+ "Exception's stack includes the test function"
+ );
+ Assert.ok(
+ !error.stack.includes("throwingMethod"),
+ "Exception's stack also includes the module function that throws"
+ );
+ }
+}
+
+function run_test() {
+ testBug1091706();
+
+ testInvalidModule();
+
+ testThrowingModule();
+}
diff --git a/devtools/shared/tests/xpcshell/test_require_lazy.js b/devtools/shared/tests/xpcshell/test_require_lazy.js
new file mode 100644
index 0000000000..deba7e2128
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_require_lazy.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { loader } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+);
+// Test devtools.lazyRequireGetter
+
+function run_test() {
+ const name = "asyncUtils";
+ const path = "devtools/shared/async-utils";
+ const o = {};
+ loader.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.
+ Assert.ok(o.asyncUtils === asyncUtils);
+
+ // A non-main loader should get a new object via |lazyRequireGetter|, just
+ // as it would via a direct |require|.
+ const o2 = {};
+ const loader2 = new DevToolsLoader();
+
+ // We have to init the loader by loading any module before
+ // lazyRequireGetter is available
+ loader2.require("resource://devtools/shared/DevToolsUtils.js");
+
+ loader2.lazyRequireGetter(o2, name, path);
+ Assert.ok(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.
+ const exposeLoader = loader2.require("xpcshell-test/exposeLoader");
+ const o3 = exposeLoader.exerciseLazyRequire(name, path);
+ Assert.ok(o3.asyncUtils === o2.asyncUtils);
+}
diff --git a/devtools/shared/tests/xpcshell/test_require_raw.js b/devtools/shared/tests/xpcshell/test_require_raw.js
new file mode 100644
index 0000000000..acd0e374b1
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_require_raw.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test require using "raw!".
+
+function run_test() {
+ const loader = new DevToolsLoader();
+ const require = loader.require;
+
+ const variableFileContents = require("raw!chrome://devtools/skin/variables.css");
+ ok(!!variableFileContents.length, "raw browserRequire worked");
+
+ const propertiesFileContents = require("raw!devtools/client/locales/shared.properties");
+ ok(
+ !!propertiesFileContents.length,
+ "unprefixed properties raw require worked"
+ );
+
+ const chromePropertiesFileContents = require("raw!chrome://devtools/locale/shared.properties");
+ ok(
+ !!chromePropertiesFileContents.length,
+ "prefixed properties raw require worked"
+ );
+}
diff --git a/devtools/shared/tests/xpcshell/test_safeErrorString.js b/devtools/shared/tests/xpcshell/test_safeErrorString.js
new file mode 100644
index 0000000000..1d2e5431ed
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_safeErrorString.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// 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() {
+ const s = DevToolsUtils.safeErrorString(new Error("foo bar"));
+ // Got the message.
+ Assert.ok(s.includes("foo bar"));
+ // Got the stack.
+ Assert.ok(s.includes("test_with_error"));
+ Assert.ok(s.includes("test_safeErrorString.js"));
+ // Got the lineNumber and columnNumber.
+ Assert.ok(s.includes("Line"));
+ Assert.ok(s.includes("column"));
+}
+
+function test_with_tricky_error() {
+ const e = new Error("batman");
+ e.stack = { toString: Object.create(null) };
+ const s = DevToolsUtils.safeErrorString(e);
+ // Still got the message, despite a bad stack property.
+ Assert.ok(s.includes("batman"));
+}
+
+function test_with_string() {
+ const s = DevToolsUtils.safeErrorString("not really an error");
+ // Still get the message.
+ Assert.ok(s.includes("not really an error"));
+}
+
+function test_with_thrower() {
+ const s = DevToolsUtils.safeErrorString({
+ toString: () => {
+ throw new Error("Muahahaha");
+ },
+ });
+ // Still don't fail, get string back.
+ Assert.equal(typeof s, "string");
+}
+
+function test_with_psychotic() {
+ const s = DevToolsUtils.safeErrorString({
+ toString: () => Object.create(null),
+ });
+ // Still get a string out, and no exceptions thrown
+ Assert.equal(typeof s, "string");
+ Assert.equal(s, "[object Object]");
+}
diff --git a/devtools/shared/tests/xpcshell/test_sprintfjs.js b/devtools/shared/tests/xpcshell/test_sprintfjs.js
new file mode 100644
index 0000000000..29f7754896
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_sprintfjs.js
@@ -0,0 +1,120 @@
+/* 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 checks that our string formatter works with different patterns and
+ * arguments.
+ * Initially copied from unit tests at https://github.com/alexei/sprintf.js
+ */
+
+const { sprintf } = require("resource://devtools/shared/sprintfjs/sprintf.js");
+const PI = 3.141592653589793;
+
+function run_test() {
+ // Simple patterns
+ equal("%", sprintf("%%"));
+ equal("10", sprintf("%b", 2));
+ equal("A", sprintf("%c", 65));
+ equal("2", sprintf("%d", 2));
+ equal("2", sprintf("%i", 2));
+ equal("2", sprintf("%d", "2"));
+ equal("2", sprintf("%i", "2"));
+ equal('{"foo":"bar"}', sprintf("%j", { foo: "bar" }));
+ equal('["foo","bar"]', sprintf("%j", ["foo", "bar"]));
+ equal("2e+0", sprintf("%e", 2));
+ equal("2", sprintf("%u", 2));
+ equal("4294967294", sprintf("%u", -2));
+ equal("2.2", sprintf("%f", 2.2));
+ equal("3.141592653589793", sprintf("%g", PI));
+ equal("10", sprintf("%o", 8));
+ equal("%s", sprintf("%s", "%s"));
+ equal("ff", sprintf("%x", 255));
+ equal("FF", sprintf("%X", 255));
+ equal(
+ "Polly wants a cracker",
+ sprintf("%2$s %3$s a %1$s", "cracker", "Polly", "wants")
+ );
+ equal("Hello world!", sprintf("Hello %(who)s!", { who: "world" }));
+ equal("true", sprintf("%t", true));
+ equal("t", sprintf("%.1t", true));
+ equal("true", sprintf("%t", "true"));
+ equal("true", sprintf("%t", 1));
+ equal("false", sprintf("%t", false));
+ equal("f", sprintf("%.1t", false));
+ equal("false", sprintf("%t", ""));
+ equal("false", sprintf("%t", 0));
+
+ equal("undefined", sprintf("%T", undefined));
+ equal("null", sprintf("%T", null));
+ equal("boolean", sprintf("%T", true));
+ equal("number", sprintf("%T", 42));
+ equal("string", sprintf("%T", "This is a string"));
+ equal("function", sprintf("%T", Math.log));
+ equal("array", sprintf("%T", [1, 2, 3]));
+ equal("object", sprintf("%T", { foo: "bar" }));
+
+ equal("regexp", sprintf("%T", /<("[^"]*"|"[^"]*"|[^"">])*>/));
+
+ equal("true", sprintf("%v", true));
+ equal("42", sprintf("%v", 42));
+ equal("This is a string", sprintf("%v", "This is a string"));
+ equal("1,2,3", sprintf("%v", [1, 2, 3]));
+ equal("[object Object]", sprintf("%v", { foo: "bar" }));
+ equal(
+ "/<(\"[^\"]*\"|'[^']*'|[^'\">])*>/",
+ sprintf("%v", /<("[^"]*"|'[^']*'|[^'">])*>/)
+ );
+
+ // sign
+ equal("2", sprintf("%d", 2));
+ equal("-2", sprintf("%d", -2));
+ equal("+2", sprintf("%+d", 2));
+ equal("-2", sprintf("%+d", -2));
+ equal("2", sprintf("%i", 2));
+ equal("-2", sprintf("%i", -2));
+ equal("+2", sprintf("%+i", 2));
+ equal("-2", sprintf("%+i", -2));
+ equal("2.2", sprintf("%f", 2.2));
+ equal("-2.2", sprintf("%f", -2.2));
+ equal("+2.2", sprintf("%+f", 2.2));
+ equal("-2.2", sprintf("%+f", -2.2));
+ equal("-2.3", sprintf("%+.1f", -2.34));
+ equal("-0.0", sprintf("%+.1f", -0.01));
+ equal("3.14159", sprintf("%.6g", PI));
+ equal("3.14", sprintf("%.3g", PI));
+ equal("3", sprintf("%.1g", PI));
+ equal("-000000123", sprintf("%+010d", -123));
+ equal("______-123", sprintf("%+'_10d", -123));
+ equal("-234.34 123.2", sprintf("%f %f", -234.34, 123.2));
+
+ // padding
+ equal("-0002", sprintf("%05d", -2));
+ equal("-0002", sprintf("%05i", -2));
+ equal(" <", sprintf("%5s", "<"));
+ equal("0000<", sprintf("%05s", "<"));
+ equal("____<", sprintf("%'_5s", "<"));
+ equal("> ", sprintf("%-5s", ">"));
+ equal(">0000", sprintf("%0-5s", ">"));
+ equal(">____", sprintf("%'_-5s", ">"));
+ equal("xxxxxx", sprintf("%5s", "xxxxxx"));
+ equal("1234", sprintf("%02u", 1234));
+ equal(" -10.235", sprintf("%8.3f", -10.23456));
+ equal("-12.34 xxx", sprintf("%f %s", -12.34, "xxx"));
+ equal('{\n "foo": "bar"\n}', sprintf("%2j", { foo: "bar" }));
+ equal('[\n "foo",\n "bar"\n]', sprintf("%2j", ["foo", "bar"]));
+
+ // precision
+ equal("2.3", sprintf("%.1f", 2.345));
+ equal("xxxxx", sprintf("%5.5s", "xxxxxx"));
+ equal(" x", sprintf("%5.1s", "xxxxxx"));
+
+ equal(
+ "foobar",
+ sprintf("%s", function () {
+ return "foobar";
+ })
+ );
+}
diff --git a/devtools/shared/tests/xpcshell/test_stack.js b/devtools/shared/tests/xpcshell/test_stack.js
new file mode 100644
index 0000000000..a95d28c57d
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_stack.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test stack.js.
+
+function run_test() {
+ const loader = new DevToolsLoader();
+ const require = loader.require;
+
+ const {
+ StackFrameCache,
+ } = require("resource://devtools/server/actors/utils/stack.js");
+
+ const cache = new StackFrameCache();
+ cache.initFrames();
+ const baseFrame = {
+ line: 23,
+ column: 77,
+ source: "nowhere",
+ functionDisplayName: "nobody",
+ parent: null,
+ asyncParent: null,
+ asyncCause: null,
+ };
+ cache.addFrame(baseFrame);
+
+ let event = cache.makeEvent();
+ Assert.equal(event[0], null);
+ Assert.equal(event[1].functionDisplayName, "nobody");
+ Assert.equal(event.length, 2);
+
+ cache.addFrame({
+ line: 24,
+ column: 78,
+ source: "nowhere",
+ functionDisplayName: "still nobody",
+ parent: null,
+ asyncParent: baseFrame,
+ asyncCause: "async",
+ });
+
+ event = cache.makeEvent();
+ Assert.equal(event[0].functionDisplayName, "still nobody");
+ Assert.equal(event[0].parent, 0);
+ Assert.equal(event[0].asyncParent, 1);
+ Assert.equal(event.length, 1);
+}
diff --git a/devtools/shared/tests/xpcshell/throwing-module-1.js b/devtools/shared/tests/xpcshell/throwing-module-1.js
new file mode 100644
index 0000000000..cc7e159a76
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/throwing-module-1.js
@@ -0,0 +1,7 @@
+"use strict";
+
+function throwingMethod() {
+ throw new Error("my-exception");
+}
+
+throwingMethod();
diff --git a/devtools/shared/tests/xpcshell/throwing-module-2.js b/devtools/shared/tests/xpcshell/throwing-module-2.js
new file mode 100644
index 0000000000..3e723844ec
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/throwing-module-2.js
@@ -0,0 +1,8 @@
+"use strict";
+
+function throwingMethod() {
+ // eslint-disable-next-line no-throw-literal
+ throw "my-exception";
+}
+
+throwingMethod();
diff --git a/devtools/shared/tests/xpcshell/xpcshell.ini b/devtools/shared/tests/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..af1ae3f888
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/xpcshell.ini
@@ -0,0 +1,48 @@
+[DEFAULT]
+tags = devtools
+head = head_devtools.js
+firefox-appdir = browser
+skip-if = toolkit == 'android'
+support-files =
+ exposeLoader.js
+ throwing-module-1.js
+ throwing-module-2.js
+
+[test_assert.js]
+[test_debugger_client.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_eventemitter_abort_controller.js]
+[test_eventemitter_basic.js]
+[test_eventemitter_destroy.js]
+[test_eventemitter_static.js]
+[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_loader.js]
+[test_isSet.js]
+[test_safeErrorString.js]
+[test_defineLazyPrototypeGetter.js]
+[test_console_filtering.js]
+[test_natural-sort.js]
+[test_pluralForm-english.js]
+[test_pluralForm-makeGetter.js]
+[test_prettifyCSS.js]
+[test_require_lazy.js]
+[test_require_raw.js]
+[test_require.js]
+[test_sprintfjs.js]
+[test_stack.js]
+[test_defer.js]
+[test_executeSoon.js]