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/test_AppInfo.js32
-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.js86
-rw-r--r--remote/shared/test/xpcshell/test_RecommendedPreferences.js111
-rw-r--r--remote/shared/test/xpcshell/test_Stack.js120
-rw-r--r--remote/shared/test/xpcshell/test_Sync.js389
-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.ini13
10 files changed, 1815 insertions, 0 deletions
diff --git a/remote/shared/test/xpcshell/test_AppInfo.js b/remote/shared/test/xpcshell/test_AppInfo.js
new file mode 100644
index 0000000000..a83983aea4
--- /dev/null
+++ b/remote/shared/test/xpcshell/test_AppInfo.js
@@ -0,0 +1,32 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { AppInfo } = 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`
+ );
+ }
+});
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..36788201ef
--- /dev/null
+++ b/remote/shared/test/xpcshell/test_Realm.js
@@ -0,0 +1,86 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { Realm } = 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"
+ );
+});
diff --git a/remote/shared/test/xpcshell/test_RecommendedPreferences.js b/remote/shared/test/xpcshell/test_RecommendedPreferences.js
new file mode 100644
index 0000000000..d42482d1d8
--- /dev/null
+++ b/remote/shared/test/xpcshell/test_RecommendedPreferences.js
@@ -0,0 +1,111 @@
+/* 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 MARIONETTE_PREF = "dom.disable_beforeunload";
+const MARIONETTE_RECOMMENDED_PREFS = new Map([[MARIONETTE_PREF, true]]);
+
+const CDP_PREF = "browser.contentblocking.features.standard";
+const CDP_RECOMMENDED_PREFS = new Map([
+ [CDP_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_RecommendedPreferences() {
+ info("Check initial values for the test preferences");
+ checkPreferences({ cdp: false, common: false, marionette: false });
+
+ checkPreferences({ cdp: false, common: false, marionette: false });
+
+ info("Apply recommended preferences for a marionette client");
+ RecommendedPreferences.applyPreferences(MARIONETTE_RECOMMENDED_PREFS);
+ checkPreferences({ cdp: false, common: true, marionette: true });
+
+ info("Apply recommended preferences for a cdp client");
+ RecommendedPreferences.applyPreferences(CDP_RECOMMENDED_PREFS);
+ checkPreferences({ cdp: true, common: true, marionette: true });
+
+ info("Restore marionette preferences");
+ RecommendedPreferences.restorePreferences(MARIONETTE_RECOMMENDED_PREFS);
+ checkPreferences({ cdp: true, common: true, marionette: false });
+
+ info("Restore cdp preferences");
+ RecommendedPreferences.restorePreferences(CDP_RECOMMENDED_PREFS);
+ checkPreferences({ cdp: false, common: true, marionette: false });
+
+ info("Restore all the altered preferences");
+ RecommendedPreferences.restoreAllPreferences();
+ checkPreferences({ cdp: false, common: false, marionette: false });
+
+ info("Attemps to restore again");
+ RecommendedPreferences.restoreAllPreferences();
+ checkPreferences({ cdp: false, common: false, marionette: false });
+
+ cleanup();
+});
+
+add_task(async function test_RecommendedPreferences_disabled() {
+ info("Disable RecommendedPreferences");
+ Services.prefs.setBoolPref("remote.prefs.recommended", false);
+
+ info("Check initial values for the test preferences");
+ checkPreferences({ cdp: false, common: false, marionette: false });
+
+ info("Recommended preferences are not applied, applyPreferences is a no-op");
+ RecommendedPreferences.applyPreferences(MARIONETTE_RECOMMENDED_PREFS);
+ checkPreferences({ cdp: false, common: false, marionette: false });
+
+ cleanup();
+});
+
+// Check that protocols can override common preferences.
+add_task(async function test_RecommendedPreferences_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({ cdp, common, marionette }) {
+ checkPreference(COMMON_PREF, { hasValue: common });
+ checkPreference(MARIONETTE_PREF, { hasValue: marionette });
+ checkPreference(CDP_PREF, { hasValue: cdp });
+}
+
+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..ae9de72718
--- /dev/null
+++ b/remote/shared/test/xpcshell/test_Sync.js
@@ -0,0 +1,389 @@
+/* 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");
+
+/**
+ * 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;
+ }
+}
+
+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);
+});
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.ini b/remote/shared/test/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..3ea41894ea
--- /dev/null
+++ b/remote/shared/test/xpcshell/xpcshell.ini
@@ -0,0 +1,13 @@
+# 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/.
+
+[test_AppInfo.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]