summaryrefslogtreecommitdiffstats
path: root/remote/marionette/test
diff options
context:
space:
mode:
Diffstat (limited to 'remote/marionette/test')
-rw-r--r--remote/marionette/test/README1
-rw-r--r--remote/marionette/test/xpcshell/.eslintrc.js7
-rw-r--r--remote/marionette/test/xpcshell/README16
-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_json.js472
-rw-r--r--remote/marionette/test/xpcshell/test_message.js245
-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/test_web-reference.js293
-rw-r--r--remote/marionette/test/xpcshell/xpcshell.toml20
13 files changed, 2099 insertions, 0 deletions
diff --git a/remote/marionette/test/README b/remote/marionette/test/README
new file mode 100644
index 0000000000..9305b92cab
--- /dev/null
+++ b/remote/marionette/test/README
@@ -0,0 +1 @@
+See ../doc/Testing.md \ No newline at end of file
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/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_json.js b/remote/marionette/test/xpcshell/test_json.js
new file mode 100644
index 0000000000..f606681a8e
--- /dev/null
+++ b/remote/marionette/test/xpcshell/test_json.js
@@ -0,0 +1,472 @@
+const { json, getKnownElement, getKnownShadowRoot } =
+ 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/web-reference.sys.mjs"
+);
+
+const MemoryReporter = Cc["@mozilla.org/memory-reporter-manager;1"].getService(
+ Ci.nsIMemoryReporterManager
+);
+
+function setupTest() {
+ const browser = Services.appShell.createWindowlessBrowser(false);
+ const nodeCache = new NodeCache();
+
+ const videoEl = browser.document.createElement("video");
+ browser.document.body.appendChild(videoEl);
+
+ const svgEl = browser.document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "rect"
+ );
+ browser.document.body.appendChild(svgEl);
+
+ const shadowRoot = videoEl.openOrClosedShadowRoot;
+
+ const iframeEl = browser.document.createElement("iframe");
+ browser.document.body.appendChild(iframeEl);
+ const childEl = iframeEl.contentDocument.createElement("div");
+
+ return {
+ browser,
+ browsingContext: browser.browsingContext,
+ nodeCache,
+ childEl,
+ iframeEl,
+ seenNodeIds: new Map(),
+ shadowRoot,
+ svgEl,
+ videoEl,
+ };
+}
+
+function assert_cloned_value(value, clonedValue, nodeCache, seenNodes = []) {
+ const { seenNodeIds, serializedValue } = json.clone(value, nodeCache);
+
+ deepEqual(serializedValue, clonedValue);
+ deepEqual([...seenNodeIds.values()], seenNodes);
+}
+
+add_task(function test_clone_generalTypes() {
+ const { nodeCache } = setupTest();
+
+ // null
+ assert_cloned_value(undefined, null, nodeCache);
+ assert_cloned_value(null, null, nodeCache);
+
+ // primitives
+ assert_cloned_value(true, true, nodeCache);
+ assert_cloned_value(42, 42, nodeCache);
+ assert_cloned_value("foo", "foo", nodeCache);
+
+ // toJSON
+ assert_cloned_value(
+ {
+ toJSON() {
+ return "foo";
+ },
+ },
+ "foo",
+ nodeCache
+ );
+});
+
+add_task(function test_clone_ShadowRoot() {
+ const { nodeCache, seenNodeIds, shadowRoot } = setupTest();
+
+ const shadowRootRef = nodeCache.getOrCreateNodeReference(
+ shadowRoot,
+ seenNodeIds
+ );
+ assert_cloned_value(
+ shadowRoot,
+ WebReference.from(shadowRoot, shadowRootRef).toJSON(),
+ nodeCache,
+ seenNodeIds
+ );
+});
+
+add_task(function test_clone_WebElement() {
+ const { videoEl, nodeCache, seenNodeIds, svgEl } = setupTest();
+
+ const videoElRef = nodeCache.getOrCreateNodeReference(videoEl, seenNodeIds);
+ assert_cloned_value(
+ videoEl,
+ WebReference.from(videoEl, videoElRef).toJSON(),
+ nodeCache,
+ seenNodeIds
+ );
+
+ // Check an element with a different namespace
+ const svgElRef = nodeCache.getOrCreateNodeReference(svgEl, seenNodeIds);
+ assert_cloned_value(
+ svgEl,
+ WebReference.from(svgEl, svgElRef).toJSON(),
+ nodeCache,
+ seenNodeIds
+ );
+});
+
+add_task(function test_clone_Sequences() {
+ const { videoEl, nodeCache, seenNodeIds } = setupTest();
+
+ const videoElRef = nodeCache.getOrCreateNodeReference(videoEl, seenNodeIds);
+
+ const input = [
+ null,
+ true,
+ [42],
+ videoEl,
+ {
+ toJSON() {
+ return "foo";
+ },
+ },
+ { bar: "baz" },
+ ];
+
+ assert_cloned_value(
+ input,
+ [
+ null,
+ true,
+ [42],
+ { [WebElement.Identifier]: videoElRef },
+ "foo",
+ { bar: "baz" },
+ ],
+ nodeCache,
+ seenNodeIds
+ );
+});
+
+add_task(function test_clone_objects() {
+ const { videoEl, nodeCache, seenNodeIds } = setupTest();
+
+ const videoElRef = nodeCache.getOrCreateNodeReference(videoEl, seenNodeIds);
+
+ const input = {
+ null: null,
+ boolean: true,
+ array: [42],
+ element: videoEl,
+ toJSON: {
+ toJSON() {
+ return "foo";
+ },
+ },
+ object: { bar: "baz" },
+ };
+
+ assert_cloned_value(
+ input,
+ {
+ null: null,
+ boolean: true,
+ array: [42],
+ element: { [WebElement.Identifier]: videoElRef },
+ toJSON: "foo",
+ object: { bar: "baz" },
+ },
+ nodeCache,
+ seenNodeIds
+ );
+});
+
+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 { browsingContext, nodeCache } = setupTest();
+
+ // null
+ equal(json.deserialize(undefined, nodeCache, browsingContext), undefined);
+ equal(json.deserialize(null, nodeCache, browsingContext), null);
+
+ // primitives
+ equal(json.deserialize(true, nodeCache, browsingContext), true);
+ equal(json.deserialize(42, nodeCache, browsingContext), 42);
+ equal(json.deserialize("foo", nodeCache, browsingContext), "foo");
+});
+
+add_task(function test_deserialize_ShadowRoot() {
+ const { browsingContext, nodeCache, seenNodeIds, shadowRoot } = setupTest();
+ const seenNodes = new Set();
+
+ // Fails to resolve for unknown elements
+ const unknownShadowRootId = { [ShadowRoot.Identifier]: "foo" };
+ Assert.throws(() => {
+ json.deserialize(
+ unknownShadowRootId,
+ nodeCache,
+ browsingContext,
+ seenNodes
+ );
+ }, /NoSuchShadowRootError/);
+
+ const shadowRootRef = nodeCache.getOrCreateNodeReference(
+ shadowRoot,
+ seenNodeIds
+ );
+ 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
+ seenNodes.add(shadowRootRef);
+ const root = json.deserialize(
+ shadowRootEl,
+ nodeCache,
+ browsingContext,
+ seenNodes
+ );
+ deepEqual(root, shadowRoot);
+ deepEqual(root, nodeCache.getNode(browsingContext, shadowRootRef));
+});
+
+add_task(function test_deserialize_WebElement() {
+ const { browser, browsingContext, videoEl, nodeCache, seenNodeIds } =
+ setupTest();
+ const seenNodes = new Set();
+
+ // Fails to resolve for unknown elements
+ const unknownWebElId = { [WebElement.Identifier]: "foo" };
+ Assert.throws(() => {
+ json.deserialize(unknownWebElId, nodeCache, browsingContext, seenNodes);
+ }, /NoSuchElementError/);
+
+ const videoElRef = nodeCache.getOrCreateNodeReference(videoEl, seenNodeIds);
+ const htmlWebEl = { [WebElement.Identifier]: videoElRef };
+
+ // Fails to resolve for missing window reference
+ Assert.throws(() => json.deserialize(htmlWebEl, nodeCache), /TypeError/);
+
+ // Previously seen element is associated with original web element reference
+ seenNodes.add(videoElRef);
+ const el = json.deserialize(htmlWebEl, nodeCache, browsingContext, seenNodes);
+ deepEqual(el, videoEl);
+ deepEqual(el, nodeCache.getNode(browser.browsingContext, videoElRef));
+});
+
+add_task(function test_deserialize_Sequences() {
+ const { browsingContext, videoEl, nodeCache, seenNodeIds } = setupTest();
+ const seenNodes = new Set();
+
+ const videoElRef = nodeCache.getOrCreateNodeReference(videoEl, seenNodeIds);
+ seenNodes.add(videoElRef);
+
+ const input = [
+ null,
+ true,
+ [42],
+ { [WebElement.Identifier]: videoElRef },
+ { bar: "baz" },
+ ];
+
+ const actual = json.deserialize(input, nodeCache, browsingContext, seenNodes);
+
+ equal(actual[0], null);
+ equal(actual[1], true);
+ deepEqual(actual[2], [42]);
+ deepEqual(actual[3], videoEl);
+ deepEqual(actual[4], { bar: "baz" });
+});
+
+add_task(function test_deserialize_objects() {
+ const { browsingContext, videoEl, nodeCache, seenNodeIds } = setupTest();
+ const seenNodes = new Set();
+
+ const videoElRef = nodeCache.getOrCreateNodeReference(videoEl, seenNodeIds);
+ seenNodes.add(videoElRef);
+
+ const input = {
+ null: null,
+ boolean: true,
+ array: [42],
+ element: { [WebElement.Identifier]: videoElRef },
+ object: { bar: "baz" },
+ };
+
+ const actual = json.deserialize(input, nodeCache, browsingContext, seenNodes);
+
+ equal(actual.null, null);
+ equal(actual.boolean, true);
+ deepEqual(actual.array, [42]);
+ deepEqual(actual.element, videoEl);
+ deepEqual(actual.object, { bar: "baz" });
+
+ nodeCache.clear({ all: true });
+});
+
+add_task(async function test_getKnownElement() {
+ const { browser, nodeCache, seenNodeIds, shadowRoot, videoEl } = setupTest();
+ const seenNodes = new Set();
+
+ // Unknown element reference
+ Assert.throws(() => {
+ getKnownElement(browser.browsingContext, "foo", nodeCache, seenNodes);
+ }, /NoSuchElementError/);
+
+ // With a ShadowRoot reference
+ const shadowRootRef = nodeCache.getOrCreateNodeReference(
+ shadowRoot,
+ seenNodeIds
+ );
+ seenNodes.add(shadowRootRef);
+
+ Assert.throws(() => {
+ getKnownElement(
+ browser.browsingContext,
+ shadowRootRef,
+ nodeCache,
+ seenNodes
+ );
+ }, /NoSuchElementError/);
+
+ let detachedEl = browser.document.createElement("div");
+ const detachedElRef = nodeCache.getOrCreateNodeReference(
+ detachedEl,
+ seenNodeIds
+ );
+ seenNodes.add(detachedElRef);
+
+ // Element not connected to the DOM
+ Assert.throws(() => {
+ getKnownElement(
+ browser.browsingContext,
+ detachedElRef,
+ nodeCache,
+ seenNodes
+ );
+ }, /StaleElementReferenceError/);
+
+ // Element garbage collected
+ detachedEl = null;
+
+ await new Promise(resolve => MemoryReporter.minimizeMemoryUsage(resolve));
+ Assert.throws(() => {
+ getKnownElement(
+ browser.browsingContext,
+ detachedElRef,
+ nodeCache,
+ seenNodes
+ );
+ }, /StaleElementReferenceError/);
+
+ // Known element reference
+ const videoElRef = nodeCache.getOrCreateNodeReference(videoEl, seenNodeIds);
+ seenNodes.add(videoElRef);
+
+ equal(
+ getKnownElement(browser.browsingContext, videoElRef, nodeCache, seenNodes),
+ videoEl
+ );
+});
+
+add_task(async function test_getKnownShadowRoot() {
+ const { browser, nodeCache, seenNodeIds, shadowRoot, videoEl } = setupTest();
+ const seenNodes = new Set();
+
+ const videoElRef = nodeCache.getOrCreateNodeReference(videoEl, seenNodeIds);
+ seenNodes.add(videoElRef);
+
+ // Unknown ShadowRoot reference
+ Assert.throws(() => {
+ getKnownShadowRoot(browser.browsingContext, "foo", nodeCache, seenNodes);
+ }, /NoSuchShadowRootError/);
+
+ // With a videoElement reference
+ Assert.throws(() => {
+ getKnownShadowRoot(
+ browser.browsingContext,
+ videoElRef,
+ nodeCache,
+ seenNodes
+ );
+ }, /NoSuchShadowRootError/);
+
+ // Known ShadowRoot reference
+ const shadowRootRef = nodeCache.getOrCreateNodeReference(
+ shadowRoot,
+ seenNodeIds
+ );
+ seenNodes.add(shadowRootRef);
+
+ equal(
+ getKnownShadowRoot(
+ browser.browsingContext,
+ shadowRootRef,
+ nodeCache,
+ seenNodes
+ ),
+ 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,
+ seenNodeIds
+ );
+ seenNodes.add(detachedShadowRootRef);
+
+ // ... not connected to the DOM
+ Assert.throws(() => {
+ getKnownShadowRoot(
+ browser.browsingContext,
+ detachedShadowRootRef,
+ nodeCache,
+ seenNodes
+ );
+ }, /DetachedShadowRootError/);
+
+ // ... host and shadow root garbage collected
+ el = null;
+ detachedShadowRoot = null;
+
+ await new Promise(resolve => MemoryReporter.minimizeMemoryUsage(resolve));
+ Assert.throws(() => {
+ getKnownShadowRoot(
+ browser.browsingContext,
+ detachedShadowRootRef,
+ nodeCache,
+ seenNodes
+ );
+ }, /DetachedShadowRootError/);
+});
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_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/test_web-reference.js b/remote/marionette/test/xpcshell/test_web-reference.js
new file mode 100644
index 0000000000..4884901ae5
--- /dev/null
+++ b/remote/marionette/test/xpcshell/test_web-reference.js
@@ -0,0 +1,293 @@
+/* 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 { ShadowRoot, WebElement, WebFrame, WebReference, WebWindow } =
+ ChromeUtils.importESModule(
+ "chrome://remote/content/marionette/web-reference.sys.mjs"
+ );
+const { NodeCache } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/webdriver/NodeCache.sys.mjs"
+);
+
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+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,
+ childEl,
+ divEl,
+ iframeEl,
+ nodeCache: new NodeCache(),
+ shadowRoot,
+ svgEl,
+ textareaEl,
+ videoEl,
+ };
+}
+
+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_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_malformed() {
+ Assert.throws(() => WebReference.fromJSON({}), /InvalidArgumentError/);
+ Assert.throws(() => WebReference.fromJSON(null), /InvalidArgumentError/);
+});
+
+add_task(function test_WebReference_fromJSON_ShadowRoot() {
+ const { Identifier } = ShadowRoot;
+
+ const ref = { [Identifier]: "foo" };
+ const shadowRootEl = WebReference.fromJSON(ref);
+ ok(shadowRootEl instanceof ShadowRoot);
+ equal(shadowRootEl.uuid, "foo");
+
+ let identifierPrecedence = {
+ [Identifier]: "identifier-uuid",
+ };
+ const precedenceShadowRoot = WebReference.fromJSON(identifierPrecedence);
+ ok(precedenceShadowRoot instanceof ShadowRoot);
+ equal(precedenceShadowRoot.uuid, "identifier-uuid");
+});
+
+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_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_WebWindow() {
+ const ref = { [WebWindow.Identifier]: "foo" };
+ const win = WebReference.fromJSON(ref);
+
+ ok(win instanceof WebWindow);
+ equal(win.uuid, "foo");
+});
+
+add_task(function test_WebReference_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_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_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_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_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_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_WebFrame_fromJSON() {
+ const ref = { [WebFrame.Identifier]: "foo" };
+ const win = WebFrame.fromJSON(ref);
+
+ ok(win instanceof WebFrame);
+ 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_WebWindow_fromJSON() {
+ const ref = { [WebWindow.Identifier]: "foo" };
+ const win = WebWindow.fromJSON(ref);
+
+ ok(win instanceof WebWindow);
+ equal(win.uuid, "foo");
+});
+
+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");
+});
diff --git a/remote/marionette/test/xpcshell/xpcshell.toml b/remote/marionette/test/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..46ee8581fd
--- /dev/null
+++ b/remote/marionette/test/xpcshell/xpcshell.toml
@@ -0,0 +1,20 @@
+[DEFAULT]
+skip-if = ["appname == 'thunderbird'"]
+
+["test_actors.js"]
+
+["test_browser.js"]
+
+["test_cookie.js"]
+
+["test_json.js"]
+
+["test_message.js"]
+
+["test_navigate.js"]
+
+["test_prefs.js"]
+
+["test_sync.js"]
+
+["test_web-reference.js"]