summaryrefslogtreecommitdiffstats
path: root/remote/marionette/test/xpcshell
diff options
context:
space:
mode:
Diffstat (limited to 'remote/marionette/test/xpcshell')
-rw-r--r--remote/marionette/test/xpcshell/.eslintrc.js7
-rw-r--r--remote/marionette/test/xpcshell/README16
-rw-r--r--remote/marionette/test/xpcshell/head.js3
-rw-r--r--remote/marionette/test/xpcshell/test_actors.js55
-rw-r--r--remote/marionette/test/xpcshell/test_browser.js21
-rw-r--r--remote/marionette/test/xpcshell/test_cookie.js362
-rw-r--r--remote/marionette/test/xpcshell/test_element.js789
-rw-r--r--remote/marionette/test/xpcshell/test_json.js275
-rw-r--r--remote/marionette/test/xpcshell/test_message.js245
-rw-r--r--remote/marionette/test/xpcshell/test_modal.js113
-rw-r--r--remote/marionette/test/xpcshell/test_navigate.js90
-rw-r--r--remote/marionette/test/xpcshell/test_prefs.js98
-rw-r--r--remote/marionette/test/xpcshell/test_sync.js419
-rw-r--r--remote/marionette/test/xpcshell/xpcshell.ini18
14 files changed, 2511 insertions, 0 deletions
diff --git a/remote/marionette/test/xpcshell/.eslintrc.js b/remote/marionette/test/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..2ef179ab5e
--- /dev/null
+++ b/remote/marionette/test/xpcshell/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ rules: {
+ camelcase: "off",
+ },
+};
diff --git a/remote/marionette/test/xpcshell/README b/remote/marionette/test/xpcshell/README
new file mode 100644
index 0000000000..ce516d17ca
--- /dev/null
+++ b/remote/marionette/test/xpcshell/README
@@ -0,0 +1,16 @@
+To run the tests in this directory, from the top source directory,
+either invoke the test despatcher in mach:
+
+ % ./mach test remote/marionette/test/xpcshell
+
+Or call out the harness specifically:
+
+ % ./mach xpcshell-test remote/marionette/test/xpcshell
+
+The latter gives you the --sequential option which can be useful
+when debugging to prevent tests from running in parallel.
+
+When adding new tests you must make sure they are listed in
+xpcshell.ini, otherwise they will not run on try.
+
+See also ../../doc/Testing.md for more advice on our other types of tests.
diff --git a/remote/marionette/test/xpcshell/head.js b/remote/marionette/test/xpcshell/head.js
new file mode 100644
index 0000000000..2e7cf578d3
--- /dev/null
+++ b/remote/marionette/test/xpcshell/head.js
@@ -0,0 +1,3 @@
+const SVG_NS = "http://www.w3.org/2000/svg";
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
diff --git a/remote/marionette/test/xpcshell/test_actors.js b/remote/marionette/test/xpcshell/test_actors.js
new file mode 100644
index 0000000000..9b24d1d10f
--- /dev/null
+++ b/remote/marionette/test/xpcshell/test_actors.js
@@ -0,0 +1,55 @@
+/* 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 {
+ getMarionetteCommandsActorProxy,
+ registerCommandsActor,
+ unregisterCommandsActor,
+} = ChromeUtils.importESModule(
+ "chrome://remote/content/marionette/actors/MarionetteCommandsParent.sys.mjs"
+);
+const { enableEventsActor, disableEventsActor } = ChromeUtils.importESModule(
+ "chrome://remote/content/marionette/actors/MarionetteEventsParent.sys.mjs"
+);
+
+registerCleanupFunction(function () {
+ unregisterCommandsActor();
+ disableEventsActor();
+});
+
+add_task(function test_commandsActor_register() {
+ registerCommandsActor();
+ unregisterCommandsActor();
+
+ registerCommandsActor();
+ registerCommandsActor();
+ unregisterCommandsActor();
+});
+
+add_task(async function test_commandsActor_getActorProxy_noBrowsingContext() {
+ registerCommandsActor();
+
+ try {
+ await getMarionetteCommandsActorProxy(() => null).sendQuery("foo", "bar");
+ ok(false, "Expected NoBrowsingContext error not raised");
+ } catch (e) {
+ ok(
+ e.message.includes("No BrowsingContext found"),
+ "Expected default error message found"
+ );
+ }
+
+ unregisterCommandsActor();
+});
+
+add_task(function test_eventsActor_enable_disable() {
+ enableEventsActor();
+ disableEventsActor();
+
+ enableEventsActor();
+ enableEventsActor();
+ disableEventsActor();
+});
diff --git a/remote/marionette/test/xpcshell/test_browser.js b/remote/marionette/test/xpcshell/test_browser.js
new file mode 100644
index 0000000000..fdd83ba7e3
--- /dev/null
+++ b/remote/marionette/test/xpcshell/test_browser.js
@@ -0,0 +1,21 @@
+const { Context } = ChromeUtils.importESModule(
+ "chrome://remote/content/marionette/browser.sys.mjs"
+);
+
+add_task(function test_Context() {
+ ok(Context.hasOwnProperty("Chrome"));
+ ok(Context.hasOwnProperty("Content"));
+ equal(typeof Context.Chrome, "string");
+ equal(typeof Context.Content, "string");
+ equal(Context.Chrome, "chrome");
+ equal(Context.Content, "content");
+});
+
+add_task(function test_Context_fromString() {
+ equal(Context.fromString("chrome"), Context.Chrome);
+ equal(Context.fromString("content"), Context.Content);
+
+ for (let typ of ["", "foo", true, 42, [], {}, null, undefined]) {
+ Assert.throws(() => Context.fromString(typ), /TypeError/);
+ }
+});
diff --git a/remote/marionette/test/xpcshell/test_cookie.js b/remote/marionette/test/xpcshell/test_cookie.js
new file mode 100644
index 0000000000..b5ce5e9008
--- /dev/null
+++ b/remote/marionette/test/xpcshell/test_cookie.js
@@ -0,0 +1,362 @@
+/* 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/. */
+
+const { cookie } = ChromeUtils.importESModule(
+ "chrome://remote/content/marionette/cookie.sys.mjs"
+);
+
+/* eslint-disable mozilla/use-chromeutils-generateqi */
+
+cookie.manager = {
+ cookies: [],
+
+ add(
+ domain,
+ path,
+ name,
+ value,
+ secure,
+ httpOnly,
+ session,
+ expiry,
+ originAttributes,
+ sameSite
+ ) {
+ if (name === "fail") {
+ throw new Error("An error occurred while adding cookie");
+ }
+ let newCookie = {
+ host: domain,
+ path,
+ name,
+ value,
+ isSecure: secure,
+ isHttpOnly: httpOnly,
+ isSession: session,
+ expiry,
+ originAttributes,
+ sameSite,
+ };
+ cookie.manager.cookies.push(newCookie);
+ },
+
+ remove(host, name, path) {
+ for (let i = 0; i < this.cookies.length; ++i) {
+ let candidate = this.cookies[i];
+ if (
+ candidate.host === host &&
+ candidate.name === name &&
+ candidate.path === path
+ ) {
+ return this.cookies.splice(i, 1);
+ }
+ }
+ return false;
+ },
+
+ getCookiesFromHost(host) {
+ let hostCookies = this.cookies.filter(
+ c => c.host === host || c.host === "." + host
+ );
+
+ return hostCookies;
+ },
+};
+
+add_task(function test_fromJSON() {
+ // object
+ for (let invalidType of ["foo", 42, true, [], null, undefined]) {
+ Assert.throws(() => cookie.fromJSON(invalidType), /Expected cookie object/);
+ }
+
+ // name and value
+ for (let invalidType of [42, true, [], {}, null, undefined]) {
+ Assert.throws(
+ () => cookie.fromJSON({ name: invalidType }),
+ /Cookie name must be string/
+ );
+ Assert.throws(
+ () => cookie.fromJSON({ name: "foo", value: invalidType }),
+ /Cookie value must be string/
+ );
+ }
+
+ // domain
+ for (let invalidType of [42, true, [], {}, null]) {
+ let domainTest = {
+ name: "foo",
+ value: "bar",
+ domain: invalidType,
+ };
+ Assert.throws(
+ () => cookie.fromJSON(domainTest),
+ /Cookie domain must be string/
+ );
+ }
+ let domainTest = {
+ name: "foo",
+ value: "bar",
+ domain: "domain",
+ };
+ let parsedCookie = cookie.fromJSON(domainTest);
+ equal(parsedCookie.domain, "domain");
+
+ // path
+ for (let invalidType of [42, true, [], {}, null]) {
+ let pathTest = {
+ name: "foo",
+ value: "bar",
+ path: invalidType,
+ };
+ Assert.throws(
+ () => cookie.fromJSON(pathTest),
+ /Cookie path must be string/
+ );
+ }
+
+ // secure
+ for (let invalidType of ["foo", 42, [], {}, null]) {
+ let secureTest = {
+ name: "foo",
+ value: "bar",
+ secure: invalidType,
+ };
+ Assert.throws(
+ () => cookie.fromJSON(secureTest),
+ /Cookie secure flag must be boolean/
+ );
+ }
+
+ // httpOnly
+ for (let invalidType of ["foo", 42, [], {}, null]) {
+ let httpOnlyTest = {
+ name: "foo",
+ value: "bar",
+ httpOnly: invalidType,
+ };
+ Assert.throws(
+ () => cookie.fromJSON(httpOnlyTest),
+ /Cookie httpOnly flag must be boolean/
+ );
+ }
+
+ // expiry
+ for (let invalidType of [
+ -1,
+ Number.MAX_SAFE_INTEGER + 1,
+ "foo",
+ true,
+ [],
+ {},
+ null,
+ ]) {
+ let expiryTest = {
+ name: "foo",
+ value: "bar",
+ expiry: invalidType,
+ };
+ Assert.throws(
+ () => cookie.fromJSON(expiryTest),
+ /Cookie expiry must be a positive integer/
+ );
+ }
+
+ // sameSite
+ for (let invalidType of ["foo", 42, [], {}, null]) {
+ const sameSiteTest = {
+ name: "foo",
+ value: "bar",
+ sameSite: invalidType,
+ };
+ Assert.throws(
+ () => cookie.fromJSON(sameSiteTest),
+ /Cookie SameSite flag must be one of None, Lax, or Strict/
+ );
+ }
+
+ // bare requirements
+ let bare = cookie.fromJSON({ name: "name", value: "value" });
+ equal("name", bare.name);
+ equal("value", bare.value);
+ for (let missing of [
+ "path",
+ "secure",
+ "httpOnly",
+ "session",
+ "expiry",
+ "sameSite",
+ ]) {
+ ok(!bare.hasOwnProperty(missing));
+ }
+
+ // everything
+ let full = cookie.fromJSON({
+ name: "name",
+ value: "value",
+ domain: ".domain",
+ path: "path",
+ secure: true,
+ httpOnly: true,
+ expiry: 42,
+ sameSite: "Lax",
+ });
+ equal("name", full.name);
+ equal("value", full.value);
+ equal(".domain", full.domain);
+ equal("path", full.path);
+ equal(true, full.secure);
+ equal(true, full.httpOnly);
+ equal(42, full.expiry);
+ equal("Lax", full.sameSite);
+});
+
+add_task(function test_add() {
+ cookie.manager.cookies = [];
+
+ for (let invalidType of [42, true, [], {}, null, undefined]) {
+ Assert.throws(
+ () => cookie.add({ name: invalidType }),
+ /Cookie name must be string/
+ );
+ Assert.throws(
+ () => cookie.add({ name: "name", value: invalidType }),
+ /Cookie value must be string/
+ );
+ Assert.throws(
+ () => cookie.add({ name: "name", value: "value", domain: invalidType }),
+ /Cookie domain must be string/
+ );
+ }
+
+ cookie.add({
+ name: "name",
+ value: "value",
+ domain: "domain",
+ });
+ equal(1, cookie.manager.cookies.length);
+ equal("name", cookie.manager.cookies[0].name);
+ equal("value", cookie.manager.cookies[0].value);
+ equal(".domain", cookie.manager.cookies[0].host);
+ equal("/", cookie.manager.cookies[0].path);
+ ok(cookie.manager.cookies[0].expiry > new Date(Date.now()).getTime() / 1000);
+
+ cookie.add({
+ name: "name2",
+ value: "value2",
+ domain: "domain2",
+ });
+ equal(2, cookie.manager.cookies.length);
+
+ Assert.throws(() => {
+ let biscuit = { name: "name3", value: "value3", domain: "domain3" };
+ cookie.add(biscuit, { restrictToHost: "other domain" });
+ }, /Cookies may only be set for the current domain/);
+
+ cookie.add({
+ name: "name4",
+ value: "value4",
+ domain: "my.domain:1234",
+ });
+ equal(".my.domain", cookie.manager.cookies[2].host);
+
+ cookie.add({
+ name: "name5",
+ value: "value5",
+ domain: "domain5",
+ path: "/foo/bar",
+ });
+ equal("/foo/bar", cookie.manager.cookies[3].path);
+
+ cookie.add({
+ name: "name6",
+ value: "value",
+ domain: ".domain",
+ });
+ equal(".domain", cookie.manager.cookies[4].host);
+
+ const sameSiteMap = new Map([
+ ["None", Ci.nsICookie.SAMESITE_NONE],
+ ["Lax", Ci.nsICookie.SAMESITE_LAX],
+ ["Strict", Ci.nsICookie.SAMESITE_STRICT],
+ ]);
+
+ Array.from(sameSiteMap.keys()).forEach((entry, index) => {
+ cookie.add({
+ name: "name" + index,
+ value: "value",
+ domain: ".domain",
+ sameSite: entry,
+ });
+ equal(sameSiteMap.get(entry), cookie.manager.cookies[5 + index].sameSite);
+ });
+
+ Assert.throws(() => {
+ cookie.add({ name: "fail", value: "value6", domain: "domain6" });
+ }, /UnableToSetCookieError/);
+});
+
+add_task(function test_remove() {
+ cookie.manager.cookies = [];
+
+ let crumble = {
+ name: "test_remove",
+ value: "value",
+ domain: "domain",
+ path: "/custom/path",
+ };
+
+ equal(0, cookie.manager.cookies.length);
+ cookie.add(crumble);
+ equal(1, cookie.manager.cookies.length);
+
+ cookie.remove(crumble);
+ equal(0, cookie.manager.cookies.length);
+ equal(undefined, cookie.manager.cookies[0]);
+});
+
+add_task(function test_iter() {
+ cookie.manager.cookies = [];
+ let tomorrow = new Date();
+ tomorrow.setHours(tomorrow.getHours() + 24);
+
+ cookie.add({
+ expiry: tomorrow,
+ name: "0",
+ value: "",
+ domain: "foo.example.com",
+ });
+ cookie.add({
+ expiry: tomorrow,
+ name: "1",
+ value: "",
+ domain: "bar.example.com",
+ });
+
+ let fooCookies = [...cookie.iter("foo.example.com")];
+ equal(1, fooCookies.length);
+ equal(".foo.example.com", fooCookies[0].domain);
+ equal(true, fooCookies[0].hasOwnProperty("expiry"));
+
+ cookie.add({
+ name: "aSessionCookie",
+ value: "",
+ domain: "session.com",
+ });
+
+ let sessionCookies = [...cookie.iter("session.com")];
+ equal(1, sessionCookies.length);
+ equal("aSessionCookie", sessionCookies[0].name);
+ equal(false, sessionCookies[0].hasOwnProperty("expiry"));
+
+ cookie.add({
+ name: "2",
+ value: "",
+ domain: "samesite.example.com",
+ sameSite: "Lax",
+ });
+
+ let sameSiteCookies = [...cookie.iter("samesite.example.com")];
+ equal(1, sameSiteCookies.length);
+ equal("Lax", sameSiteCookies[0].sameSite);
+});
diff --git a/remote/marionette/test/xpcshell/test_element.js b/remote/marionette/test/xpcshell/test_element.js
new file mode 100644
index 0000000000..7b2379b1ab
--- /dev/null
+++ b/remote/marionette/test/xpcshell/test_element.js
@@ -0,0 +1,789 @@
+/* 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/. */
+
+const { element, ShadowRoot, WebElement, WebFrame, WebReference, WebWindow } =
+ ChromeUtils.importESModule(
+ "chrome://remote/content/marionette/element.sys.mjs"
+ );
+const { NodeCache } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/webdriver/NodeCache.sys.mjs"
+);
+
+const MemoryReporter = Cc["@mozilla.org/memory-reporter-manager;1"].getService(
+ Ci.nsIMemoryReporterManager
+);
+
+class MockElement {
+ constructor(tagName, attrs = {}) {
+ this.tagName = tagName;
+ this.localName = tagName;
+
+ this.isConnected = false;
+ this.ownerGlobal = {
+ document: {
+ isActive() {
+ return true;
+ },
+ },
+ };
+
+ for (let attr in attrs) {
+ this[attr] = attrs[attr];
+ }
+ }
+
+ get nodeType() {
+ return 1;
+ }
+
+ get ELEMENT_NODE() {
+ return 1;
+ }
+
+ // this is a severely limited CSS selector
+ // that only supports lists of tag names
+ matches(selector) {
+ let tags = selector.split(",");
+ return tags.includes(this.localName);
+ }
+}
+
+class MockXULElement extends MockElement {
+ constructor(tagName, attrs = {}) {
+ super(tagName, attrs);
+ this.namespaceURI = XUL_NS;
+
+ if (typeof this.ownerDocument == "undefined") {
+ this.ownerDocument = {};
+ }
+ if (typeof this.ownerDocument.documentElement == "undefined") {
+ this.ownerDocument.documentElement = { namespaceURI: XUL_NS };
+ }
+ }
+}
+
+const xulEl = new MockXULElement("text");
+
+const domElInPrivilegedDocument = new MockElement("input", {
+ nodePrincipal: { isSystemPrincipal: true },
+});
+const xulElInPrivilegedDocument = new MockXULElement("text", {
+ nodePrincipal: { isSystemPrincipal: true },
+});
+
+function setupTest() {
+ const browser = Services.appShell.createWindowlessBrowser(false);
+
+ browser.document.body.innerHTML = `
+ <div id="foo" style="margin: 50px">
+ <iframe></iframe>
+ <video></video>
+ <svg xmlns="http://www.w3.org/2000/svg"></svg>
+ <textarea></textarea>
+ </div>
+ `;
+
+ const divEl = browser.document.querySelector("div");
+ const svgEl = browser.document.querySelector("svg");
+ const textareaEl = browser.document.querySelector("textarea");
+ const videoEl = browser.document.querySelector("video");
+
+ const iframeEl = browser.document.querySelector("iframe");
+ const childEl = iframeEl.contentDocument.createElement("div");
+ iframeEl.contentDocument.body.appendChild(childEl);
+
+ const shadowRoot = videoEl.openOrClosedShadowRoot;
+
+ return {
+ browser,
+ nodeCache: new NodeCache(),
+ childEl,
+ divEl,
+ iframeEl,
+ shadowRoot,
+ svgEl,
+ textareaEl,
+ videoEl,
+ };
+}
+
+add_task(function test_findClosest() {
+ const { divEl, videoEl } = setupTest();
+
+ equal(element.findClosest(divEl, "foo"), null);
+ equal(element.findClosest(videoEl, "div"), divEl);
+});
+
+add_task(function test_isSelected() {
+ const { browser, divEl } = setupTest();
+
+ const checkbox = browser.document.createElement("input");
+ checkbox.setAttribute("type", "checkbox");
+
+ ok(!element.isSelected(checkbox));
+ checkbox.checked = true;
+ ok(element.isSelected(checkbox));
+
+ // selected is not a property of <input type=checkbox>
+ checkbox.selected = true;
+ checkbox.checked = false;
+ ok(!element.isSelected(checkbox));
+
+ const option = browser.document.createElement("option");
+
+ ok(!element.isSelected(option));
+ option.selected = true;
+ ok(element.isSelected(option));
+
+ // checked is not a property of <option>
+ option.checked = true;
+ option.selected = false;
+ ok(!element.isSelected(option));
+
+ // anything else should not be selected
+ for (const type of [undefined, null, "foo", true, [], {}, divEl]) {
+ ok(!element.isSelected(type));
+ }
+});
+
+add_task(function test_isElement() {
+ const { divEl, iframeEl, shadowRoot, svgEl } = setupTest();
+
+ ok(element.isElement(divEl));
+ ok(element.isElement(svgEl));
+ ok(element.isElement(xulEl));
+ ok(element.isElement(domElInPrivilegedDocument));
+ ok(element.isElement(xulElInPrivilegedDocument));
+
+ ok(!element.isElement(shadowRoot));
+ ok(!element.isElement(divEl.ownerGlobal));
+ ok(!element.isElement(iframeEl.contentWindow));
+
+ for (const type of [true, 42, {}, [], undefined, null]) {
+ ok(!element.isElement(type));
+ }
+});
+
+add_task(function test_isDOMElement() {
+ const { divEl, iframeEl, shadowRoot, svgEl } = setupTest();
+
+ ok(element.isDOMElement(divEl));
+ ok(element.isDOMElement(svgEl));
+ ok(element.isDOMElement(domElInPrivilegedDocument));
+
+ ok(!element.isDOMElement(shadowRoot));
+ ok(!element.isDOMElement(divEl.ownerGlobal));
+ ok(!element.isDOMElement(iframeEl.contentWindow));
+ ok(!element.isDOMElement(xulEl));
+ ok(!element.isDOMElement(xulElInPrivilegedDocument));
+
+ for (const type of [true, 42, "foo", {}, [], undefined, null]) {
+ ok(!element.isDOMElement(type));
+ }
+});
+
+add_task(function test_isXULElement() {
+ const { divEl, iframeEl, shadowRoot, svgEl } = setupTest();
+
+ ok(element.isXULElement(xulEl));
+ ok(element.isXULElement(xulElInPrivilegedDocument));
+
+ ok(!element.isXULElement(divEl));
+ ok(!element.isXULElement(domElInPrivilegedDocument));
+ ok(!element.isXULElement(svgEl));
+ ok(!element.isXULElement(shadowRoot));
+ ok(!element.isXULElement(divEl.ownerGlobal));
+ ok(!element.isXULElement(iframeEl.contentWindow));
+
+ for (const type of [true, 42, "foo", {}, [], undefined, null]) {
+ ok(!element.isXULElement(type));
+ }
+});
+
+add_task(function test_isDOMWindow() {
+ const { divEl, iframeEl, shadowRoot, svgEl } = setupTest();
+
+ ok(element.isDOMWindow(divEl.ownerGlobal));
+ ok(element.isDOMWindow(iframeEl.contentWindow));
+
+ ok(!element.isDOMWindow(divEl));
+ ok(!element.isDOMWindow(svgEl));
+ ok(!element.isDOMWindow(shadowRoot));
+ ok(!element.isDOMWindow(domElInPrivilegedDocument));
+ ok(!element.isDOMWindow(xulEl));
+ ok(!element.isDOMWindow(xulElInPrivilegedDocument));
+
+ for (const type of [true, 42, {}, [], undefined, null]) {
+ ok(!element.isDOMWindow(type));
+ }
+});
+
+add_task(function test_isShadowRoot() {
+ const { browser, divEl, iframeEl, shadowRoot, svgEl } = setupTest();
+
+ ok(element.isShadowRoot(shadowRoot));
+
+ ok(!element.isShadowRoot(divEl));
+ ok(!element.isShadowRoot(svgEl));
+ ok(!element.isShadowRoot(divEl.ownerGlobal));
+ ok(!element.isShadowRoot(iframeEl.contentWindow));
+ ok(!element.isShadowRoot(xulEl));
+ ok(!element.isShadowRoot(domElInPrivilegedDocument));
+ ok(!element.isShadowRoot(xulElInPrivilegedDocument));
+
+ for (const type of [true, 42, "foo", {}, [], undefined, null]) {
+ ok(!element.isShadowRoot(type));
+ }
+
+ const documentFragment = browser.document.createDocumentFragment();
+ ok(!element.isShadowRoot(documentFragment));
+});
+
+add_task(function test_isReadOnly() {
+ const { browser, divEl, textareaEl } = setupTest();
+
+ const input = browser.document.createElement("input");
+ input.readOnly = true;
+ ok(element.isReadOnly(input));
+
+ textareaEl.readOnly = true;
+ ok(element.isReadOnly(textareaEl));
+
+ ok(!element.isReadOnly(divEl));
+ divEl.readOnly = true;
+ ok(!element.isReadOnly(divEl));
+
+ ok(!element.isReadOnly(null));
+});
+
+add_task(function test_isDisabled() {
+ const { browser, divEl, svgEl } = setupTest();
+
+ const select = browser.document.createElement("select");
+ const option = browser.document.createElement("option");
+ select.appendChild(option);
+ select.disabled = true;
+ ok(element.isDisabled(option));
+
+ const optgroup = browser.document.createElement("optgroup");
+ option.parentNode = optgroup;
+ ok(element.isDisabled(option));
+
+ optgroup.parentNode = select;
+ ok(element.isDisabled(option));
+
+ select.disabled = false;
+ ok(!element.isDisabled(option));
+
+ for (const type of ["button", "input", "select", "textarea"]) {
+ const elem = browser.document.createElement(type);
+ ok(!element.isDisabled(elem));
+ elem.disabled = true;
+ ok(element.isDisabled(elem));
+ }
+
+ ok(!element.isDisabled(divEl));
+
+ svgEl.disabled = true;
+ ok(!element.isDisabled(svgEl));
+
+ ok(!element.isDisabled(new MockXULElement("browser", { disabled: true })));
+});
+
+add_task(function test_isEditingHost() {
+ const { browser, divEl, svgEl } = setupTest();
+
+ ok(!element.isEditingHost(null));
+
+ ok(!element.isEditingHost(divEl));
+ divEl.contentEditable = true;
+ ok(element.isEditingHost(divEl));
+
+ ok(!element.isEditingHost(svgEl));
+ browser.document.designMode = "on";
+ ok(element.isEditingHost(svgEl));
+});
+
+add_task(function test_isEditable() {
+ const { browser, divEl, svgEl, textareaEl } = setupTest();
+
+ ok(!element.isEditable(null));
+
+ for (let type of [
+ "checkbox",
+ "radio",
+ "hidden",
+ "submit",
+ "button",
+ "image",
+ ]) {
+ const input = browser.document.createElement("input");
+ input.setAttribute("type", type);
+
+ ok(!element.isEditable(input));
+ }
+
+ const input = browser.document.createElement("input");
+ ok(element.isEditable(input));
+ input.setAttribute("type", "text");
+ ok(element.isEditable(input));
+
+ ok(element.isEditable(textareaEl));
+
+ const textareaDisabled = browser.document.createElement("textarea");
+ textareaDisabled.disabled = true;
+ ok(!element.isEditable(textareaDisabled));
+
+ const textareaReadOnly = browser.document.createElement("textarea");
+ textareaReadOnly.readOnly = true;
+ ok(!element.isEditable(textareaReadOnly));
+
+ ok(!element.isEditable(divEl));
+ divEl.contentEditable = true;
+ ok(element.isEditable(divEl));
+
+ ok(!element.isEditable(svgEl));
+ browser.document.designMode = "on";
+ ok(element.isEditable(svgEl));
+});
+
+add_task(function test_isMutableFormControlElement() {
+ const { browser, divEl, textareaEl } = setupTest();
+
+ ok(!element.isMutableFormControl(null));
+
+ ok(element.isMutableFormControl(textareaEl));
+
+ const textareaDisabled = browser.document.createElement("textarea");
+ textareaDisabled.disabled = true;
+ ok(!element.isMutableFormControl(textareaDisabled));
+
+ const textareaReadOnly = browser.document.createElement("textarea");
+ textareaReadOnly.readOnly = true;
+ ok(!element.isMutableFormControl(textareaReadOnly));
+
+ const mutableStates = new Set([
+ "color",
+ "date",
+ "datetime-local",
+ "email",
+ "file",
+ "month",
+ "number",
+ "password",
+ "range",
+ "search",
+ "tel",
+ "text",
+ "url",
+ "week",
+ ]);
+ for (const type of mutableStates) {
+ const input = browser.document.createElement("input");
+ input.setAttribute("type", type);
+ ok(element.isMutableFormControl(input));
+ }
+
+ const inputHidden = browser.document.createElement("input");
+ inputHidden.setAttribute("type", "hidden");
+ ok(!element.isMutableFormControl(inputHidden));
+
+ ok(!element.isMutableFormControl(divEl));
+ divEl.contentEditable = true;
+ ok(!element.isMutableFormControl(divEl));
+ browser.document.designMode = "on";
+ ok(!element.isMutableFormControl(divEl));
+});
+
+add_task(function test_coordinates() {
+ const { divEl } = setupTest();
+
+ let coords = element.coordinates(divEl);
+ ok(coords.hasOwnProperty("x"));
+ ok(coords.hasOwnProperty("y"));
+ equal(typeof coords.x, "number");
+ equal(typeof coords.y, "number");
+
+ deepEqual(element.coordinates(divEl), { x: 0, y: 0 });
+ deepEqual(element.coordinates(divEl, 10, 10), { x: 10, y: 10 });
+ deepEqual(element.coordinates(divEl, -5, -5), { x: -5, y: -5 });
+
+ Assert.throws(() => element.coordinates(null), /node is null/);
+
+ Assert.throws(
+ () => element.coordinates(divEl, "string", undefined),
+ /Offset must be a number/
+ );
+ Assert.throws(
+ () => element.coordinates(divEl, undefined, "string"),
+ /Offset must be a number/
+ );
+ Assert.throws(
+ () => element.coordinates(divEl, "string", "string"),
+ /Offset must be a number/
+ );
+ Assert.throws(
+ () => element.coordinates(divEl, {}, undefined),
+ /Offset must be a number/
+ );
+ Assert.throws(
+ () => element.coordinates(divEl, undefined, {}),
+ /Offset must be a number/
+ );
+ Assert.throws(
+ () => element.coordinates(divEl, {}, {}),
+ /Offset must be a number/
+ );
+ Assert.throws(
+ () => element.coordinates(divEl, [], undefined),
+ /Offset must be a number/
+ );
+ Assert.throws(
+ () => element.coordinates(divEl, undefined, []),
+ /Offset must be a number/
+ );
+ Assert.throws(
+ () => element.coordinates(divEl, [], []),
+ /Offset must be a number/
+ );
+});
+
+add_task(function test_isNodeReferenceKnown() {
+ const { browser, nodeCache, childEl, iframeEl, videoEl } = setupTest();
+
+ // Unknown node reference
+ ok(!element.isNodeReferenceKnown(browser.browsingContext, "foo", nodeCache));
+
+ // Known node reference
+ const videoElRef = nodeCache.getOrCreateNodeReference(videoEl);
+ ok(
+ element.isNodeReferenceKnown(browser.browsingContext, videoElRef, nodeCache)
+ );
+
+ // Different top-level browsing context
+ const browser2 = Services.appShell.createWindowlessBrowser(false);
+ ok(
+ !element.isNodeReferenceKnown(
+ browser2.browsingContext,
+ videoElRef,
+ nodeCache
+ )
+ );
+
+ // Different child browsing context
+ const childElRef = nodeCache.getOrCreateNodeReference(childEl);
+ const childBrowsingContext = iframeEl.contentWindow.browsingContext;
+ ok(element.isNodeReferenceKnown(childBrowsingContext, childElRef, nodeCache));
+
+ const iframeEl2 = browser2.document.createElement("iframe");
+ browser2.document.body.appendChild(iframeEl2);
+ const childBrowsingContext2 = iframeEl2.contentWindow.browsingContext;
+ ok(
+ !element.isNodeReferenceKnown(childBrowsingContext2, childElRef, nodeCache)
+ );
+});
+
+add_task(function test_getKnownElement() {
+ const { browser, nodeCache, shadowRoot, videoEl } = setupTest();
+
+ // Unknown element reference
+ Assert.throws(() => {
+ element.getKnownElement(browser.browsingContext, "foo", nodeCache);
+ }, /NoSuchElementError/);
+
+ // With a ShadowRoot reference
+ const shadowRootRef = nodeCache.getOrCreateNodeReference(shadowRoot);
+ Assert.throws(() => {
+ element.getKnownElement(browser.browsingContext, shadowRootRef, nodeCache);
+ }, /NoSuchElementError/);
+
+ // Deleted element (eg. garbage collected)
+ let detachedEl = browser.document.createElement("div");
+ const detachedElRef = nodeCache.getOrCreateNodeReference(detachedEl);
+
+ // ... not connected to the DOM
+ Assert.throws(() => {
+ element.getKnownElement(browser.browsingContext, detachedElRef, nodeCache);
+ }, /StaleElementReferenceError/);
+
+ // ... element garbage collected
+ detachedEl = null;
+ MemoryReporter.minimizeMemoryUsage(() => {
+ Assert.throws(() => {
+ element.getKnownElement(
+ browser.browsingContext,
+ detachedElRef,
+ nodeCache
+ );
+ }, /StaleElementReferenceError/);
+ });
+
+ // Known element reference
+ const videoElRef = nodeCache.getOrCreateNodeReference(videoEl);
+ equal(
+ element.getKnownElement(browser.browsingContext, videoElRef, nodeCache),
+ videoEl
+ );
+});
+
+add_task(function test_getKnownShadowRoot() {
+ const { browser, nodeCache, shadowRoot, videoEl } = setupTest();
+
+ const videoElRef = nodeCache.getOrCreateNodeReference(videoEl);
+
+ // Unknown ShadowRoot reference
+ Assert.throws(() => {
+ element.getKnownShadowRoot(browser.browsingContext, "foo", nodeCache);
+ }, /NoSuchShadowRootError/);
+
+ // With a HTMLElement reference
+ Assert.throws(() => {
+ element.getKnownShadowRoot(browser.browsingContext, videoElRef, nodeCache);
+ }, /NoSuchShadowRootError/);
+
+ // Known ShadowRoot reference
+ const shadowRootRef = nodeCache.getOrCreateNodeReference(shadowRoot);
+ equal(
+ element.getKnownShadowRoot(
+ browser.browsingContext,
+ shadowRootRef,
+ nodeCache
+ ),
+ shadowRoot
+ );
+
+ // Detached ShadowRoot host
+ let el = browser.document.createElement("div");
+ let detachedShadowRoot = el.attachShadow({ mode: "open" });
+ detachedShadowRoot.innerHTML = "<input></input>";
+
+ const detachedShadowRootRef =
+ nodeCache.getOrCreateNodeReference(detachedShadowRoot);
+
+ // ... not connected to the DOM
+ Assert.throws(() => {
+ element.getKnownShadowRoot(
+ browser.browsingContext,
+ detachedShadowRootRef,
+ nodeCache
+ );
+ }, /DetachedShadowRootError/);
+
+ // ... host and shadow root garbage collected
+ el = null;
+ detachedShadowRoot = null;
+ MemoryReporter.minimizeMemoryUsage(() => {
+ Assert.throws(() => {
+ element.getKnownShadowRoot(
+ browser.browsingContext,
+ detachedShadowRootRef,
+ nodeCache
+ );
+ }, /DetachedShadowRootError/);
+ });
+});
+
+add_task(function test_isDetached() {
+ const { childEl, iframeEl } = setupTest();
+
+ let detachedShadowRoot = childEl.attachShadow({ mode: "open" });
+ detachedShadowRoot.innerHTML = "<input></input>";
+
+ // Connected to the DOM
+ ok(!element.isDetached(detachedShadowRoot));
+
+ // Node document (ownerDocument) is not the active document
+ iframeEl.remove();
+ ok(element.isDetached(detachedShadowRoot));
+
+ // host element is stale (eg. not connected)
+ detachedShadowRoot.host.remove();
+ equal(childEl.isConnected, false);
+ ok(element.isDetached(detachedShadowRoot));
+});
+
+add_task(function test_isStale() {
+ const { childEl, iframeEl } = setupTest();
+
+ // Connected to the DOM
+ ok(!element.isStale(childEl));
+
+ // Not part of the active document
+ iframeEl.remove();
+ ok(element.isStale(childEl));
+
+ // Not connected to the DOM
+ childEl.remove();
+ ok(element.isStale(childEl));
+});
+
+add_task(function test_WebReference_ctor() {
+ const el = new WebReference("foo");
+ equal(el.uuid, "foo");
+
+ for (let t of [42, true, [], {}, null, undefined]) {
+ Assert.throws(() => new WebReference(t), /to be a string/);
+ }
+});
+
+add_task(function test_WebElemenet_is() {
+ const a = new WebReference("a");
+ const b = new WebReference("b");
+
+ ok(a.is(a));
+ ok(b.is(b));
+ ok(!a.is(b));
+ ok(!b.is(a));
+
+ ok(!a.is({}));
+});
+
+add_task(function test_WebReference_from() {
+ const { divEl, iframeEl } = setupTest();
+
+ ok(WebReference.from(divEl) instanceof WebElement);
+ ok(WebReference.from(xulEl) instanceof WebElement);
+ ok(WebReference.from(divEl.ownerGlobal) instanceof WebWindow);
+ ok(WebReference.from(iframeEl.contentWindow) instanceof WebFrame);
+ ok(WebReference.from(domElInPrivilegedDocument) instanceof WebElement);
+ ok(WebReference.from(xulElInPrivilegedDocument) instanceof WebElement);
+
+ Assert.throws(() => WebReference.from({}), /InvalidArgumentError/);
+});
+
+add_task(function test_WebReference_fromJSON_WebElement() {
+ const { Identifier } = WebElement;
+
+ const ref = { [Identifier]: "foo" };
+ const webEl = WebReference.fromJSON(ref);
+ ok(webEl instanceof WebElement);
+ equal(webEl.uuid, "foo");
+
+ let identifierPrecedence = {
+ [Identifier]: "identifier-uuid",
+ };
+ const precedenceEl = WebReference.fromJSON(identifierPrecedence);
+ ok(precedenceEl instanceof WebElement);
+ equal(precedenceEl.uuid, "identifier-uuid");
+});
+
+add_task(function test_WebReference_fromJSON_WebWindow() {
+ const ref = { [WebWindow.Identifier]: "foo" };
+ const win = WebReference.fromJSON(ref);
+
+ ok(win instanceof WebWindow);
+ equal(win.uuid, "foo");
+});
+
+add_task(function test_WebReference_fromJSON_WebFrame() {
+ const ref = { [WebFrame.Identifier]: "foo" };
+ const frame = WebReference.fromJSON(ref);
+ ok(frame instanceof WebFrame);
+ equal(frame.uuid, "foo");
+});
+
+add_task(function test_WebReference_fromJSON_malformed() {
+ Assert.throws(() => WebReference.fromJSON({}), /InvalidArgumentError/);
+ Assert.throws(() => WebReference.fromJSON(null), /InvalidArgumentError/);
+});
+
+add_task(function test_WebReference_isReference() {
+ for (let t of [42, true, "foo", [], {}]) {
+ ok(!WebReference.isReference(t));
+ }
+
+ ok(WebReference.isReference({ [WebElement.Identifier]: "foo" }));
+ ok(WebReference.isReference({ [WebWindow.Identifier]: "foo" }));
+ ok(WebReference.isReference({ [WebFrame.Identifier]: "foo" }));
+});
+
+add_task(function test_WebElement_toJSON() {
+ const { Identifier } = WebElement;
+
+ const el = new WebElement("foo");
+ const json = el.toJSON();
+
+ ok(Identifier in json);
+ equal(json[Identifier], "foo");
+});
+
+add_task(function test_WebElement_fromJSON() {
+ const { Identifier } = WebElement;
+
+ const el = WebElement.fromJSON({ [Identifier]: "foo" });
+ ok(el instanceof WebElement);
+ equal(el.uuid, "foo");
+
+ Assert.throws(() => WebElement.fromJSON({}), /InvalidArgumentError/);
+});
+
+add_task(function test_WebElement_fromUUID() {
+ const domWebEl = WebElement.fromUUID("bar");
+
+ ok(domWebEl instanceof WebElement);
+ equal(domWebEl.uuid, "bar");
+
+ Assert.throws(() => WebElement.fromUUID(), /InvalidArgumentError/);
+});
+
+add_task(function test_ShadowRoot_toJSON() {
+ const { Identifier } = ShadowRoot;
+
+ const shadowRoot = new ShadowRoot("foo");
+ const json = shadowRoot.toJSON();
+
+ ok(Identifier in json);
+ equal(json[Identifier], "foo");
+});
+
+add_task(function test_ShadowRoot_fromJSON() {
+ const { Identifier } = ShadowRoot;
+
+ const shadowRoot = ShadowRoot.fromJSON({ [Identifier]: "foo" });
+ ok(shadowRoot instanceof ShadowRoot);
+ equal(shadowRoot.uuid, "foo");
+
+ Assert.throws(() => ShadowRoot.fromJSON({}), /InvalidArgumentError/);
+});
+
+add_task(function test_ShadowRoot_fromUUID() {
+ const shadowRoot = ShadowRoot.fromUUID("baz");
+
+ ok(shadowRoot instanceof ShadowRoot);
+ equal(shadowRoot.uuid, "baz");
+
+ Assert.throws(() => ShadowRoot.fromUUID(), /InvalidArgumentError/);
+});
+
+add_task(function test_WebWindow_toJSON() {
+ const win = new WebWindow("foo");
+ const json = win.toJSON();
+
+ ok(WebWindow.Identifier in json);
+ equal(json[WebWindow.Identifier], "foo");
+});
+
+add_task(function test_WebWindow_fromJSON() {
+ const ref = { [WebWindow.Identifier]: "foo" };
+ const win = WebWindow.fromJSON(ref);
+
+ ok(win instanceof WebWindow);
+ equal(win.uuid, "foo");
+});
+
+add_task(function test_WebFrame_toJSON() {
+ const frame = new WebFrame("foo");
+ const json = frame.toJSON();
+
+ ok(WebFrame.Identifier in json);
+ equal(json[WebFrame.Identifier], "foo");
+});
+
+add_task(function test_WebFrame_fromJSON() {
+ const ref = { [WebFrame.Identifier]: "foo" };
+ const win = WebFrame.fromJSON(ref);
+
+ ok(win instanceof WebFrame);
+ equal(win.uuid, "foo");
+});
diff --git a/remote/marionette/test/xpcshell/test_json.js b/remote/marionette/test/xpcshell/test_json.js
new file mode 100644
index 0000000000..479e1fe473
--- /dev/null
+++ b/remote/marionette/test/xpcshell/test_json.js
@@ -0,0 +1,275 @@
+const { json } = ChromeUtils.importESModule(
+ "chrome://remote/content/marionette/json.sys.mjs"
+);
+const { NodeCache } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/webdriver/NodeCache.sys.mjs"
+);
+const { ShadowRoot, WebElement, WebReference } = ChromeUtils.importESModule(
+ "chrome://remote/content/marionette/element.sys.mjs"
+);
+
+function setupTest() {
+ const browser = Services.appShell.createWindowlessBrowser(false);
+ const nodeCache = new NodeCache();
+
+ const htmlEl = browser.document.createElement("video");
+ browser.document.body.appendChild(htmlEl);
+
+ const svgEl = browser.document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "rect"
+ );
+ browser.document.body.appendChild(svgEl);
+
+ const shadowRoot = htmlEl.openOrClosedShadowRoot;
+
+ const iframeEl = browser.document.createElement("iframe");
+ browser.document.body.appendChild(iframeEl);
+ const childEl = iframeEl.contentDocument.createElement("div");
+
+ return { browser, nodeCache, childEl, iframeEl, htmlEl, shadowRoot, svgEl };
+}
+
+add_task(function test_clone_generalTypes() {
+ const { nodeCache } = setupTest();
+
+ // null
+ equal(json.clone(undefined, nodeCache), null);
+ equal(json.clone(null, nodeCache), null);
+
+ // primitives
+ equal(json.clone(true, nodeCache), true);
+ equal(json.clone(42, nodeCache), 42);
+ equal(json.clone("foo", nodeCache), "foo");
+
+ // toJSON
+ equal(
+ json.clone({
+ toJSON() {
+ return "foo";
+ },
+ }),
+ "foo"
+ );
+});
+
+add_task(function test_clone_ShadowRoot() {
+ const { nodeCache, shadowRoot } = setupTest();
+
+ const shadowRootRef = nodeCache.getOrCreateNodeReference(shadowRoot);
+ deepEqual(
+ json.clone(shadowRoot, nodeCache),
+ WebReference.from(shadowRoot, shadowRootRef).toJSON()
+ );
+});
+
+add_task(function test_clone_WebElement() {
+ const { htmlEl, nodeCache, svgEl } = setupTest();
+
+ const htmlElRef = nodeCache.getOrCreateNodeReference(htmlEl);
+ deepEqual(
+ json.clone(htmlEl, nodeCache),
+ WebReference.from(htmlEl, htmlElRef).toJSON()
+ );
+
+ // Check an element with a different namespace
+ const svgElRef = nodeCache.getOrCreateNodeReference(svgEl);
+ deepEqual(
+ json.clone(svgEl, nodeCache),
+ WebReference.from(svgEl, svgElRef).toJSON()
+ );
+});
+
+add_task(function test_clone_Sequences() {
+ const { htmlEl, nodeCache } = setupTest();
+
+ const htmlElRef = nodeCache.getOrCreateNodeReference(htmlEl);
+
+ const input = [
+ null,
+ true,
+ [],
+ htmlEl,
+ {
+ toJSON() {
+ return "foo";
+ },
+ },
+ { bar: "baz" },
+ ];
+
+ const actual = json.clone(input, nodeCache);
+
+ equal(actual[0], null);
+ equal(actual[1], true);
+ deepEqual(actual[2], []);
+ deepEqual(actual[3], { [WebElement.Identifier]: htmlElRef });
+ equal(actual[4], "foo");
+ deepEqual(actual[5], { bar: "baz" });
+});
+
+add_task(function test_clone_objects() {
+ const { htmlEl, nodeCache } = setupTest();
+
+ const htmlElRef = nodeCache.getOrCreateNodeReference(htmlEl);
+
+ const input = {
+ null: null,
+ boolean: true,
+ array: [42],
+ element: htmlEl,
+ toJSON: {
+ toJSON() {
+ return "foo";
+ },
+ },
+ object: { bar: "baz" },
+ };
+
+ const actual = json.clone(input, nodeCache);
+
+ equal(actual.null, null);
+ equal(actual.boolean, true);
+ deepEqual(actual.array, [42]);
+ deepEqual(actual.element, { [WebElement.Identifier]: htmlElRef });
+ equal(actual.toJSON, "foo");
+ deepEqual(actual.object, { bar: "baz" });
+});
+
+add_task(function test_clone_сyclicReference() {
+ const { nodeCache } = setupTest();
+
+ // object
+ Assert.throws(() => {
+ const obj = {};
+ obj.reference = obj;
+ json.clone(obj, nodeCache);
+ }, /JavaScriptError/);
+
+ // array
+ Assert.throws(() => {
+ const array = [];
+ array.push(array);
+ json.clone(array, nodeCache);
+ }, /JavaScriptError/);
+
+ // array in object
+ Assert.throws(() => {
+ const array = [];
+ array.push(array);
+ json.clone({ array }, nodeCache);
+ }, /JavaScriptError/);
+
+ // object in array
+ Assert.throws(() => {
+ const obj = {};
+ obj.reference = obj;
+ json.clone([obj], nodeCache);
+ }, /JavaScriptError/);
+});
+
+add_task(function test_deserialize_generalTypes() {
+ const { browser, nodeCache } = setupTest();
+ const win = browser.document.ownerGlobal;
+
+ // null
+ equal(json.deserialize(undefined, nodeCache, win), undefined);
+ equal(json.deserialize(null, nodeCache, win), null);
+
+ // primitives
+ equal(json.deserialize(true, nodeCache, win), true);
+ equal(json.deserialize(42, nodeCache, win), 42);
+ equal(json.deserialize("foo", nodeCache, win), "foo");
+});
+
+add_task(function test_deserialize_ShadowRoot() {
+ const { browser, nodeCache, shadowRoot } = setupTest();
+ const win = browser.document.ownerGlobal;
+
+ // Fails to resolve for unknown elements
+ const unknownShadowRootId = { [ShadowRoot.Identifier]: "foo" };
+ Assert.throws(() => {
+ json.deserialize(unknownShadowRootId, nodeCache, win);
+ }, /NoSuchShadowRootError/);
+
+ const shadowRootRef = nodeCache.getOrCreateNodeReference(shadowRoot);
+ const shadowRootEl = { [ShadowRoot.Identifier]: shadowRootRef };
+
+ // Fails to resolve for missing window reference
+ Assert.throws(() => json.deserialize(shadowRootEl, nodeCache), /TypeError/);
+
+ // Previously seen element is associated with original web element reference
+ const root = json.deserialize(shadowRootEl, nodeCache, win);
+ deepEqual(root, shadowRoot);
+ deepEqual(root, nodeCache.getNode(browser.browsingContext, shadowRootRef));
+});
+
+add_task(function test_deserialize_WebElement() {
+ const { browser, htmlEl, nodeCache } = setupTest();
+ const win = browser.document.ownerGlobal;
+
+ // Fails to resolve for unknown elements
+ const unknownWebElId = { [WebElement.Identifier]: "foo" };
+ Assert.throws(() => {
+ json.deserialize(unknownWebElId, nodeCache, win);
+ }, /NoSuchElementError/);
+
+ const htmlElRef = nodeCache.getOrCreateNodeReference(htmlEl);
+ const htmlWebEl = { [WebElement.Identifier]: htmlElRef };
+
+ // Fails to resolve for missing window reference
+ Assert.throws(() => json.deserialize(htmlWebEl, nodeCache), /TypeError/);
+
+ // Previously seen element is associated with original web element reference
+ const el = json.deserialize(htmlWebEl, nodeCache, win);
+ deepEqual(el, htmlEl);
+ deepEqual(el, nodeCache.getNode(browser.browsingContext, htmlElRef));
+});
+
+add_task(function test_deserialize_Sequences() {
+ const { browser, htmlEl, nodeCache } = setupTest();
+ const win = browser.document.ownerGlobal;
+
+ const htmlElRef = nodeCache.getOrCreateNodeReference(htmlEl);
+
+ const input = [
+ null,
+ true,
+ [42],
+ { [WebElement.Identifier]: htmlElRef },
+ { bar: "baz" },
+ ];
+
+ const actual = json.deserialize(input, nodeCache, win);
+
+ equal(actual[0], null);
+ equal(actual[1], true);
+ deepEqual(actual[2], [42]);
+ deepEqual(actual[3], htmlEl);
+ deepEqual(actual[4], { bar: "baz" });
+});
+
+add_task(function test_deserialize_objects() {
+ const { browser, htmlEl, nodeCache } = setupTest();
+ const win = browser.document.ownerGlobal;
+
+ const htmlElRef = nodeCache.getOrCreateNodeReference(htmlEl);
+
+ const input = {
+ null: null,
+ boolean: true,
+ array: [42],
+ element: { [WebElement.Identifier]: htmlElRef },
+ object: { bar: "baz" },
+ };
+
+ const actual = json.deserialize(input, nodeCache, win);
+
+ equal(actual.null, null);
+ equal(actual.boolean, true);
+ deepEqual(actual.array, [42]);
+ deepEqual(actual.element, htmlEl);
+ deepEqual(actual.object, { bar: "baz" });
+
+ nodeCache.clear({ all: true });
+});
diff --git a/remote/marionette/test/xpcshell/test_message.js b/remote/marionette/test/xpcshell/test_message.js
new file mode 100644
index 0000000000..9926aea191
--- /dev/null
+++ b/remote/marionette/test/xpcshell/test_message.js
@@ -0,0 +1,245 @@
+/* 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/. */
+
+const { error } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/webdriver/Errors.sys.mjs"
+);
+const { Command, Message, Response } = ChromeUtils.importESModule(
+ "chrome://remote/content/marionette/message.sys.mjs"
+);
+
+add_task(function test_Message_Origin() {
+ equal(0, Message.Origin.Client);
+ equal(1, Message.Origin.Server);
+});
+
+add_task(function test_Message_fromPacket() {
+ let cmd = new Command(4, "foo");
+ let resp = new Response(5, () => {});
+ resp.error = "foo";
+
+ ok(Message.fromPacket(cmd.toPacket()) instanceof Command);
+ ok(Message.fromPacket(resp.toPacket()) instanceof Response);
+ Assert.throws(
+ () => Message.fromPacket([3, 4, 5, 6]),
+ /Unrecognised message type in packet/
+ );
+});
+
+add_task(function test_Command() {
+ let cmd = new Command(42, "foo", { bar: "baz" });
+ equal(42, cmd.id);
+ equal("foo", cmd.name);
+ deepEqual({ bar: "baz" }, cmd.parameters);
+ equal(null, cmd.onerror);
+ equal(null, cmd.onresult);
+ equal(Message.Origin.Client, cmd.origin);
+ equal(false, cmd.sent);
+});
+
+add_task(function test_Command_onresponse() {
+ let onerrorOk = false;
+ let onresultOk = false;
+
+ let cmd = new Command(7, "foo");
+ cmd.onerror = () => (onerrorOk = true);
+ cmd.onresult = () => (onresultOk = true);
+
+ let errorResp = new Response(8, () => {});
+ errorResp.error = new error.WebDriverError("foo");
+
+ let bodyResp = new Response(9, () => {});
+ bodyResp.body = "bar";
+
+ cmd.onresponse(errorResp);
+ equal(true, onerrorOk);
+ equal(false, onresultOk);
+
+ cmd.onresponse(bodyResp);
+ equal(true, onresultOk);
+});
+
+add_task(function test_Command_ctor() {
+ let cmd = new Command(42, "bar", { bar: "baz" });
+ let msg = cmd.toPacket();
+
+ equal(Command.Type, msg[0]);
+ equal(cmd.id, msg[1]);
+ equal(cmd.name, msg[2]);
+ equal(cmd.parameters, msg[3]);
+});
+
+add_task(function test_Command_toString() {
+ let cmd = new Command(42, "foo", { bar: "baz" });
+ equal(JSON.stringify(cmd.toPacket()), cmd.toString());
+});
+
+add_task(function test_Command_fromPacket() {
+ let c1 = new Command(42, "foo", { bar: "baz" });
+
+ let msg = c1.toPacket();
+ let c2 = Command.fromPacket(msg);
+
+ equal(c1.id, c2.id);
+ equal(c1.name, c2.name);
+ equal(c1.parameters, c2.parameters);
+
+ Assert.throws(
+ () => Command.fromPacket([null, 2, "foo", {}]),
+ /InvalidArgumentError/
+ );
+ Assert.throws(
+ () => Command.fromPacket([1, 2, "foo", {}]),
+ /InvalidArgumentError/
+ );
+ Assert.throws(
+ () => Command.fromPacket([0, null, "foo", {}]),
+ /InvalidArgumentError/
+ );
+ Assert.throws(
+ () => Command.fromPacket([0, 2, null, {}]),
+ /InvalidArgumentError/
+ );
+ Assert.throws(
+ () => Command.fromPacket([0, 2, "foo", false]),
+ /InvalidArgumentError/
+ );
+
+ let nullParams = Command.fromPacket([0, 2, "foo", null]);
+ equal(
+ "[object Object]",
+ Object.prototype.toString.call(nullParams.parameters)
+ );
+});
+
+add_task(function test_Command_Type() {
+ equal(0, Command.Type);
+});
+
+add_task(function test_Response_ctor() {
+ let handler = () => {
+ throw new Error("foo");
+ };
+
+ let resp = new Response(42, handler);
+ equal(42, resp.id);
+ equal(null, resp.error);
+ ok("origin" in resp);
+ equal(Message.Origin.Server, resp.origin);
+ equal(false, resp.sent);
+ equal(handler, resp.respHandler_);
+});
+
+add_task(function test_Response_sendConditionally() {
+ let fired = false;
+ let resp = new Response(42, () => (fired = true));
+ resp.sendConditionally(() => false);
+ equal(false, resp.sent);
+ equal(false, fired);
+ resp.sendConditionally(() => true);
+ equal(true, resp.sent);
+ equal(true, fired);
+});
+
+add_task(function test_Response_send() {
+ let fired = false;
+ let resp = new Response(42, () => (fired = true));
+ resp.send();
+ equal(true, resp.sent);
+ equal(true, fired);
+});
+
+add_task(function test_Response_sendError_sent() {
+ let resp = new Response(42, r => equal(false, r.sent));
+ resp.sendError(new error.WebDriverError());
+ ok(resp.sent);
+ Assert.throws(() => resp.send(), /already been sent/);
+});
+
+add_task(function test_Response_sendError_body() {
+ let resp = new Response(42, r => equal(null, r.body));
+ resp.sendError(new error.WebDriverError());
+});
+
+add_task(function test_Response_sendError_errorSerialisation() {
+ let err1 = new error.WebDriverError();
+ let resp1 = new Response(42);
+ resp1.sendError(err1);
+ equal(err1.status, resp1.error.error);
+ deepEqual(err1.toJSON(), resp1.error);
+
+ let err2 = new error.InvalidArgumentError();
+ let resp2 = new Response(43);
+ resp2.sendError(err2);
+ equal(err2.status, resp2.error.error);
+ deepEqual(err2.toJSON(), resp2.error);
+});
+
+add_task(function test_Response_sendError_wrapInternalError() {
+ let err = new ReferenceError("foo");
+
+ // errors that originate from JavaScript (i.e. Marionette implementation
+ // issues) should be converted to UnknownError for transport
+ let resp = new Response(42, r => {
+ equal("unknown error", r.error.error);
+ equal(false, resp.sent);
+ });
+
+ // they should also throw after being sent
+ Assert.throws(() => resp.sendError(err), /foo/);
+ equal(true, resp.sent);
+});
+
+add_task(function test_Response_toPacket() {
+ let resp = new Response(42, () => {});
+ let msg = resp.toPacket();
+
+ equal(Response.Type, msg[0]);
+ equal(resp.id, msg[1]);
+ equal(resp.error, msg[2]);
+ equal(resp.body, msg[3]);
+});
+
+add_task(function test_Response_toString() {
+ let resp = new Response(42, () => {});
+ resp.error = "foo";
+ resp.body = "bar";
+
+ equal(JSON.stringify(resp.toPacket()), resp.toString());
+});
+
+add_task(function test_Response_fromPacket() {
+ let r1 = new Response(42, () => {});
+ r1.error = "foo";
+ r1.body = "bar";
+
+ let msg = r1.toPacket();
+ let r2 = Response.fromPacket(msg);
+
+ equal(r1.id, r2.id);
+ equal(r1.error, r2.error);
+ equal(r1.body, r2.body);
+
+ Assert.throws(
+ () => Response.fromPacket([null, 2, "foo", {}]),
+ /InvalidArgumentError/
+ );
+ Assert.throws(
+ () => Response.fromPacket([0, 2, "foo", {}]),
+ /InvalidArgumentError/
+ );
+ Assert.throws(
+ () => Response.fromPacket([1, null, "foo", {}]),
+ /InvalidArgumentError/
+ );
+ Assert.throws(
+ () => Response.fromPacket([1, 2, null, {}]),
+ /InvalidArgumentError/
+ );
+ Response.fromPacket([1, 2, "foo", null]);
+});
+
+add_task(function test_Response_Type() {
+ equal(1, Response.Type);
+});
diff --git a/remote/marionette/test/xpcshell/test_modal.js b/remote/marionette/test/xpcshell/test_modal.js
new file mode 100644
index 0000000000..c36811f7ec
--- /dev/null
+++ b/remote/marionette/test/xpcshell/test_modal.js
@@ -0,0 +1,113 @@
+/* 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 { modal } = ChromeUtils.importESModule(
+ "chrome://remote/content/marionette/modal.sys.mjs"
+);
+
+const chromeWindow = {};
+
+const mockModalDialog = {
+ docShell: {
+ chromeEventHandler: null,
+ },
+ opener: {
+ ownerGlobal: chromeWindow,
+ },
+ Dialog: {
+ args: {
+ modalType: Services.prompt.MODAL_TYPE_WINDOW,
+ },
+ },
+};
+
+const mockCurBrowser = {
+ window: chromeWindow,
+};
+
+add_task(function test_addCallback() {
+ let observer = new modal.DialogObserver(() => mockCurBrowser);
+ let cb1 = () => true;
+ let cb2 = () => false;
+
+ equal(observer.callbacks.size, 0);
+ observer.add(cb1);
+ equal(observer.callbacks.size, 1);
+ observer.add(cb1);
+ equal(observer.callbacks.size, 1);
+ observer.add(cb2);
+ equal(observer.callbacks.size, 2);
+});
+
+add_task(function test_removeCallback() {
+ let observer = new modal.DialogObserver(() => mockCurBrowser);
+ let cb1 = () => true;
+ let cb2 = () => false;
+
+ equal(observer.callbacks.size, 0);
+ observer.add(cb1);
+ observer.add(cb2);
+
+ equal(observer.callbacks.size, 2);
+ observer.remove(cb1);
+ equal(observer.callbacks.size, 1);
+ observer.remove(cb1);
+ equal(observer.callbacks.size, 1);
+ observer.remove(cb2);
+ equal(observer.callbacks.size, 0);
+});
+
+add_task(function test_registerDialogClosedEventHandler() {
+ let observer = new modal.DialogObserver(() => mockCurBrowser);
+ let mockChromeWindow = {
+ addEventListener(event, cb) {
+ equal(
+ event,
+ "DOMModalDialogClosed",
+ "registered event for closing modal"
+ );
+ equal(cb, observer, "set itself as handler");
+ },
+ };
+
+ observer.observe(mockChromeWindow, "domwindowopened");
+});
+
+add_task(function test_handleCallbackOpenModalDialog() {
+ let observer = new modal.DialogObserver(() => mockCurBrowser);
+
+ observer.add((action, dialog) => {
+ equal(action, modal.ACTION_OPENED, "'opened' action has been passed");
+ equal(dialog, mockModalDialog, "dialog has been passed");
+ });
+ observer.observe(mockModalDialog, "common-dialog-loaded");
+});
+
+add_task(function test_handleCallbackCloseModalDialog() {
+ let observer = new modal.DialogObserver(() => mockCurBrowser);
+
+ observer.add((action, dialog) => {
+ equal(action, modal.ACTION_CLOSED, "'closed' action has been passed");
+ equal(dialog, mockModalDialog, "dialog has been passed");
+ });
+ observer.handleEvent({
+ type: "DOMModalDialogClosed",
+ target: mockModalDialog,
+ });
+});
+
+add_task(async function test_dialogClosed() {
+ let observer = new modal.DialogObserver(() => mockCurBrowser);
+
+ const dialogClosed = observer.dialogClosed();
+
+ observer.handleEvent({
+ type: "DOMModalDialogClosed",
+ target: mockModalDialog,
+ });
+
+ await dialogClosed;
+});
diff --git a/remote/marionette/test/xpcshell/test_navigate.js b/remote/marionette/test/xpcshell/test_navigate.js
new file mode 100644
index 0000000000..9b5e2a1bc7
--- /dev/null
+++ b/remote/marionette/test/xpcshell/test_navigate.js
@@ -0,0 +1,90 @@
+/* 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/. */
+
+const { navigate } = ChromeUtils.importESModule(
+ "chrome://remote/content/marionette/navigate.sys.mjs"
+);
+
+const mockTopContext = {
+ get children() {
+ return [mockNestedContext];
+ },
+ id: 7,
+ get top() {
+ return this;
+ },
+};
+
+const mockNestedContext = {
+ id: 8,
+ parent: mockTopContext,
+ top: mockTopContext,
+};
+
+add_task(function test_isLoadEventExpectedForCurrent() {
+ Assert.throws(
+ () => navigate.isLoadEventExpected(undefined),
+ /Expected at least one URL/
+ );
+
+ ok(navigate.isLoadEventExpected(new URL("http://a/")));
+});
+
+add_task(function test_isLoadEventExpectedForFuture() {
+ const data = [
+ { current: "http://a/", future: undefined, expected: true },
+ { current: "http://a/", future: "http://a/", expected: true },
+ { current: "http://a/", future: "http://a/#", expected: true },
+ { current: "http://a/#", future: "http://a/", expected: true },
+ { current: "http://a/#a", future: "http://a/#A", expected: true },
+ { current: "http://a/#a", future: "http://a/#a", expected: false },
+ { current: "http://a/", future: "javascript:whatever", expected: false },
+ ];
+
+ for (const entry of data) {
+ const current = new URL(entry.current);
+ const future = entry.future ? new URL(entry.future) : undefined;
+ equal(navigate.isLoadEventExpected(current, { future }), entry.expected);
+ }
+});
+
+add_task(function test_isLoadEventExpectedForTarget() {
+ for (const target of ["_parent", "_top"]) {
+ Assert.throws(
+ () => navigate.isLoadEventExpected(new URL("http://a"), { target }),
+ /Expected browsingContext when target is _parent or _top/
+ );
+ }
+
+ const data = [
+ { cur: "http://a/", target: "", expected: true },
+ { cur: "http://a/", target: "_blank", expected: false },
+ { cur: "http://a/", target: "_parent", bc: mockTopContext, expected: true },
+ {
+ cur: "http://a/",
+ target: "_parent",
+ bc: mockNestedContext,
+ expected: false,
+ },
+ { cur: "http://a/", target: "_self", expected: true },
+ { cur: "http://a/", target: "_top", bc: mockTopContext, expected: true },
+ {
+ cur: "http://a/",
+ target: "_top",
+ bc: mockNestedContext,
+ expected: false,
+ },
+ ];
+
+ for (const entry of data) {
+ const current = entry.cur ? new URL(entry.cur) : undefined;
+ equal(
+ navigate.isLoadEventExpected(current, {
+ target: entry.target,
+ browsingContext: entry.bc,
+ }),
+ entry.expected
+ );
+ }
+});
diff --git a/remote/marionette/test/xpcshell/test_prefs.js b/remote/marionette/test/xpcshell/test_prefs.js
new file mode 100644
index 0000000000..ac3432544b
--- /dev/null
+++ b/remote/marionette/test/xpcshell/test_prefs.js
@@ -0,0 +1,98 @@
+/* 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 { Branch, EnvironmentPrefs, MarionettePrefs } =
+ ChromeUtils.importESModule(
+ "chrome://remote/content/marionette/prefs.sys.mjs"
+ );
+
+function reset() {
+ Services.prefs.setBoolPref("test.bool", false);
+ Services.prefs.setStringPref("test.string", "foo");
+ Services.prefs.setIntPref("test.int", 777);
+}
+
+// Give us something to work with:
+reset();
+
+add_task(function test_Branch_get_root() {
+ let root = new Branch(null);
+ equal(false, root.get("test.bool"));
+ equal("foo", root.get("test.string"));
+ equal(777, root.get("test.int"));
+ Assert.throws(() => root.get("doesnotexist"), /TypeError/);
+});
+
+add_task(function test_Branch_get_branch() {
+ let test = new Branch("test.");
+ equal(false, test.get("bool"));
+ equal("foo", test.get("string"));
+ equal(777, test.get("int"));
+ Assert.throws(() => test.get("doesnotexist"), /TypeError/);
+});
+
+add_task(function test_Branch_set_root() {
+ let root = new Branch(null);
+
+ try {
+ root.set("test.string", "bar");
+ root.set("test.in", 777);
+ root.set("test.bool", true);
+
+ equal("bar", Services.prefs.getStringPref("test.string"));
+ equal(777, Services.prefs.getIntPref("test.int"));
+ equal(true, Services.prefs.getBoolPref("test.bool"));
+ } finally {
+ reset();
+ }
+});
+
+add_task(function test_Branch_set_branch() {
+ let test = new Branch("test.");
+
+ try {
+ test.set("string", "bar");
+ test.set("int", 888);
+ test.set("bool", true);
+
+ equal("bar", Services.prefs.getStringPref("test.string"));
+ equal(888, Services.prefs.getIntPref("test.int"));
+ equal(true, Services.prefs.getBoolPref("test.bool"));
+ } finally {
+ reset();
+ }
+});
+
+add_task(function test_EnvironmentPrefs_from() {
+ let prefsTable = {
+ "test.bool": true,
+ "test.int": 888,
+ "test.string": "bar",
+ };
+ Services.env.set("FOO", JSON.stringify(prefsTable));
+
+ try {
+ for (let [key, value] of EnvironmentPrefs.from("FOO")) {
+ equal(prefsTable[key], value);
+ }
+ } finally {
+ Services.env.set("FOO", null);
+ }
+});
+
+add_task(function test_MarionettePrefs_getters() {
+ equal(false, MarionettePrefs.clickToStart);
+ equal(2828, MarionettePrefs.port);
+});
+
+add_task(function test_MarionettePrefs_setters() {
+ try {
+ MarionettePrefs.port = 777;
+ equal(777, MarionettePrefs.port);
+ } finally {
+ Services.prefs.clearUserPref("marionette.port");
+ }
+});
diff --git a/remote/marionette/test/xpcshell/test_sync.js b/remote/marionette/test/xpcshell/test_sync.js
new file mode 100644
index 0000000000..87ec44e960
--- /dev/null
+++ b/remote/marionette/test/xpcshell/test_sync.js
@@ -0,0 +1,419 @@
+/* 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/. */
+
+const {
+ DebounceCallback,
+ IdlePromise,
+ PollPromise,
+ Sleep,
+ TimedPromise,
+ waitForMessage,
+ waitForObserverTopic,
+} = ChromeUtils.importESModule(
+ "chrome://remote/content/marionette/sync.sys.mjs"
+);
+
+/**
+ * Mimic a message manager for sending messages.
+ */
+class MessageManager {
+ constructor() {
+ this.func = null;
+ this.message = null;
+ }
+
+ addMessageListener(message, func) {
+ this.func = func;
+ this.message = message;
+ }
+
+ removeMessageListener(message) {
+ this.func = null;
+ this.message = null;
+ }
+
+ send(message, data) {
+ if (this.func) {
+ this.func({
+ data,
+ message,
+ target: this,
+ });
+ }
+ }
+}
+
+/**
+ * Mimics nsITimer, but instead of using a system clock you can
+ * preprogram it to invoke the callback after a given number of ticks.
+ */
+class MockTimer {
+ constructor(ticksBeforeFiring) {
+ this.goal = ticksBeforeFiring;
+ this.ticks = 0;
+ this.cancelled = false;
+ }
+
+ initWithCallback(cb, timeout, type) {
+ this.ticks++;
+ if (this.ticks >= this.goal) {
+ cb();
+ }
+ }
+
+ cancel() {
+ this.cancelled = true;
+ }
+}
+
+add_task(function test_executeSoon_callback() {
+ // executeSoon() is already defined for xpcshell in head.js. As such import
+ // our implementation into a custom namespace.
+ let sync = ChromeUtils.importESModule(
+ "chrome://remote/content/marionette/sync.sys.mjs"
+ );
+
+ for (let func of ["foo", null, true, [], {}]) {
+ Assert.throws(() => sync.executeSoon(func), /TypeError/);
+ }
+
+ let a;
+ sync.executeSoon(() => {
+ a = 1;
+ });
+ executeSoon(() => equal(1, a));
+});
+
+add_task(function test_PollPromise_funcTypes() {
+ for (let type of ["foo", 42, null, undefined, true, [], {}]) {
+ Assert.throws(() => new PollPromise(type), /TypeError/);
+ }
+ new PollPromise(() => {});
+ new PollPromise(function () {});
+});
+
+add_task(function test_PollPromise_timeoutTypes() {
+ for (let timeout of ["foo", true, [], {}]) {
+ Assert.throws(() => new PollPromise(() => {}, { timeout }), /TypeError/);
+ }
+ for (let timeout of [1.2, -1]) {
+ Assert.throws(() => new PollPromise(() => {}, { timeout }), /RangeError/);
+ }
+ for (let timeout of [null, undefined, 42]) {
+ new PollPromise(resolve => resolve(1), { timeout });
+ }
+});
+
+add_task(function test_PollPromise_intervalTypes() {
+ for (let interval of ["foo", null, true, [], {}]) {
+ Assert.throws(() => new PollPromise(() => {}, { interval }), /TypeError/);
+ }
+ for (let interval of [1.2, -1]) {
+ Assert.throws(() => new PollPromise(() => {}, { interval }), /RangeError/);
+ }
+ new PollPromise(() => {}, { interval: 42 });
+});
+
+add_task(async function test_PollPromise_retvalTypes() {
+ for (let typ of [true, false, "foo", 42, [], {}]) {
+ strictEqual(typ, await new PollPromise(resolve => resolve(typ)));
+ }
+});
+
+add_task(async function test_PollPromise_rethrowError() {
+ let nevals = 0;
+ let err;
+ try {
+ await PollPromise(() => {
+ ++nevals;
+ throw new Error();
+ });
+ } catch (e) {
+ err = e;
+ }
+ equal(1, nevals);
+ ok(err instanceof Error);
+});
+
+add_task(async function test_PollPromise_noTimeout() {
+ let nevals = 0;
+ await new PollPromise((resolve, reject) => {
+ ++nevals;
+ nevals < 100 ? reject() : resolve();
+ });
+ equal(100, nevals);
+});
+
+add_task(async function test_PollPromise_zeroTimeout() {
+ // run at least once when timeout is 0
+ let nevals = 0;
+ let start = new Date().getTime();
+ await new PollPromise(
+ (resolve, reject) => {
+ ++nevals;
+ reject();
+ },
+ { timeout: 0 }
+ );
+ let end = new Date().getTime();
+ equal(1, nevals);
+ less(end - start, 500);
+});
+
+add_task(async function test_PollPromise_timeoutElapse() {
+ let nevals = 0;
+ let start = new Date().getTime();
+ await new PollPromise(
+ (resolve, reject) => {
+ ++nevals;
+ reject();
+ },
+ { timeout: 100 }
+ );
+ let end = new Date().getTime();
+ lessOrEqual(nevals, 11);
+ greaterOrEqual(end - start, 100);
+});
+
+add_task(async function test_PollPromise_interval() {
+ let nevals = 0;
+ await new PollPromise(
+ (resolve, reject) => {
+ ++nevals;
+ reject();
+ },
+ { timeout: 100, interval: 100 }
+ );
+ equal(2, nevals);
+});
+
+add_task(function test_TimedPromise_funcTypes() {
+ for (let type of ["foo", 42, null, undefined, true, [], {}]) {
+ Assert.throws(() => new TimedPromise(type), /TypeError/);
+ }
+ new TimedPromise(resolve => resolve());
+ new TimedPromise(function (resolve) {
+ resolve();
+ });
+});
+
+add_task(function test_TimedPromise_timeoutTypes() {
+ for (let timeout of ["foo", null, true, [], {}]) {
+ Assert.throws(
+ () => new TimedPromise(resolve => resolve(), { timeout }),
+ /TypeError/
+ );
+ }
+ for (let timeout of [1.2, -1]) {
+ Assert.throws(
+ () => new TimedPromise(resolve => resolve(), { timeout }),
+ /RangeError/
+ );
+ }
+ new TimedPromise(resolve => resolve(), { timeout: 42 });
+});
+
+add_task(async function test_TimedPromise_errorMessage() {
+ try {
+ await new TimedPromise(resolve => {}, { timeout: 0 });
+ ok(false, "Expected Timeout error not raised");
+ } catch (e) {
+ ok(
+ e.message.includes("TimedPromise timed out after"),
+ "Expected default error message found"
+ );
+ }
+
+ try {
+ await new TimedPromise(resolve => {}, {
+ errorMessage: "Not found",
+ timeout: 0,
+ });
+ ok(false, "Expected Timeout error not raised");
+ } catch (e) {
+ ok(
+ e.message.includes("Not found after"),
+ "Expected custom error message found"
+ );
+ }
+});
+
+add_task(async function test_Sleep() {
+ await Sleep(0);
+ for (let type of ["foo", true, null, undefined]) {
+ Assert.throws(() => new Sleep(type), /TypeError/);
+ }
+ Assert.throws(() => new Sleep(1.2), /RangeError/);
+ Assert.throws(() => new Sleep(-1), /RangeError/);
+});
+
+add_task(async function test_IdlePromise() {
+ let called = false;
+ let win = {
+ requestAnimationFrame(callback) {
+ called = true;
+ callback();
+ },
+ };
+ await IdlePromise(win);
+ ok(called);
+});
+
+add_task(async function test_IdlePromiseAbortWhenWindowClosed() {
+ let win = {
+ closed: true,
+ requestAnimationFrame() {},
+ };
+ await IdlePromise(win);
+});
+
+add_task(function test_DebounceCallback_constructor() {
+ for (let cb of [42, "foo", true, null, undefined, [], {}]) {
+ Assert.throws(() => new DebounceCallback(cb), /TypeError/);
+ }
+ for (let timeout of ["foo", true, [], {}, () => {}]) {
+ Assert.throws(
+ () => new DebounceCallback(() => {}, { timeout }),
+ /TypeError/
+ );
+ }
+ for (let timeout of [-1, 2.3, NaN]) {
+ Assert.throws(
+ () => new DebounceCallback(() => {}, { timeout }),
+ /RangeError/
+ );
+ }
+});
+
+add_task(async function test_DebounceCallback_repeatedCallback() {
+ let uniqueEvent = {};
+ let ncalls = 0;
+
+ let cb = ev => {
+ ncalls++;
+ equal(ev, uniqueEvent);
+ };
+ let debouncer = new DebounceCallback(cb);
+ debouncer.timer = new MockTimer(3);
+
+ // flood the debouncer with events,
+ // we only expect the last one to fire
+ debouncer.handleEvent(uniqueEvent);
+ debouncer.handleEvent(uniqueEvent);
+ debouncer.handleEvent(uniqueEvent);
+
+ equal(ncalls, 1);
+ ok(debouncer.timer.cancelled);
+});
+
+add_task(async function test_waitForMessage_messageManagerAndMessageTypes() {
+ let messageManager = new MessageManager();
+
+ for (let manager of ["foo", 42, null, undefined, true, [], {}]) {
+ Assert.throws(() => waitForMessage(manager, "message"), /TypeError/);
+ }
+
+ for (let message of [42, null, undefined, true, [], {}]) {
+ Assert.throws(() => waitForMessage(messageManager, message), /TypeError/);
+ }
+
+ let data = { foo: "bar" };
+ let sent = waitForMessage(messageManager, "message");
+ messageManager.send("message", data);
+ equal(data, await sent);
+});
+
+add_task(async function test_waitForMessage_checkFnTypes() {
+ let messageManager = new MessageManager();
+
+ for (let checkFn of ["foo", 42, true, [], {}]) {
+ Assert.throws(
+ () => waitForMessage(messageManager, "message", { checkFn }),
+ /TypeError/
+ );
+ }
+
+ let data1 = { fo: "bar" };
+ let data2 = { foo: "bar" };
+
+ for (let checkFn of [null, undefined, msg => "foo" in msg.data]) {
+ let expected_data = checkFn == null ? data1 : data2;
+
+ messageManager = new MessageManager();
+ let sent = waitForMessage(messageManager, "message", { checkFn });
+ messageManager.send("message", data1);
+ messageManager.send("message", data2);
+ equal(expected_data, await sent);
+ }
+});
+
+add_task(async function test_waitForObserverTopic_topicTypes() {
+ for (let topic of [42, null, undefined, true, [], {}]) {
+ Assert.throws(() => waitForObserverTopic(topic), /TypeError/);
+ }
+
+ let data = { foo: "bar" };
+ let sent = waitForObserverTopic("message");
+ Services.obs.notifyObservers(this, "message", data);
+ let result = await sent;
+ equal(this, result.subject);
+ equal(data, result.data);
+});
+
+add_task(async function test_waitForObserverTopic_checkFnTypes() {
+ for (let checkFn of ["foo", 42, true, [], {}]) {
+ Assert.throws(
+ () => waitForObserverTopic("message", { checkFn }),
+ /TypeError/
+ );
+ }
+
+ let data1 = { fo: "bar" };
+ let data2 = { foo: "bar" };
+
+ for (let checkFn of [null, undefined, (subject, data) => data == data2]) {
+ let expected_data = checkFn == null ? data1 : data2;
+
+ let sent = waitForObserverTopic("message");
+ Services.obs.notifyObservers(this, "message", data1);
+ Services.obs.notifyObservers(this, "message", data2);
+ let result = await sent;
+ equal(expected_data, result.data);
+ }
+});
+
+add_task(async function test_waitForObserverTopic_timeoutTypes() {
+ for (let timeout of ["foo", true, [], {}]) {
+ Assert.throws(
+ () => waitForObserverTopic("message", { timeout }),
+ /TypeError/
+ );
+ }
+ for (let timeout of [1.2, -1]) {
+ Assert.throws(
+ () => waitForObserverTopic("message", { timeout }),
+ /RangeError/
+ );
+ }
+ for (let timeout of [null, undefined, 42]) {
+ let data = { foo: "bar" };
+ let sent = waitForObserverTopic("message", { timeout });
+ Services.obs.notifyObservers(this, "message", data);
+ let result = await sent;
+ equal(this, result.subject);
+ equal(data, result.data);
+ }
+});
+
+add_task(async function test_waitForObserverTopic_timeoutElapse() {
+ try {
+ await waitForObserverTopic("message", { timeout: 0 });
+ ok(false, "Expected Timeout error not raised");
+ } catch (e) {
+ ok(
+ e.message.includes("waitForObserverTopic timed out after"),
+ "Expected error received"
+ );
+ }
+});
diff --git a/remote/marionette/test/xpcshell/xpcshell.ini b/remote/marionette/test/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..262d57ca39
--- /dev/null
+++ b/remote/marionette/test/xpcshell/xpcshell.ini
@@ -0,0 +1,18 @@
+# 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/.
+
+[DEFAULT]
+head = head.js
+skip-if = appname == "thunderbird"
+
+[test_actors.js]
+[test_browser.js]
+[test_cookie.js]
+[test_element.js]
+[test_json.js]
+[test_message.js]
+[test_modal.js]
+[test_navigate.js]
+[test_prefs.js]
+[test_sync.js]