summaryrefslogtreecommitdiffstats
path: root/remote/shared/test/xpcshell
diff options
context:
space:
mode:
Diffstat (limited to 'remote/shared/test/xpcshell')
-rw-r--r--remote/shared/test/xpcshell/head.js3
-rw-r--r--remote/shared/test/xpcshell/test_AppInfo.js53
-rw-r--r--remote/shared/test/xpcshell/test_ChallengeHeaderParser.js140
-rw-r--r--remote/shared/test/xpcshell/test_DOM.js479
-rw-r--r--remote/shared/test/xpcshell/test_Format.js108
-rw-r--r--remote/shared/test/xpcshell/test_Navigate.js879
-rw-r--r--remote/shared/test/xpcshell/test_Realm.js116
-rw-r--r--remote/shared/test/xpcshell/test_RecommendedPreferences.js118
-rw-r--r--remote/shared/test/xpcshell/test_Stack.js120
-rw-r--r--remote/shared/test/xpcshell/test_Sync.js436
-rw-r--r--remote/shared/test/xpcshell/test_TabManager.js56
-rw-r--r--remote/shared/test/xpcshell/test_UUID.js21
-rw-r--r--remote/shared/test/xpcshell/xpcshell.toml24
13 files changed, 2553 insertions, 0 deletions
diff --git a/remote/shared/test/xpcshell/head.js b/remote/shared/test/xpcshell/head.js
new file mode 100644
index 0000000000..2e7cf578d3
--- /dev/null
+++ b/remote/shared/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/shared/test/xpcshell/test_AppInfo.js b/remote/shared/test/xpcshell/test_AppInfo.js
new file mode 100644
index 0000000000..9149564aa1
--- /dev/null
+++ b/remote/shared/test/xpcshell/test_AppInfo.js
@@ -0,0 +1,53 @@
+/* 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 { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+const { AppInfo, getTimeoutMultiplier } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/AppInfo.sys.mjs"
+);
+
+// Minimal xpcshell tests for AppInfo; Services.appinfo.* is not available
+
+add_task(function test_custom_properties() {
+ const properties = [
+ // platforms
+ "isAndroid",
+ "isLinux",
+ "isMac",
+ "isWindows",
+ // applications
+ "isFirefox",
+ "isThunderbird",
+ ];
+
+ for (const prop of properties) {
+ equal(
+ typeof AppInfo[prop],
+ "boolean",
+ `Custom property ${prop} has expected type`
+ );
+ }
+});
+
+add_task(function test_getTimeoutMultiplier() {
+ const message = "Timeout multiplier has expected value";
+ const timeoutMultiplier = getTimeoutMultiplier();
+
+ if (
+ AppConstants.DEBUG ||
+ AppConstants.MOZ_CODE_COVERAGE ||
+ AppConstants.ASAN
+ ) {
+ equal(timeoutMultiplier, 4, message);
+ } else if (AppConstants.TSAN) {
+ equal(timeoutMultiplier, 8, message);
+ } else {
+ equal(timeoutMultiplier, 1, message);
+ }
+});
diff --git a/remote/shared/test/xpcshell/test_ChallengeHeaderParser.js b/remote/shared/test/xpcshell/test_ChallengeHeaderParser.js
new file mode 100644
index 0000000000..fa624e9c20
--- /dev/null
+++ b/remote/shared/test/xpcshell/test_ChallengeHeaderParser.js
@@ -0,0 +1,140 @@
+/* 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 { parseChallengeHeader } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/ChallengeHeaderParser.sys.mjs"
+);
+
+add_task(async function test_single_scheme() {
+ const TEST_HEADERS = [
+ {
+ // double quotes
+ header: 'Basic realm="test"',
+ params: [{ name: "realm", value: "test" }],
+ },
+ {
+ // single quote
+ header: "Basic realm='test'",
+ params: [{ name: "realm", value: "test" }],
+ },
+ {
+ // multiline
+ header: `Basic
+ realm='test'`,
+ params: [{ name: "realm", value: "test" }],
+ },
+ {
+ // with additional parameter.
+ header: 'Basic realm="test", charset="UTF-8"',
+ params: [
+ { name: "realm", value: "test" },
+ { name: "charset", value: "UTF-8" },
+ ],
+ },
+ ];
+ for (const { header, params } of TEST_HEADERS) {
+ const challenges = parseChallengeHeader(header);
+ equal(challenges.length, 1);
+ equal(challenges[0].scheme, "Basic");
+ deepEqual(challenges[0].params, params);
+ }
+});
+
+add_task(async function test_realmless_scheme() {
+ const TEST_HEADERS = [
+ {
+ // no parameter
+ header: "Custom",
+ params: [],
+ },
+ {
+ // one non-realm parameter
+ header: "Custom charset='UTF-8'",
+ params: [{ name: "charset", value: "UTF-8" }],
+ },
+ ];
+
+ for (const { header, params } of TEST_HEADERS) {
+ const challenges = parseChallengeHeader(header);
+ equal(challenges.length, 1);
+ equal(challenges[0].scheme, "Custom");
+ deepEqual(challenges[0].params, params);
+ }
+});
+
+add_task(async function test_multiple_schemes() {
+ const TEST_HEADERS = [
+ {
+ header: 'Scheme1 realm="foo", Scheme2 realm="bar"',
+ params: [
+ [{ name: "realm", value: "foo" }],
+ [{ name: "realm", value: "bar" }],
+ ],
+ },
+ {
+ header: 'Scheme1 realm="foo", charset="UTF-8", Scheme2 realm="bar"',
+ params: [
+ [
+ { name: "realm", value: "foo" },
+ { name: "charset", value: "UTF-8" },
+ ],
+ [{ name: "realm", value: "bar" }],
+ ],
+ },
+ {
+ header: `Scheme1 realm="foo",
+ charset="UTF-8",
+ Scheme2 realm="bar"`,
+ params: [
+ [
+ { name: "realm", value: "foo" },
+ { name: "charset", value: "UTF-8" },
+ ],
+ [{ name: "realm", value: "bar" }],
+ ],
+ },
+ ];
+ for (const { header, params } of TEST_HEADERS) {
+ const challenges = parseChallengeHeader(header);
+ equal(challenges.length, 2);
+ equal(challenges[0].scheme, "Scheme1");
+ deepEqual(challenges[0].params, params[0]);
+ equal(challenges[1].scheme, "Scheme2");
+ deepEqual(challenges[1].params, params[1]);
+ }
+});
+
+add_task(async function test_digest_scheme() {
+ const header = `Digest
+ realm="http-auth@example.org",
+ qop="auth, auth-int",
+ algorithm=SHA-256,
+ nonce="7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v",
+ opaque="FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS"`;
+
+ const challenges = parseChallengeHeader(header);
+ equal(challenges.length, 1);
+ equal(challenges[0].scheme, "Digest");
+
+ // Note: we are not doing a deepEqual check here, because one of the params
+ // actually contains a `,` inside quotes for its value, which will not be
+ // handled properly by the current ChallengeHeaderParser. See Bug 1857847.
+ const realmParam = challenges[0].params.find(param => param.name === "realm");
+ ok(realmParam);
+ equal(realmParam.value, "http-auth@example.org");
+
+ // Once Bug 1857847 is addressed, this should start failing and can be
+ // switched to deepEqual.
+ notDeepEqual(
+ challenges[0].params,
+ [
+ { name: "realm", value: "http-auth@example.org" },
+ { name: "qop", value: "auth, auth-int" },
+ { name: "algorithm", value: "SHA-256" },
+ { name: "nonce", value: "7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v" },
+ { name: "opaque", value: "FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS" },
+ ],
+ "notDeepEqual should be changed to deepEqual when Bug 1857847 is fixed"
+ );
+});
diff --git a/remote/shared/test/xpcshell/test_DOM.js b/remote/shared/test/xpcshell/test_DOM.js
new file mode 100644
index 0000000000..19844659b9
--- /dev/null
+++ b/remote/shared/test/xpcshell/test_DOM.js
@@ -0,0 +1,479 @@
+/* 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 { dom } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/DOM.sys.mjs"
+);
+const { NodeCache } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/webdriver/NodeCache.sys.mjs"
+);
+
+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(dom.findClosest(divEl, "foo"), null);
+ equal(dom.findClosest(videoEl, "div"), divEl);
+});
+
+add_task(function test_isSelected() {
+ const { browser, divEl } = setupTest();
+
+ const checkbox = browser.document.createElement("input");
+ checkbox.setAttribute("type", "checkbox");
+
+ ok(!dom.isSelected(checkbox));
+ checkbox.checked = true;
+ ok(dom.isSelected(checkbox));
+
+ // selected is not a property of <input type=checkbox>
+ checkbox.selected = true;
+ checkbox.checked = false;
+ ok(!dom.isSelected(checkbox));
+
+ const option = browser.document.createElement("option");
+
+ ok(!dom.isSelected(option));
+ option.selected = true;
+ ok(dom.isSelected(option));
+
+ // checked is not a property of <option>
+ option.checked = true;
+ option.selected = false;
+ ok(!dom.isSelected(option));
+
+ // anything else should not be selected
+ for (const type of [undefined, null, "foo", true, [], {}, divEl]) {
+ ok(!dom.isSelected(type));
+ }
+});
+
+add_task(function test_isElement() {
+ const { divEl, iframeEl, shadowRoot, svgEl } = setupTest();
+
+ ok(dom.isElement(divEl));
+ ok(dom.isElement(svgEl));
+ ok(dom.isElement(xulEl));
+ ok(dom.isElement(domElInPrivilegedDocument));
+ ok(dom.isElement(xulElInPrivilegedDocument));
+
+ ok(!dom.isElement(shadowRoot));
+ ok(!dom.isElement(divEl.ownerGlobal));
+ ok(!dom.isElement(iframeEl.contentWindow));
+
+ for (const type of [true, 42, {}, [], undefined, null]) {
+ ok(!dom.isElement(type));
+ }
+});
+
+add_task(function test_isDOMElement() {
+ const { divEl, iframeEl, shadowRoot, svgEl } = setupTest();
+
+ ok(dom.isDOMElement(divEl));
+ ok(dom.isDOMElement(svgEl));
+ ok(dom.isDOMElement(domElInPrivilegedDocument));
+
+ ok(!dom.isDOMElement(shadowRoot));
+ ok(!dom.isDOMElement(divEl.ownerGlobal));
+ ok(!dom.isDOMElement(iframeEl.contentWindow));
+ ok(!dom.isDOMElement(xulEl));
+ ok(!dom.isDOMElement(xulElInPrivilegedDocument));
+
+ for (const type of [true, 42, "foo", {}, [], undefined, null]) {
+ ok(!dom.isDOMElement(type));
+ }
+});
+
+add_task(function test_isXULElement() {
+ const { divEl, iframeEl, shadowRoot, svgEl } = setupTest();
+
+ ok(dom.isXULElement(xulEl));
+ ok(dom.isXULElement(xulElInPrivilegedDocument));
+
+ ok(!dom.isXULElement(divEl));
+ ok(!dom.isXULElement(domElInPrivilegedDocument));
+ ok(!dom.isXULElement(svgEl));
+ ok(!dom.isXULElement(shadowRoot));
+ ok(!dom.isXULElement(divEl.ownerGlobal));
+ ok(!dom.isXULElement(iframeEl.contentWindow));
+
+ for (const type of [true, 42, "foo", {}, [], undefined, null]) {
+ ok(!dom.isXULElement(type));
+ }
+});
+
+add_task(function test_isDOMWindow() {
+ const { divEl, iframeEl, shadowRoot, svgEl } = setupTest();
+
+ ok(dom.isDOMWindow(divEl.ownerGlobal));
+ ok(dom.isDOMWindow(iframeEl.contentWindow));
+
+ ok(!dom.isDOMWindow(divEl));
+ ok(!dom.isDOMWindow(svgEl));
+ ok(!dom.isDOMWindow(shadowRoot));
+ ok(!dom.isDOMWindow(domElInPrivilegedDocument));
+ ok(!dom.isDOMWindow(xulEl));
+ ok(!dom.isDOMWindow(xulElInPrivilegedDocument));
+
+ for (const type of [true, 42, {}, [], undefined, null]) {
+ ok(!dom.isDOMWindow(type));
+ }
+});
+
+add_task(function test_isShadowRoot() {
+ const { browser, divEl, iframeEl, shadowRoot, svgEl } = setupTest();
+
+ ok(dom.isShadowRoot(shadowRoot));
+
+ ok(!dom.isShadowRoot(divEl));
+ ok(!dom.isShadowRoot(svgEl));
+ ok(!dom.isShadowRoot(divEl.ownerGlobal));
+ ok(!dom.isShadowRoot(iframeEl.contentWindow));
+ ok(!dom.isShadowRoot(xulEl));
+ ok(!dom.isShadowRoot(domElInPrivilegedDocument));
+ ok(!dom.isShadowRoot(xulElInPrivilegedDocument));
+
+ for (const type of [true, 42, "foo", {}, [], undefined, null]) {
+ ok(!dom.isShadowRoot(type));
+ }
+
+ const documentFragment = browser.document.createDocumentFragment();
+ ok(!dom.isShadowRoot(documentFragment));
+});
+
+add_task(function test_isReadOnly() {
+ const { browser, divEl, textareaEl } = setupTest();
+
+ const input = browser.document.createElement("input");
+ input.readOnly = true;
+ ok(dom.isReadOnly(input));
+
+ textareaEl.readOnly = true;
+ ok(dom.isReadOnly(textareaEl));
+
+ ok(!dom.isReadOnly(divEl));
+ divEl.readOnly = true;
+ ok(!dom.isReadOnly(divEl));
+
+ ok(!dom.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(dom.isDisabled(option));
+
+ const optgroup = browser.document.createElement("optgroup");
+ option.parentNode = optgroup;
+ ok(dom.isDisabled(option));
+
+ optgroup.parentNode = select;
+ ok(dom.isDisabled(option));
+
+ select.disabled = false;
+ ok(!dom.isDisabled(option));
+
+ for (const type of ["button", "input", "select", "textarea"]) {
+ const elem = browser.document.createElement(type);
+ ok(!dom.isDisabled(elem));
+ elem.disabled = true;
+ ok(dom.isDisabled(elem));
+ }
+
+ ok(!dom.isDisabled(divEl));
+
+ svgEl.disabled = true;
+ ok(!dom.isDisabled(svgEl));
+
+ ok(!dom.isDisabled(new MockXULElement("browser", { disabled: true })));
+});
+
+add_task(function test_isEditingHost() {
+ const { browser, divEl, svgEl } = setupTest();
+
+ ok(!dom.isEditingHost(null));
+
+ ok(!dom.isEditingHost(divEl));
+ divEl.contentEditable = true;
+ ok(dom.isEditingHost(divEl));
+
+ ok(!dom.isEditingHost(svgEl));
+ browser.document.designMode = "on";
+ ok(dom.isEditingHost(svgEl));
+});
+
+add_task(function test_isEditable() {
+ const { browser, divEl, svgEl, textareaEl } = setupTest();
+
+ ok(!dom.isEditable(null));
+
+ for (let type of [
+ "checkbox",
+ "radio",
+ "hidden",
+ "submit",
+ "button",
+ "image",
+ ]) {
+ const input = browser.document.createElement("input");
+ input.setAttribute("type", type);
+
+ ok(!dom.isEditable(input));
+ }
+
+ const input = browser.document.createElement("input");
+ ok(dom.isEditable(input));
+ input.setAttribute("type", "text");
+ ok(dom.isEditable(input));
+
+ ok(dom.isEditable(textareaEl));
+
+ const textareaDisabled = browser.document.createElement("textarea");
+ textareaDisabled.disabled = true;
+ ok(!dom.isEditable(textareaDisabled));
+
+ const textareaReadOnly = browser.document.createElement("textarea");
+ textareaReadOnly.readOnly = true;
+ ok(!dom.isEditable(textareaReadOnly));
+
+ ok(!dom.isEditable(divEl));
+ divEl.contentEditable = true;
+ ok(dom.isEditable(divEl));
+
+ ok(!dom.isEditable(svgEl));
+ browser.document.designMode = "on";
+ ok(dom.isEditable(svgEl));
+});
+
+add_task(function test_isMutableFormControlElement() {
+ const { browser, divEl, textareaEl } = setupTest();
+
+ ok(!dom.isMutableFormControl(null));
+
+ ok(dom.isMutableFormControl(textareaEl));
+
+ const textareaDisabled = browser.document.createElement("textarea");
+ textareaDisabled.disabled = true;
+ ok(!dom.isMutableFormControl(textareaDisabled));
+
+ const textareaReadOnly = browser.document.createElement("textarea");
+ textareaReadOnly.readOnly = true;
+ ok(!dom.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(dom.isMutableFormControl(input));
+ }
+
+ const inputHidden = browser.document.createElement("input");
+ inputHidden.setAttribute("type", "hidden");
+ ok(!dom.isMutableFormControl(inputHidden));
+
+ ok(!dom.isMutableFormControl(divEl));
+ divEl.contentEditable = true;
+ ok(!dom.isMutableFormControl(divEl));
+ browser.document.designMode = "on";
+ ok(!dom.isMutableFormControl(divEl));
+});
+
+add_task(function test_coordinates() {
+ const { divEl } = setupTest();
+
+ let coords = dom.coordinates(divEl);
+ ok(coords.hasOwnProperty("x"));
+ ok(coords.hasOwnProperty("y"));
+ equal(typeof coords.x, "number");
+ equal(typeof coords.y, "number");
+
+ deepEqual(dom.coordinates(divEl), { x: 0, y: 0 });
+ deepEqual(dom.coordinates(divEl, 10, 10), { x: 10, y: 10 });
+ deepEqual(dom.coordinates(divEl, -5, -5), { x: -5, y: -5 });
+
+ Assert.throws(() => dom.coordinates(null), /node is null/);
+
+ Assert.throws(
+ () => dom.coordinates(divEl, "string", undefined),
+ /Offset must be a number/
+ );
+ Assert.throws(
+ () => dom.coordinates(divEl, undefined, "string"),
+ /Offset must be a number/
+ );
+ Assert.throws(
+ () => dom.coordinates(divEl, "string", "string"),
+ /Offset must be a number/
+ );
+ Assert.throws(
+ () => dom.coordinates(divEl, {}, undefined),
+ /Offset must be a number/
+ );
+ Assert.throws(
+ () => dom.coordinates(divEl, undefined, {}),
+ /Offset must be a number/
+ );
+ Assert.throws(
+ () => dom.coordinates(divEl, {}, {}),
+ /Offset must be a number/
+ );
+ Assert.throws(
+ () => dom.coordinates(divEl, [], undefined),
+ /Offset must be a number/
+ );
+ Assert.throws(
+ () => dom.coordinates(divEl, undefined, []),
+ /Offset must be a number/
+ );
+ Assert.throws(
+ () => dom.coordinates(divEl, [], []),
+ /Offset must be a number/
+ );
+});
+
+add_task(function test_isDetached() {
+ const { childEl, iframeEl } = setupTest();
+
+ let detachedShadowRoot = childEl.attachShadow({ mode: "open" });
+ detachedShadowRoot.innerHTML = "<input></input>";
+
+ // Connected to the DOM
+ ok(!dom.isDetached(detachedShadowRoot));
+
+ // Node document (ownerDocument) is not the active document
+ iframeEl.remove();
+ ok(dom.isDetached(detachedShadowRoot));
+
+ // host element is stale (eg. not connected)
+ detachedShadowRoot.host.remove();
+ equal(childEl.isConnected, false);
+ ok(dom.isDetached(detachedShadowRoot));
+});
+
+add_task(function test_isStale() {
+ const { childEl, iframeEl } = setupTest();
+
+ // Connected to the DOM
+ ok(!dom.isStale(childEl));
+
+ // Not part of the active document
+ iframeEl.remove();
+ ok(dom.isStale(childEl));
+
+ // Not connected to the DOM
+ childEl.remove();
+ ok(dom.isStale(childEl));
+});
diff --git a/remote/shared/test/xpcshell/test_Format.js b/remote/shared/test/xpcshell/test_Format.js
new file mode 100644
index 0000000000..cfdd35be08
--- /dev/null
+++ b/remote/shared/test/xpcshell/test_Format.js
@@ -0,0 +1,108 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { truncate, pprint } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/Format.sys.mjs"
+);
+
+const MAX_STRING_LENGTH = 250;
+const HALF = "x".repeat(MAX_STRING_LENGTH / 2);
+
+add_task(function test_pprint() {
+ equal('[object Object] {"foo":"bar"}', pprint`${{ foo: "bar" }}`);
+
+ equal("[object Number] 42", pprint`${42}`);
+ equal("[object Boolean] true", pprint`${true}`);
+ equal("[object Undefined] undefined", pprint`${undefined}`);
+ equal("[object Null] null", pprint`${null}`);
+
+ let complexObj = { toJSON: () => "foo" };
+ equal('[object Object] "foo"', pprint`${complexObj}`);
+
+ let cyclic = {};
+ cyclic.me = cyclic;
+ equal("[object Object] <cyclic object value>", pprint`${cyclic}`);
+
+ let el = {
+ hasAttribute: attr => attr in el,
+ getAttribute: attr => (attr in el ? el[attr] : null),
+ nodeType: 1,
+ localName: "input",
+ id: "foo",
+ class: "a b",
+ href: "#",
+ name: "bar",
+ src: "s",
+ type: "t",
+ };
+ equal(
+ '<input id="foo" class="a b" href="#" name="bar" src="s" type="t">',
+ pprint`${el}`
+ );
+});
+
+add_task(function test_truncate_empty() {
+ equal(truncate``, "");
+});
+
+add_task(function test_truncate_noFields() {
+ equal(truncate`foo bar`, "foo bar");
+});
+
+add_task(function test_truncate_multipleFields() {
+ equal(truncate`${0}`, "0");
+ equal(truncate`${1}${2}${3}`, "123");
+ equal(truncate`a${1}b${2}c${3}`, "a1b2c3");
+});
+
+add_task(function test_truncate_primitiveFields() {
+ equal(truncate`${123}`, "123");
+ equal(truncate`${true}`, "true");
+ equal(truncate`${null}`, "");
+ equal(truncate`${undefined}`, "");
+});
+
+add_task(function test_truncate_string() {
+ equal(truncate`${"foo"}`, "foo");
+ equal(truncate`${"x".repeat(250)}`, "x".repeat(250));
+ equal(truncate`${"x".repeat(260)}`, `${HALF} ... ${HALF}`);
+});
+
+add_task(function test_truncate_array() {
+ equal(truncate`${["foo"]}`, JSON.stringify(["foo"]));
+ equal(truncate`${"foo"} ${["bar"]}`, `foo ${JSON.stringify(["bar"])}`);
+ equal(
+ truncate`${["x".repeat(260)]}`,
+ JSON.stringify([`${HALF} ... ${HALF}`])
+ );
+});
+
+add_task(function test_truncate_object() {
+ equal(truncate`${{}}`, JSON.stringify({}));
+ equal(truncate`${{ foo: "bar" }}`, JSON.stringify({ foo: "bar" }));
+ equal(
+ truncate`${{ foo: "x".repeat(260) }}`,
+ JSON.stringify({ foo: `${HALF} ... ${HALF}` })
+ );
+ equal(truncate`${{ foo: ["bar"] }}`, JSON.stringify({ foo: ["bar"] }));
+ equal(
+ truncate`${{ foo: ["bar", { baz: 42 }] }}`,
+ JSON.stringify({ foo: ["bar", { baz: 42 }] })
+ );
+
+ let complex = {
+ toString() {
+ return "hello world";
+ },
+ };
+ equal(truncate`${complex}`, "hello world");
+
+ let longComplex = {
+ toString() {
+ return "x".repeat(260);
+ },
+ };
+ equal(truncate`${longComplex}`, `${HALF} ... ${HALF}`);
+});
diff --git a/remote/shared/test/xpcshell/test_Navigate.js b/remote/shared/test/xpcshell/test_Navigate.js
new file mode 100644
index 0000000000..e41508189a
--- /dev/null
+++ b/remote/shared/test/xpcshell/test_Navigate.js
@@ -0,0 +1,879 @@
+/* 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 { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+const {
+ DEFAULT_UNLOAD_TIMEOUT,
+ getUnloadTimeoutMultiplier,
+ ProgressListener,
+ waitForInitialNavigationCompleted,
+} = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/Navigate.sys.mjs"
+);
+
+const CURRENT_URI = Services.io.newURI("http://foo.bar/");
+const INITIAL_URI = Services.io.newURI("about:blank");
+const TARGET_URI = Services.io.newURI("http://foo.cheese/");
+const TARGET_URI_IS_ERROR_PAGE = Services.io.newURI("doesnotexist://");
+const TARGET_URI_WITH_HASH = Services.io.newURI("http://foo.cheese/#foo");
+
+function wait(time) {
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ return new Promise(resolve => setTimeout(resolve, time));
+}
+
+class MockRequest {
+ constructor(uri) {
+ this.originalURI = uri;
+ }
+
+ get QueryInterface() {
+ return ChromeUtils.generateQI(["nsIRequest", "nsIChannel"]);
+ }
+}
+
+class MockWebProgress {
+ constructor(browsingContext) {
+ this.browsingContext = browsingContext;
+
+ this.documentRequest = null;
+ this.isLoadingDocument = false;
+ this.listener = null;
+ this.progressListenerRemoved = false;
+ }
+
+ addProgressListener(listener) {
+ if (this.listener) {
+ throw new Error("Cannot register listener twice");
+ }
+
+ this.listener = listener;
+ }
+
+ removeProgressListener(listener) {
+ if (listener === this.listener) {
+ this.listener = null;
+ this.progressListenerRemoved = true;
+ } else {
+ throw new Error("Unknown listener");
+ }
+ }
+
+ sendLocationChange(options = {}) {
+ const { flag = 0 } = options;
+
+ this.documentRequest = null;
+
+ if (flag & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) {
+ this.browsingContext.currentURI = TARGET_URI_WITH_HASH;
+ } else if (flag & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
+ this.browsingContext.currentURI = TARGET_URI_IS_ERROR_PAGE;
+ }
+
+ this.listener?.onLocationChange(
+ this,
+ this.documentRequest,
+ TARGET_URI_WITH_HASH,
+ flag
+ );
+
+ return new Promise(executeSoon);
+ }
+
+ sendStartState(options = {}) {
+ const { coop = false, isInitial = false } = options;
+
+ if (coop) {
+ this.browsingContext = new MockTopContext(this);
+ }
+
+ if (!this.browsingContext.currentWindowGlobal) {
+ this.browsingContext.currentWindowGlobal = {};
+ }
+
+ this.browsingContext.currentWindowGlobal.isInitialDocument = isInitial;
+
+ this.isLoadingDocument = true;
+ const uri = isInitial ? INITIAL_URI : TARGET_URI;
+ this.documentRequest = new MockRequest(uri);
+
+ this.listener?.onStateChange(
+ this,
+ this.documentRequest,
+ Ci.nsIWebProgressListener.STATE_START,
+ null
+ );
+
+ return new Promise(executeSoon);
+ }
+
+ sendStopState(options = {}) {
+ const { errorFlag = 0 } = options;
+
+ this.browsingContext.currentURI = this.documentRequest.originalURI;
+
+ this.isLoadingDocument = false;
+ this.documentRequest = null;
+
+ this.listener?.onStateChange(
+ this,
+ this.documentRequest,
+ Ci.nsIWebProgressListener.STATE_STOP,
+ errorFlag
+ );
+
+ return new Promise(executeSoon);
+ }
+}
+
+class MockTopContext {
+ constructor(webProgress = null) {
+ this.currentURI = CURRENT_URI;
+ this.currentWindowGlobal = { isInitialDocument: true };
+ this.id = 7;
+ this.top = this;
+ this.webProgress = webProgress || new MockWebProgress(this);
+ }
+}
+
+const hasPromiseResolved = async function (promise) {
+ let resolved = false;
+ promise.finally(() => (resolved = true));
+ // Make sure microtasks have time to run.
+ await new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
+ return resolved;
+};
+
+const hasPromiseRejected = async function (promise) {
+ let rejected = false;
+ promise.catch(() => (rejected = true));
+ // Make sure microtasks have time to run.
+ await new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
+ return rejected;
+};
+
+add_task(
+ async function test_waitForInitialNavigation_initialDocumentNoWindowGlobal() {
+ const browsingContext = new MockTopContext();
+ const webProgress = browsingContext.webProgress;
+
+ // In some cases there might be no window global yet.
+ delete browsingContext.currentWindowGlobal;
+
+ ok(!webProgress.isLoadingDocument, "Document is not loading");
+
+ const navigated = waitForInitialNavigationCompleted(webProgress);
+ await webProgress.sendStartState({ isInitial: true });
+
+ ok(
+ !(await hasPromiseResolved(navigated)),
+ "waitForInitialNavigationCompleted has not resolved yet"
+ );
+
+ await webProgress.sendStopState();
+ const { currentURI, targetURI } = await navigated;
+
+ ok(!webProgress.isLoadingDocument, "Document is not loading");
+ ok(
+ webProgress.browsingContext.currentWindowGlobal.isInitialDocument,
+ "Is initial document"
+ );
+ equal(
+ currentURI.spec,
+ INITIAL_URI.spec,
+ "Expected current URI has been set"
+ );
+ equal(targetURI.spec, INITIAL_URI.spec, "Expected target URI has been set");
+ }
+);
+
+add_task(
+ async function test_waitForInitialNavigation_initialDocumentNotLoaded() {
+ const browsingContext = new MockTopContext();
+ const webProgress = browsingContext.webProgress;
+
+ ok(!webProgress.isLoadingDocument, "Document is not loading");
+
+ const navigated = waitForInitialNavigationCompleted(webProgress);
+
+ await webProgress.sendStartState({ isInitial: true });
+
+ ok(
+ !(await hasPromiseResolved(navigated)),
+ "waitForInitialNavigationCompleted has not resolved yet"
+ );
+
+ await webProgress.sendStopState();
+ const { currentURI, targetURI } = await navigated;
+
+ ok(!webProgress.isLoadingDocument, "Document is not loading");
+ ok(
+ webProgress.browsingContext.currentWindowGlobal.isInitialDocument,
+ "Is initial document"
+ );
+ equal(
+ currentURI.spec,
+ INITIAL_URI.spec,
+ "Expected current URI has been set"
+ );
+ equal(targetURI.spec, INITIAL_URI.spec, "Expected target URI has been set");
+ }
+);
+
+add_task(
+ async function test_waitForInitialNavigation_initialDocumentLoadingAndNoAdditionalLoad() {
+ const browsingContext = new MockTopContext();
+ const webProgress = browsingContext.webProgress;
+
+ await webProgress.sendStartState({ isInitial: true });
+ ok(webProgress.isLoadingDocument, "Document is loading");
+
+ const navigated = waitForInitialNavigationCompleted(webProgress);
+
+ ok(
+ !(await hasPromiseResolved(navigated)),
+ "waitForInitialNavigationCompleted has not resolved yet"
+ );
+
+ await webProgress.sendStopState();
+ const { currentURI, targetURI } = await navigated;
+
+ ok(!webProgress.isLoadingDocument, "Document is not loading");
+ ok(
+ webProgress.browsingContext.currentWindowGlobal.isInitialDocument,
+ "Is initial document"
+ );
+ equal(
+ currentURI.spec,
+ INITIAL_URI.spec,
+ "Expected current URI has been set"
+ );
+ equal(targetURI.spec, INITIAL_URI.spec, "Expected target URI has been set");
+ }
+);
+
+add_task(
+ async function test_waitForInitialNavigation_initialDocumentFinishedLoadingNoAdditionalLoad() {
+ const browsingContext = new MockTopContext();
+ const webProgress = browsingContext.webProgress;
+
+ await webProgress.sendStartState({ isInitial: true });
+ await webProgress.sendStopState();
+
+ ok(!webProgress.isLoadingDocument, "Document is not loading");
+
+ const navigated = waitForInitialNavigationCompleted(webProgress);
+
+ ok(
+ !(await hasPromiseResolved(navigated)),
+ "waitForInitialNavigationCompleted has not resolved yet"
+ );
+
+ const { currentURI, targetURI } = await navigated;
+
+ ok(!webProgress.isLoadingDocument, "Document is not loading");
+ ok(
+ webProgress.browsingContext.currentWindowGlobal.isInitialDocument,
+ "Is initial document"
+ );
+ equal(
+ currentURI.spec,
+ INITIAL_URI.spec,
+ "Expected current URI has been set"
+ );
+ equal(targetURI.spec, INITIAL_URI.spec, "Expected target URI has been set");
+ }
+);
+
+add_task(
+ async function test_waitForInitialNavigation_initialDocumentLoadingAndAdditionalLoad() {
+ const browsingContext = new MockTopContext();
+ const webProgress = browsingContext.webProgress;
+
+ await webProgress.sendStartState({ isInitial: true });
+
+ ok(webProgress.isLoadingDocument, "Document is loading");
+
+ const navigated = waitForInitialNavigationCompleted(webProgress);
+
+ ok(
+ !(await hasPromiseResolved(navigated)),
+ "waitForInitialNavigationCompleted has not resolved yet"
+ );
+
+ await webProgress.sendStopState();
+
+ await wait(100);
+
+ await webProgress.sendStartState({ isInitial: false });
+ await webProgress.sendStopState();
+
+ const { currentURI, targetURI } = await navigated;
+
+ ok(!webProgress.isLoadingDocument, "Document is not loading");
+ ok(
+ !webProgress.browsingContext.currentWindowGlobal.isInitialDocument,
+ "Is not initial document"
+ );
+ equal(
+ currentURI.spec,
+ TARGET_URI.spec,
+ "Expected current URI has been set"
+ );
+ equal(targetURI.spec, TARGET_URI.spec, "Expected target URI has been set");
+ }
+);
+
+add_task(
+ async function test_waitForInitialNavigation_initialDocumentFinishedLoadingAndAdditionalLoad() {
+ const browsingContext = new MockTopContext();
+ const webProgress = browsingContext.webProgress;
+
+ await webProgress.sendStartState({ isInitial: true });
+ await webProgress.sendStopState();
+
+ ok(!webProgress.isLoadingDocument, "Document is not loading");
+
+ const navigated = waitForInitialNavigationCompleted(webProgress);
+
+ ok(
+ !(await hasPromiseResolved(navigated)),
+ "waitForInitialNavigationCompleted has not resolved yet"
+ );
+
+ await wait(100);
+
+ await webProgress.sendStartState({ isInitial: false });
+ await webProgress.sendStopState();
+
+ const { currentURI, targetURI } = await navigated;
+
+ ok(!webProgress.isLoadingDocument, "Document is not loading");
+ ok(
+ !webProgress.browsingContext.currentWindowGlobal.isInitialDocument,
+ "Is not initial document"
+ );
+ equal(
+ currentURI.spec,
+ TARGET_URI.spec,
+ "Expected current URI has been set"
+ );
+ equal(targetURI.spec, TARGET_URI.spec, "Expected target URI has been set");
+ }
+);
+
+add_task(
+ async function test_waitForInitialNavigation_notInitialDocumentNotLoading() {
+ const browsingContext = new MockTopContext();
+ const webProgress = browsingContext.webProgress;
+
+ ok(!webProgress.isLoadingDocument, "Document is not loading");
+
+ const navigated = waitForInitialNavigationCompleted(webProgress);
+ await webProgress.sendStartState({ isInitial: false });
+
+ ok(
+ !(await hasPromiseResolved(navigated)),
+ "waitForInitialNavigationCompleted has not resolved yet"
+ );
+
+ await webProgress.sendStopState();
+ const { currentURI, targetURI } = await navigated;
+
+ ok(!webProgress.isLoadingDocument, "Document is not loading");
+ ok(
+ !browsingContext.currentWindowGlobal.isInitialDocument,
+ "Is not initial document"
+ );
+ equal(
+ currentURI.spec,
+ TARGET_URI.spec,
+ "Expected current URI has been set"
+ );
+ equal(targetURI.spec, TARGET_URI.spec, "Expected target URI has been set");
+ }
+);
+
+add_task(
+ async function test_waitForInitialNavigation_notInitialDocumentAlreadyLoading() {
+ const browsingContext = new MockTopContext();
+ const webProgress = browsingContext.webProgress;
+
+ await webProgress.sendStartState({ isInitial: false });
+ ok(webProgress.isLoadingDocument, "Document is loading");
+
+ const navigated = waitForInitialNavigationCompleted(webProgress);
+
+ ok(
+ !(await hasPromiseResolved(navigated)),
+ "waitForInitialNavigationCompleted has not resolved yet"
+ );
+
+ await webProgress.sendStopState();
+ const { currentURI, targetURI } = await navigated;
+
+ ok(!webProgress.isLoadingDocument, "Document is not loading");
+ ok(
+ !browsingContext.currentWindowGlobal.isInitialDocument,
+ "Is not initial document"
+ );
+ equal(
+ currentURI.spec,
+ TARGET_URI.spec,
+ "Expected current URI has been set"
+ );
+ equal(targetURI.spec, TARGET_URI.spec, "Expected target URI has been set");
+ }
+);
+
+add_task(
+ async function test_waitForInitialNavigation_notInitialDocumentFinishedLoading() {
+ const browsingContext = new MockTopContext();
+ const webProgress = browsingContext.webProgress;
+
+ await webProgress.sendStartState({ isInitial: false });
+ await webProgress.sendStopState();
+
+ ok(!webProgress.isLoadingDocument, "Document is not loading");
+
+ const { currentURI, targetURI } = await waitForInitialNavigationCompleted(
+ webProgress
+ );
+
+ ok(!webProgress.isLoadingDocument, "Document is not loading");
+ ok(
+ !webProgress.browsingContext.currentWindowGlobal.isInitialDocument,
+ "Is not initial document"
+ );
+ equal(
+ currentURI.spec,
+ TARGET_URI.spec,
+ "Expected current URI has been set"
+ );
+ equal(targetURI.spec, TARGET_URI.spec, "Expected target URI has been set");
+ }
+);
+
+add_task(async function test_waitForInitialNavigation_resolveWhenStarted() {
+ const browsingContext = new MockTopContext();
+ const webProgress = browsingContext.webProgress;
+
+ await webProgress.sendStartState({ isInitial: true });
+ ok(webProgress.isLoadingDocument, "Document is already loading");
+
+ const { currentURI, targetURI } = await waitForInitialNavigationCompleted(
+ webProgress,
+ {
+ resolveWhenStarted: true,
+ }
+ );
+
+ ok(webProgress.isLoadingDocument, "Document is still loading");
+ ok(
+ webProgress.browsingContext.currentWindowGlobal.isInitialDocument,
+ "Is initial document"
+ );
+ equal(currentURI.spec, CURRENT_URI.spec, "Expected current URI has been set");
+ equal(targetURI.spec, INITIAL_URI.spec, "Expected target URI has been set");
+});
+
+add_task(async function test_waitForInitialNavigation_crossOrigin() {
+ const browsingContext = new MockTopContext();
+ const webProgress = browsingContext.webProgress;
+
+ ok(!webProgress.isLoadingDocument, "Document is not loading");
+
+ const navigated = waitForInitialNavigationCompleted(webProgress);
+ await webProgress.sendStartState({ coop: true });
+
+ ok(
+ !(await hasPromiseResolved(navigated)),
+ "waitForInitialNavigationCompleted has not resolved yet"
+ );
+
+ await webProgress.sendStopState();
+ const { currentURI, targetURI } = await navigated;
+
+ notEqual(
+ browsingContext,
+ webProgress.browsingContext,
+ "Got new browsing context"
+ );
+ ok(!webProgress.isLoadingDocument, "Document is not loading");
+ ok(
+ !webProgress.browsingContext.currentWindowGlobal.isInitialDocument,
+ "Is not initial document"
+ );
+ equal(currentURI.spec, TARGET_URI.spec, "Expected current URI has been set");
+ equal(targetURI.spec, TARGET_URI.spec, "Expected target URI has been set");
+});
+
+add_task(async function test_waitForInitialNavigation_unloadTimeout_default() {
+ const browsingContext = new MockTopContext();
+ const webProgress = browsingContext.webProgress;
+
+ // Stop the navigation on an initial page which is not loading anymore.
+ // This situation happens with new tabs on Android, even though they are on
+ // the initial document, they will not start another navigation on their own.
+ await webProgress.sendStartState({ isInitial: true });
+ await webProgress.sendStopState();
+
+ ok(!webProgress.isLoadingDocument, "Document is not loading");
+
+ const navigated = waitForInitialNavigationCompleted(webProgress);
+
+ // Start a timer longer than the timeout which will be used by
+ // waitForInitialNavigationCompleted, and check that navigated resolves first.
+ const waitForMoreThanDefaultTimeout = wait(
+ DEFAULT_UNLOAD_TIMEOUT * 1.5 * getUnloadTimeoutMultiplier()
+ );
+ await Promise.race([navigated, waitForMoreThanDefaultTimeout]);
+
+ ok(
+ await hasPromiseResolved(navigated),
+ "waitForInitialNavigationCompleted has resolved"
+ );
+
+ ok(!webProgress.isLoadingDocument, "Document is not loading");
+ ok(
+ webProgress.browsingContext.currentWindowGlobal.isInitialDocument,
+ "Document is still on the initial document"
+ );
+});
+
+add_task(async function test_waitForInitialNavigation_unloadTimeout_longer() {
+ const browsingContext = new MockTopContext();
+ const webProgress = browsingContext.webProgress;
+
+ // Stop the navigation on an initial page which is not loading anymore.
+ // This situation happens with new tabs on Android, even though they are on
+ // the initial document, they will not start another navigation on their own.
+ await webProgress.sendStartState({ isInitial: true });
+ await webProgress.sendStopState();
+
+ ok(!webProgress.isLoadingDocument, "Document is not loading");
+
+ const navigated = waitForInitialNavigationCompleted(webProgress, {
+ unloadTimeout: DEFAULT_UNLOAD_TIMEOUT * 3,
+ });
+
+ // Start a timer longer than the default timeout of the Navigate module.
+ // However here we used a custom timeout, so we expect that the navigation
+ // will not be done yet by the time this timer is done.
+ const waitForMoreThanDefaultTimeout = wait(
+ DEFAULT_UNLOAD_TIMEOUT * 1.5 * getUnloadTimeoutMultiplier()
+ );
+ await Promise.race([navigated, waitForMoreThanDefaultTimeout]);
+
+ // The promise should not have resolved because we didn't reached the custom
+ // timeout which is 3 times the default one.
+ ok(
+ !(await hasPromiseResolved(navigated)),
+ "waitForInitialNavigationCompleted has not resolved yet"
+ );
+
+ // The navigation should eventually resolve once we reach the custom timeout.
+ await navigated;
+
+ ok(!webProgress.isLoadingDocument, "Document is not loading");
+ ok(
+ webProgress.browsingContext.currentWindowGlobal.isInitialDocument,
+ "Document is still on the initial document"
+ );
+});
+
+add_task(async function test_ProgressListener_expectNavigation() {
+ const browsingContext = new MockTopContext();
+ const webProgress = browsingContext.webProgress;
+
+ const progressListener = new ProgressListener(webProgress, {
+ expectNavigation: true,
+ unloadTimeout: 10,
+ });
+ const navigated = progressListener.start();
+
+ // Wait for unloadTimeout to finish in case it started
+ await wait(30);
+
+ ok(!(await hasPromiseResolved(navigated)), "Listener has not resolved yet");
+
+ await webProgress.sendStartState();
+ await webProgress.sendStopState();
+
+ ok(await hasPromiseResolved(navigated), "Listener has resolved");
+});
+
+add_task(
+ async function test_ProgressListener_expectNavigation_initialDocumentFinishedLoading() {
+ const browsingContext = new MockTopContext();
+ const webProgress = browsingContext.webProgress;
+
+ const progressListener = new ProgressListener(webProgress, {
+ expectNavigation: true,
+ unloadTimeout: 10,
+ });
+ const navigated = progressListener.start();
+
+ ok(!(await hasPromiseResolved(navigated)), "Listener has not resolved yet");
+
+ await webProgress.sendStartState({ isInitial: true });
+ await webProgress.sendStopState();
+
+ // Wait for unloadTimeout to finish in case it started
+ await wait(30);
+
+ ok(!(await hasPromiseResolved(navigated)), "Listener has not resolved yet");
+
+ await webProgress.sendStartState();
+ await webProgress.sendStopState();
+
+ ok(await hasPromiseResolved(navigated), "Listener has resolved");
+ }
+);
+
+add_task(async function test_ProgressListener_isStarted() {
+ const browsingContext = new MockTopContext();
+ const webProgress = browsingContext.webProgress;
+
+ const progressListener = new ProgressListener(webProgress);
+ ok(!progressListener.isStarted);
+
+ progressListener.start();
+ ok(progressListener.isStarted);
+
+ progressListener.stop();
+ ok(!progressListener.isStarted);
+});
+
+add_task(async function test_ProgressListener_notWaitForExplicitStart() {
+ // Create a webprogress and start it before creating the progress listener.
+ const browsingContext = new MockTopContext();
+ const webProgress = browsingContext.webProgress;
+ await webProgress.sendStartState();
+
+ // Create the progress listener for a webprogress already in a navigation.
+ const progressListener = new ProgressListener(webProgress, {
+ waitForExplicitStart: false,
+ });
+ const navigated = progressListener.start();
+
+ // Send stop state to complete the initial navigation
+ await webProgress.sendStopState();
+ ok(
+ await hasPromiseResolved(navigated),
+ "Listener has resolved after initial navigation"
+ );
+});
+
+add_task(async function test_ProgressListener_waitForExplicitStart() {
+ // Create a webprogress and start it before creating the progress listener.
+ const browsingContext = new MockTopContext();
+ const webProgress = browsingContext.webProgress;
+ await webProgress.sendStartState();
+
+ // Create the progress listener for a webprogress already in a navigation.
+ const progressListener = new ProgressListener(webProgress, {
+ waitForExplicitStart: true,
+ });
+ const navigated = progressListener.start();
+
+ // Send stop state to complete the initial navigation
+ await webProgress.sendStopState();
+ ok(
+ !(await hasPromiseResolved(navigated)),
+ "Listener has not resolved after initial navigation"
+ );
+
+ // Start a new navigation
+ await webProgress.sendStartState();
+ ok(
+ !(await hasPromiseResolved(navigated)),
+ "Listener has not resolved after starting new navigation"
+ );
+
+ // Finish the new navigation
+ await webProgress.sendStopState();
+ ok(
+ await hasPromiseResolved(navigated),
+ "Listener resolved after finishing the new navigation"
+ );
+});
+
+add_task(
+ async function test_ProgressListener_waitForExplicitStartAndResolveWhenStarted() {
+ // Create a webprogress and start it before creating the progress listener.
+ const browsingContext = new MockTopContext();
+ const webProgress = browsingContext.webProgress;
+ await webProgress.sendStartState();
+
+ // Create the progress listener for a webprogress already in a navigation.
+ const progressListener = new ProgressListener(webProgress, {
+ resolveWhenStarted: true,
+ waitForExplicitStart: true,
+ });
+ const navigated = progressListener.start();
+
+ // Send stop state to complete the initial navigation
+ await webProgress.sendStopState();
+ ok(
+ !(await hasPromiseResolved(navigated)),
+ "Listener has not resolved after initial navigation"
+ );
+
+ // Start a new navigation
+ await webProgress.sendStartState();
+ ok(
+ await hasPromiseResolved(navigated),
+ "Listener resolved after starting the new navigation"
+ );
+ }
+);
+
+add_task(
+ async function test_ProgressListener_resolveWhenNavigatingInsideDocument() {
+ const browsingContext = new MockTopContext();
+ const webProgress = browsingContext.webProgress;
+
+ const progressListener = new ProgressListener(webProgress);
+ const navigated = progressListener.start();
+
+ ok(!(await hasPromiseResolved(navigated)), "Listener has not resolved");
+
+ // Send hash change location change notification to complete the navigation
+ await webProgress.sendLocationChange({
+ flag: Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT,
+ });
+
+ ok(await hasPromiseResolved(navigated), "Listener has resolved");
+
+ const { currentURI, targetURI } = progressListener;
+ equal(
+ currentURI.spec,
+ TARGET_URI_WITH_HASH.spec,
+ "Expected current URI has been set"
+ );
+ equal(
+ targetURI.spec,
+ TARGET_URI_WITH_HASH.spec,
+ "Expected target URI has been set"
+ );
+ }
+);
+
+add_task(async function test_ProgressListener_ignoreCacheError() {
+ const browsingContext = new MockTopContext();
+ const webProgress = browsingContext.webProgress;
+
+ const progressListener = new ProgressListener(webProgress);
+ const navigated = progressListener.start();
+
+ ok(!(await hasPromiseResolved(navigated)), "Listener has not resolved");
+
+ await webProgress.sendStartState();
+ await webProgress.sendStopState({
+ errorFlag: Cr.NS_ERROR_PARSED_DATA_CACHED,
+ });
+
+ ok(await hasPromiseResolved(navigated), "Listener has resolved");
+});
+
+add_task(async function test_ProgressListener_navigationRejectedOnErrorPage() {
+ const browsingContext = new MockTopContext();
+ const webProgress = browsingContext.webProgress;
+
+ const progressListener = new ProgressListener(webProgress, {
+ waitForExplicitStart: false,
+ });
+ const navigated = progressListener.start();
+
+ await webProgress.sendStartState();
+ await webProgress.sendLocationChange({
+ flag:
+ Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT |
+ Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE,
+ });
+
+ ok(
+ await hasPromiseRejected(navigated),
+ "Listener has rejected in location change for error page"
+ );
+});
+
+add_task(async function test_ProgressListener_navigationRejectedOnStopState() {
+ const browsingContext = new MockTopContext();
+ const webProgress = browsingContext.webProgress;
+
+ const progressListener = new ProgressListener(webProgress, {
+ waitForExplicitStart: false,
+ });
+ const navigated = progressListener.start();
+
+ await webProgress.sendStartState();
+ await webProgress.sendStopState({ errorFlag: Cr.NS_BINDING_ABORTED });
+
+ ok(
+ await hasPromiseRejected(navigated),
+ "Listener has rejected in stop state for erroneous navigation"
+ );
+});
+
+add_task(async function test_ProgressListener_stopIfStarted() {
+ const browsingContext = new MockTopContext();
+ const webProgress = browsingContext.webProgress;
+
+ const progressListener = new ProgressListener(webProgress);
+ const navigated = progressListener.start();
+
+ progressListener.stopIfStarted();
+ ok(!(await hasPromiseResolved(navigated)), "Listener has not resolved");
+
+ await webProgress.sendStartState();
+ progressListener.stopIfStarted();
+ ok(await hasPromiseResolved(navigated), "Listener has resolved");
+});
+
+add_task(async function test_ProgressListener_stopIfStarted_alreadyStarted() {
+ // Create an already navigating browsing context.
+ const browsingContext = new MockTopContext();
+ const webProgress = browsingContext.webProgress;
+ await webProgress.sendStartState();
+
+ // Create a progress listener which accepts already ongoing navigations.
+ const progressListener = new ProgressListener(webProgress, {
+ waitForExplicitStart: false,
+ });
+ const navigated = progressListener.start();
+
+ // stopIfStarted should stop the listener because of the ongoing navigation.
+ progressListener.stopIfStarted();
+ ok(await hasPromiseResolved(navigated), "Listener has resolved");
+});
+
+add_task(
+ async function test_ProgressListener_stopIfStarted_alreadyStarted_waitForExplicitStart() {
+ // Create an already navigating browsing context.
+ const browsingContext = new MockTopContext();
+ const webProgress = browsingContext.webProgress;
+ await webProgress.sendStartState();
+
+ // Create a progress listener which rejects already ongoing navigations.
+ const progressListener = new ProgressListener(webProgress, {
+ waitForExplicitStart: true,
+ });
+ const navigated = progressListener.start();
+
+ // stopIfStarted will not stop the listener for the existing navigation.
+ progressListener.stopIfStarted();
+ ok(!(await hasPromiseResolved(navigated)), "Listener has not resolved");
+
+ // stopIfStarted will stop the listener when called after starting a new
+ // navigation.
+ await webProgress.sendStartState();
+ progressListener.stopIfStarted();
+ ok(await hasPromiseResolved(navigated), "Listener has resolved");
+ }
+);
diff --git a/remote/shared/test/xpcshell/test_Realm.js b/remote/shared/test/xpcshell/test_Realm.js
new file mode 100644
index 0000000000..3990cce482
--- /dev/null
+++ b/remote/shared/test/xpcshell/test_Realm.js
@@ -0,0 +1,116 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { Realm, WindowRealm } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/Realm.sys.mjs"
+);
+
+add_task(function test_id() {
+ const realm1 = new Realm();
+ const id1 = realm1.id;
+ Assert.equal(typeof id1, "string");
+
+ const realm2 = new Realm();
+ const id2 = realm2.id;
+ Assert.equal(typeof id2, "string");
+
+ Assert.notEqual(id1, id2, "Ids for different realms are different");
+});
+
+add_task(function test_handleObjectMap() {
+ const realm = new Realm();
+
+ // Test an unknown handle.
+ Assert.equal(
+ realm.getObjectForHandle("unknown"),
+ undefined,
+ "Unknown handles return undefined"
+ );
+
+ // Test creating a simple handle.
+ const object = {};
+ const handle = realm.getHandleForObject(object);
+ Assert.equal(typeof handle, "string", "Created a valid handle");
+ Assert.equal(
+ realm.getObjectForHandle(handle),
+ object,
+ "Using the handle returned the original object"
+ );
+
+ // Test another handle for the same object.
+ const secondHandle = realm.getHandleForObject(object);
+ Assert.equal(typeof secondHandle, "string", "Created a valid handle");
+ Assert.notEqual(secondHandle, handle, "A different handle was generated");
+ Assert.equal(
+ realm.getObjectForHandle(secondHandle),
+ object,
+ "Using the second handle also returned the original object"
+ );
+
+ // Test using the handles in another realm.
+ const otherRealm = new Realm();
+ Assert.equal(
+ otherRealm.getObjectForHandle(handle),
+ undefined,
+ "A realm returns undefined for handles from another realm"
+ );
+
+ // Removing an unknown handle should not throw or have any side effect on
+ // existing handles.
+ realm.removeObjectHandle("unknown");
+ Assert.equal(realm.getObjectForHandle(handle), object);
+ Assert.equal(realm.getObjectForHandle(secondHandle), object);
+
+ // Remove the second handle
+ realm.removeObjectHandle(secondHandle);
+ Assert.equal(
+ realm.getObjectForHandle(handle),
+ object,
+ "The first handle is still resolving the object"
+ );
+ Assert.equal(
+ realm.getObjectForHandle(secondHandle),
+ undefined,
+ "The second handle returns undefined after calling removeObjectHandle"
+ );
+
+ // Remove the original handle
+ realm.removeObjectHandle(handle);
+ Assert.equal(
+ realm.getObjectForHandle(handle),
+ undefined,
+ "The first handle returns undefined as well"
+ );
+});
+
+add_task(async function test_windowRealm_isSandbox() {
+ const windowlessBrowser = Services.appShell.createWindowlessBrowser(false);
+ const contentWindow = windowlessBrowser.docShell.domWindow;
+
+ const realm1 = new WindowRealm(contentWindow);
+ Assert.equal(realm1.isSandbox, false);
+
+ const realm2 = new WindowRealm(contentWindow, { sandboxName: "test" });
+ Assert.equal(realm2.isSandbox, true);
+});
+
+add_task(async function test_windowRealm_userActivationEnabled() {
+ const windowlessBrowser = Services.appShell.createWindowlessBrowser(false);
+ const contentWindow = windowlessBrowser.docShell.domWindow;
+ const userActivation = contentWindow.navigator.userActivation;
+
+ const realm = new WindowRealm(contentWindow);
+
+ Assert.equal(realm.userActivationEnabled, false);
+ Assert.equal(userActivation.isActive && userActivation.hasBeenActive, false);
+
+ realm.userActivationEnabled = true;
+ Assert.equal(realm.userActivationEnabled, true);
+ Assert.equal(userActivation.isActive && userActivation.hasBeenActive, true);
+
+ realm.userActivationEnabled = false;
+ Assert.equal(realm.userActivationEnabled, false);
+ Assert.equal(userActivation.isActive && userActivation.hasBeenActive, false);
+});
diff --git a/remote/shared/test/xpcshell/test_RecommendedPreferences.js b/remote/shared/test/xpcshell/test_RecommendedPreferences.js
new file mode 100644
index 0000000000..20de07a528
--- /dev/null
+++ b/remote/shared/test/xpcshell/test_RecommendedPreferences.js
@@ -0,0 +1,118 @@
+/* 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 { RecommendedPreferences } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/RecommendedPreferences.sys.mjs"
+);
+
+const COMMON_PREF = "toolkit.startup.max_resumed_crashes";
+
+const PROTOCOL_1_PREF = "dom.disable_beforeunload";
+const PROTOCOL_1_RECOMMENDED_PREFS = new Map([[PROTOCOL_1_PREF, true]]);
+
+const PROTOCOL_2_PREF = "browser.contentblocking.features.standard";
+const PROTOCOL_2_RECOMMENDED_PREFS = new Map([
+ [PROTOCOL_2_PREF, "-tp,tpPrivate,cookieBehavior0,-cm,-fp"],
+]);
+
+function cleanup() {
+ info("Restore recommended preferences and test preferences");
+ Services.prefs.clearUserPref("remote.prefs.recommended");
+ RecommendedPreferences.restoreAllPreferences();
+}
+
+// cleanup() should be called:
+// - explicitly after each test to avoid side effects
+// - via registerCleanupFunction in case a test crashes/times out
+registerCleanupFunction(cleanup);
+
+add_task(async function test_multipleClients() {
+ info("Check initial values for the test preferences");
+ checkPreferences({ common: false, protocol_1: false, protocol_2: false });
+
+ checkPreferences({ common: false, protocol_1: false, protocol_2: false });
+
+ info("Apply recommended preferences for a protocol_1 client");
+ RecommendedPreferences.applyPreferences(PROTOCOL_1_RECOMMENDED_PREFS);
+ checkPreferences({ common: true, protocol_1: true, protocol_2: false });
+
+ info("Apply recommended preferences for a protocol_2 client");
+ RecommendedPreferences.applyPreferences(PROTOCOL_2_RECOMMENDED_PREFS);
+ checkPreferences({ common: true, protocol_1: true, protocol_2: true });
+
+ info("Restore protocol_1 preferences");
+ RecommendedPreferences.restorePreferences(PROTOCOL_1_RECOMMENDED_PREFS);
+ checkPreferences({ common: true, protocol_1: false, protocol_2: true });
+
+ info("Restore protocol_2 preferences");
+ RecommendedPreferences.restorePreferences(PROTOCOL_2_RECOMMENDED_PREFS);
+ checkPreferences({ common: true, protocol_1: false, protocol_2: false });
+
+ info("Restore all the altered preferences");
+ RecommendedPreferences.restoreAllPreferences();
+ checkPreferences({ common: false, protocol_1: false, protocol_2: false });
+
+ info("Attemps to restore again");
+ RecommendedPreferences.restoreAllPreferences();
+ checkPreferences({ common: false, protocol_1: false, protocol_2: false });
+
+ cleanup();
+});
+
+add_task(async function test_disabled() {
+ info("Disable RecommendedPreferences");
+ Services.prefs.setBoolPref("remote.prefs.recommended", false);
+
+ info("Check initial values for the test preferences");
+ checkPreferences({ common: false, protocol_1: false, protocol_2: false });
+
+ info("Recommended preferences are not applied, applyPreferences is a no-op");
+ RecommendedPreferences.applyPreferences(PROTOCOL_1_RECOMMENDED_PREFS);
+ checkPreferences({ common: false, protocol_1: false, protocol_2: false });
+
+ cleanup();
+});
+
+add_task(async function test_noCustomPreferences() {
+ info("Applying preferences without any custom preference should not throw");
+ RecommendedPreferences.applyPreferences();
+
+ cleanup();
+});
+
+// Check that protocols can override common preferences.
+add_task(async function test_override() {
+ info("Make sure the common preference has no user value");
+ Services.prefs.clearUserPref(COMMON_PREF);
+
+ const OVERRIDE_VALUE = 42;
+ const OVERRIDE_COMMON_PREF = new Map([[COMMON_PREF, OVERRIDE_VALUE]]);
+
+ info("Apply a map of preferences overriding a common preference");
+ RecommendedPreferences.applyPreferences(OVERRIDE_COMMON_PREF);
+
+ equal(
+ Services.prefs.getIntPref(COMMON_PREF),
+ OVERRIDE_VALUE,
+ "The common preference was set to the expected value"
+ );
+
+ cleanup();
+});
+
+function checkPreferences({ common, protocol_1, protocol_2 }) {
+ checkPreference(COMMON_PREF, { hasValue: common });
+ checkPreference(PROTOCOL_1_PREF, { hasValue: protocol_1 });
+ checkPreference(PROTOCOL_2_PREF, { hasValue: protocol_2 });
+}
+
+function checkPreference(pref, { hasValue }) {
+ equal(
+ Services.prefs.prefHasUserValue(pref),
+ hasValue,
+ hasValue
+ ? `The preference ${pref} has a user value`
+ : `The preference ${pref} has no user value`
+ );
+}
diff --git a/remote/shared/test/xpcshell/test_Stack.js b/remote/shared/test/xpcshell/test_Stack.js
new file mode 100644
index 0000000000..c41c5f0240
--- /dev/null
+++ b/remote/shared/test/xpcshell/test_Stack.js
@@ -0,0 +1,120 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { getFramesFromStack, isChromeFrame } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/Stack.sys.mjs"
+);
+
+const sourceFrames = [
+ {
+ column: 1,
+ functionDisplayName: "foo",
+ line: 2,
+ source: "cheese",
+ sourceId: 1,
+ },
+ {
+ column: 3,
+ functionDisplayName: null,
+ line: 4,
+ source: "cake",
+ sourceId: 2,
+ },
+ {
+ column: 5,
+ functionDisplayName: "chrome",
+ line: 6,
+ source: "chrome://foo",
+ sourceId: 3,
+ },
+];
+
+const targetFrames = [
+ {
+ columnNumber: 1,
+ functionName: "foo",
+ lineNumber: 2,
+ filename: "cheese",
+ sourceId: 1,
+ },
+ {
+ columnNumber: 3,
+ functionName: "",
+ lineNumber: 4,
+ filename: "cake",
+ sourceId: 2,
+ },
+ {
+ columnNumber: 5,
+ functionName: "chrome",
+ lineNumber: 6,
+ filename: "chrome://foo",
+ sourceId: 3,
+ },
+];
+
+add_task(async function test_getFramesFromStack() {
+ const stack = buildStack(sourceFrames);
+ const frames = getFramesFromStack(stack, { includeChrome: false });
+
+ ok(Array.isArray(frames), "frames is of expected type Array");
+ equal(frames.length, 3, "Got expected amount of frames");
+ checkFrame(frames.at(0), targetFrames.at(0));
+ checkFrame(frames.at(1), targetFrames.at(1));
+ checkFrame(frames.at(2), targetFrames.at(2));
+});
+
+add_task(async function test_getFramesFromStack_asyncStack() {
+ const stack = buildStack(sourceFrames, true);
+ const frames = getFramesFromStack(stack);
+
+ ok(Array.isArray(frames), "frames is of expected type Array");
+ equal(frames.length, 3, "Got expected amount of frames");
+ checkFrame(frames.at(0), targetFrames.at(0));
+ checkFrame(frames.at(1), targetFrames.at(1));
+ checkFrame(frames.at(2), targetFrames.at(2));
+});
+
+add_task(async function test_isChromeFrame() {
+ for (const filename of ["chrome://foo/bar", "resource://foo/bar"]) {
+ ok(isChromeFrame({ filename }), "Frame is of expected chrome scope");
+ }
+
+ for (const filename of ["http://foo.bar", "about:blank"]) {
+ ok(!isChromeFrame({ filename }), "Frame is of expected content scope");
+ }
+});
+
+function buildStack(frames, async = false) {
+ const parent = async ? "asyncParent" : "parent";
+
+ let currentFrame, stack;
+ for (const frame of frames) {
+ if (currentFrame) {
+ currentFrame[parent] = Object.assign({}, frame);
+ currentFrame = currentFrame[parent];
+ } else {
+ stack = Object.assign({}, frame);
+ currentFrame = stack;
+ }
+ }
+
+ return stack;
+}
+
+function checkFrame(frame, expectedFrame) {
+ equal(
+ frame.columnNumber,
+ expectedFrame.columnNumber,
+ "Got expected column number"
+ );
+ equal(
+ frame.functionName,
+ expectedFrame.functionName,
+ "Got expected function name"
+ );
+ equal(frame.lineNumber, expectedFrame.lineNumber, "Got expected line number");
+ equal(frame.filename, expectedFrame.filename, "Got expected filename");
+ equal(frame.sourceId, expectedFrame.sourceId, "Got expected source id");
+}
diff --git a/remote/shared/test/xpcshell/test_Sync.js b/remote/shared/test/xpcshell/test_Sync.js
new file mode 100644
index 0000000000..de4a4d30fe
--- /dev/null
+++ b/remote/shared/test/xpcshell/test_Sync.js
@@ -0,0 +1,436 @@
+/* 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 { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+const { AnimationFramePromise, Deferred, EventPromise, PollPromise } =
+ ChromeUtils.importESModule("chrome://remote/content/shared/Sync.sys.mjs");
+
+const { Log } = ChromeUtils.importESModule(
+ "resource://gre/modules/Log.sys.mjs"
+);
+
+/**
+ * Mimic a DOM node for listening for events.
+ */
+class MockElement {
+ constructor() {
+ this.capture = false;
+ this.eventName = null;
+ this.func = null;
+ this.mozSystemGroup = false;
+ this.wantUntrusted = false;
+ this.untrusted = false;
+ }
+
+ addEventListener(name, func, options = {}) {
+ const { capture, mozSystemGroup, wantUntrusted } = options;
+
+ this.eventName = name;
+ this.func = func;
+ this.capture = capture ?? false;
+ this.mozSystemGroup = mozSystemGroup ?? false;
+ this.wantUntrusted = wantUntrusted ?? false;
+ }
+
+ click() {
+ if (this.func) {
+ const event = {
+ capture: this.capture,
+ mozSystemGroup: this.mozSystemGroup,
+ target: this,
+ type: this.eventName,
+ untrusted: this.untrusted,
+ wantUntrusted: this.wantUntrusted,
+ };
+ this.func(event);
+ }
+ }
+
+ dispatchEvent(event) {
+ if (this.wantUntrusted) {
+ this.untrusted = true;
+ }
+ this.click();
+ }
+
+ removeEventListener(name, func) {
+ this.capture = false;
+ this.eventName = null;
+ this.func = null;
+ this.mozSystemGroup = false;
+ this.untrusted = false;
+ this.wantUntrusted = false;
+ }
+}
+
+class MockAppender extends Log.Appender {
+ constructor(formatter) {
+ super(formatter);
+ this.messages = [];
+ }
+
+ append(message) {
+ this.doAppend(message);
+ }
+
+ doAppend(message) {
+ this.messages.push(message);
+ }
+}
+
+add_task(async function test_AnimationFramePromise() {
+ let called = false;
+ let win = {
+ requestAnimationFrame(callback) {
+ called = true;
+ callback();
+ },
+ };
+ await AnimationFramePromise(win);
+ ok(called);
+});
+
+add_task(async function test_AnimationFramePromiseAbortWhenWindowClosed() {
+ let win = {
+ closed: true,
+ requestAnimationFrame() {},
+ };
+ await AnimationFramePromise(win);
+});
+
+add_task(async function test_DeferredPending() {
+ const deferred = Deferred();
+ ok(deferred.pending);
+
+ deferred.resolve();
+ await deferred.promise;
+ ok(!deferred.pending);
+});
+
+add_task(async function test_DeferredRejected() {
+ const deferred = Deferred();
+
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ setTimeout(() => deferred.reject(new Error("foo")), 100);
+
+ try {
+ await deferred.promise;
+ ok(false);
+ } catch (e) {
+ ok(!deferred.pending);
+
+ ok(!deferred.fulfilled);
+ ok(deferred.rejected);
+ equal(e.message, "foo");
+ }
+});
+
+add_task(async function test_DeferredResolved() {
+ const deferred = Deferred();
+ ok(deferred.pending);
+
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ setTimeout(() => deferred.resolve("foo"), 100);
+
+ const result = await deferred.promise;
+ ok(!deferred.pending);
+
+ ok(deferred.fulfilled);
+ ok(!deferred.rejected);
+ equal(result, "foo");
+});
+
+add_task(async function test_EventPromise_subjectTypes() {
+ for (const subject of ["foo", 42, null, undefined, true, [], {}]) {
+ Assert.throws(() => new EventPromise(subject, "click"), /TypeError/);
+ }
+});
+
+add_task(async function test_EventPromise_eventNameTypes() {
+ const element = new MockElement();
+
+ for (const eventName of [42, null, undefined, true, [], {}]) {
+ Assert.throws(() => new EventPromise(element, eventName), /TypeError/);
+ }
+});
+
+add_task(async function test_EventPromise_subjectAndEventNameEvent() {
+ const element = new MockElement();
+
+ const clicked = new EventPromise(element, "click");
+ element.click();
+ const event = await clicked;
+
+ equal(element, event.target);
+});
+
+add_task(async function test_EventPromise_captureTypes() {
+ const element = new MockElement();
+
+ for (const capture of [null, "foo", 42, [], {}]) {
+ Assert.throws(
+ () => new EventPromise(element, "click", { capture }),
+ /TypeError/
+ );
+ }
+});
+
+add_task(async function test_EventPromise_captureEvent() {
+ const element = new MockElement();
+
+ for (const capture of [undefined, false, true]) {
+ const expectedCapture = capture ?? false;
+
+ const clicked = new EventPromise(element, "click", { capture });
+ element.click();
+ const event = await clicked;
+
+ equal(element, event.target);
+ equal(expectedCapture, event.capture);
+ }
+});
+
+add_task(async function test_EventPromise_checkFnTypes() {
+ const element = new MockElement();
+
+ for (const checkFn of ["foo", 42, true, [], {}]) {
+ Assert.throws(
+ () => new EventPromise(element, "click", { checkFn }),
+ /TypeError/
+ );
+ }
+});
+
+add_task(async function test_EventPromise_checkFnCallback() {
+ const element = new MockElement();
+
+ let count;
+ const data = [
+ { checkFn: null, expected_count: 0 },
+ { checkFn: undefined, expected_count: 0 },
+ {
+ checkFn: event => {
+ throw new Error("foo");
+ },
+ expected_count: 0,
+ },
+ { checkFn: event => count++ > 0, expected_count: 2 },
+ ];
+
+ for (const { checkFn, expected_count } of data) {
+ count = 0;
+
+ const clicked = new EventPromise(element, "click", { checkFn });
+ element.click();
+ element.click();
+ const event = await clicked;
+
+ equal(element, event.target);
+ equal(expected_count, count);
+ }
+});
+
+add_task(async function test_EventPromise_mozSystemGroupTypes() {
+ const element = new MockElement();
+
+ for (const mozSystemGroup of [null, "foo", 42, [], {}]) {
+ Assert.throws(
+ () => new EventPromise(element, "click", { mozSystemGroup }),
+ /TypeError/
+ );
+ }
+});
+
+add_task(async function test_EventPromise_mozSystemGroupEvent() {
+ const element = new MockElement();
+
+ for (const mozSystemGroup of [undefined, false, true]) {
+ const expectedMozSystemGroup = mozSystemGroup ?? false;
+
+ const clicked = new EventPromise(element, "click", { mozSystemGroup });
+ element.click();
+ const event = await clicked;
+
+ equal(element, event.target);
+ equal(expectedMozSystemGroup, event.mozSystemGroup);
+ }
+});
+
+add_task(async function test_EventPromise_wantUntrustedTypes() {
+ const element = new MockElement();
+
+ for (let wantUntrusted of [null, "foo", 42, [], {}]) {
+ Assert.throws(
+ () => new EventPromise(element, "click", { wantUntrusted }),
+ /TypeError/
+ );
+ }
+});
+
+add_task(async function test_EventPromise_wantUntrustedEvent() {
+ for (const wantUntrusted of [undefined, false, true]) {
+ let expected_untrusted = wantUntrusted ?? false;
+
+ const element = new MockElement();
+
+ const clicked = new EventPromise(element, "click", { wantUntrusted });
+ element.dispatchEvent(new CustomEvent("click", {}));
+ const event = await clicked;
+
+ equal(element, event.target);
+ equal(expected_untrusted, event.untrusted);
+ }
+});
+
+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/shared/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(async function test_PollPromise_resolve() {
+ const log = Log.repository.getLogger("RemoteAgent");
+ const appender = new MockAppender(new Log.BasicFormatter());
+ appender.level = Log.Level.Info;
+ log.addAppender(appender);
+
+ const errorMessage = "PollingFailed";
+ const timeout = 100;
+
+ await new PollPromise(
+ (resolve, reject) => {
+ resolve();
+ },
+ { timeout, errorMessage }
+ );
+ Assert.equal(appender.messages.length, 0);
+
+ await new PollPromise(
+ (resolve, reject) => {
+ reject();
+ },
+ { timeout, errorMessage: "PollingFailed" }
+ );
+ Assert.equal(appender.messages.length, 1);
+ Assert.equal(appender.messages[0].level, Log.Level.Warn);
+ Assert.equal(appender.messages[0].message, "PollingFailed after 100 ms");
+});
diff --git a/remote/shared/test/xpcshell/test_TabManager.js b/remote/shared/test/xpcshell/test_TabManager.js
new file mode 100644
index 0000000000..e9da02c861
--- /dev/null
+++ b/remote/shared/test/xpcshell/test_TabManager.js
@@ -0,0 +1,56 @@
+/* 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 { TabManager } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/TabManager.sys.mjs"
+);
+
+class MockTopBrowsingContext {
+ constructor() {
+ this.embedderElement = { permanentKey: {} };
+ this.id = 1;
+ this.top = this;
+ }
+}
+
+class MockBrowsingContext {
+ constructor() {
+ this.id = 2;
+
+ const topContext = new MockTopBrowsingContext();
+ this.parent = topContext;
+ this.top = topContext;
+ }
+}
+
+const mockTopBrowsingContext = new MockTopBrowsingContext();
+const mockBrowsingContext = new MockBrowsingContext();
+
+add_task(async function test_getIdForBrowsingContext() {
+ // Browsing context not set.
+ equal(TabManager.getIdForBrowsingContext(null), null);
+ equal(TabManager.getIdForBrowsingContext(undefined), null);
+
+ // Child browsing context.
+ equal(
+ TabManager.getIdForBrowsingContext(mockBrowsingContext),
+ mockBrowsingContext.id
+ );
+
+ const browser = mockTopBrowsingContext.embedderElement;
+ equal(
+ TabManager.getIdForBrowsingContext(mockTopBrowsingContext),
+ TabManager.getIdForBrowser(browser)
+ );
+});
+
+add_task(async function test_removeTab() {
+ // Tab not defined.
+ await TabManager.removeTab(null);
+});
+
+add_task(async function test_selectTab() {
+ // Tab not defined.
+ await TabManager.selectTab(null);
+});
diff --git a/remote/shared/test/xpcshell/test_UUID.js b/remote/shared/test/xpcshell/test_UUID.js
new file mode 100644
index 0000000000..e929a9e0a8
--- /dev/null
+++ b/remote/shared/test/xpcshell/test_UUID.js
@@ -0,0 +1,21 @@
+/* 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 { generateUUID } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/UUID.sys.mjs"
+);
+
+add_task(function test_UUID_valid() {
+ const uuid = generateUUID();
+ const regExp = new RegExp(
+ /^[a-f|0-9]{8}-[a-f|0-9]{4}-[a-f|0-9]{4}-[a-f|0-9]{4}-[a-f|0-9]{12}$/g
+ );
+ ok(regExp.test(uuid));
+});
+
+add_task(function test_UUID_unique() {
+ const uuid1 = generateUUID();
+ const uuid2 = generateUUID();
+ notEqual(uuid1, uuid2);
+});
diff --git a/remote/shared/test/xpcshell/xpcshell.toml b/remote/shared/test/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..ebb6c77950
--- /dev/null
+++ b/remote/shared/test/xpcshell/xpcshell.toml
@@ -0,0 +1,24 @@
+[DEFAULT]
+head = "head.js"
+
+["test_AppInfo.js"]
+
+["test_ChallengeHeaderParser.js"]
+
+["test_DOM.js"]
+
+["test_Format.js"]
+
+["test_Navigate.js"]
+
+["test_Realm.js"]
+
+["test_RecommendedPreferences.js"]
+
+["test_Stack.js"]
+
+["test_Sync.js"]
+
+["test_TabManager.js"]
+
+["test_UUID.js"]