summaryrefslogtreecommitdiffstats
path: root/remote/shared/test
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /remote/shared/test
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'remote/shared/test')
-rw-r--r--remote/shared/test/browser/browser.toml16
-rw-r--r--remote/shared/test/browser/browser_NavigationManager.js372
-rw-r--r--remote/shared/test/browser/browser_NavigationManager_failed_navigation.js99
-rw-r--r--remote/shared/test/browser/browser_NavigationManager_no_navigation.js60
-rw-r--r--remote/shared/test/browser/browser_NavigationManager_notify.js170
-rw-r--r--remote/shared/test/browser/browser_TabManager.js178
-rw-r--r--remote/shared/test/browser/browser_UserContextManager.js236
-rw-r--r--remote/shared/test/browser/head.js205
-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
21 files changed, 3889 insertions, 0 deletions
diff --git a/remote/shared/test/browser/browser.toml b/remote/shared/test/browser/browser.toml
new file mode 100644
index 0000000000..de336a1cb7
--- /dev/null
+++ b/remote/shared/test/browser/browser.toml
@@ -0,0 +1,16 @@
+[DEFAULT]
+tags = "remote"
+subsuite = "remote"
+support-files = ["head.js"]
+
+["browser_NavigationManager.js"]
+
+["browser_NavigationManager_failed_navigation.js"]
+
+["browser_NavigationManager_no_navigation.js"]
+
+["browser_NavigationManager_notify.js"]
+
+["browser_TabManager.js"]
+
+["browser_UserContextManager.js"]
diff --git a/remote/shared/test/browser/browser_NavigationManager.js b/remote/shared/test/browser/browser_NavigationManager.js
new file mode 100644
index 0000000000..7e0464c2fa
--- /dev/null
+++ b/remote/shared/test/browser/browser_NavigationManager.js
@@ -0,0 +1,372 @@
+/* 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 { NavigationManager } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/NavigationManager.sys.mjs"
+);
+const { TabManager } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/TabManager.sys.mjs"
+);
+
+const FIRST_URL = "https://example.com/document-builder.sjs?html=first";
+const SECOND_URL = "https://example.com/document-builder.sjs?html=second";
+const THIRD_URL = "https://example.com/document-builder.sjs?html=third";
+
+const FIRST_COOP_URL =
+ "https://example.com/document-builder.sjs?headers=Cross-Origin-Opener-Policy:same-origin&html=first_coop";
+const SECOND_COOP_URL =
+ "https://example.net/document-builder.sjs?headers=Cross-Origin-Opener-Policy:same-origin&html=second_coop";
+
+add_task(async function test_simpleNavigation() {
+ const events = [];
+ const onEvent = (name, data) => events.push({ name, data });
+
+ const navigationManager = new NavigationManager();
+ navigationManager.on("navigation-started", onEvent);
+ navigationManager.on("navigation-stopped", onEvent);
+
+ const tab = addTab(gBrowser, FIRST_URL);
+ const browser = tab.linkedBrowser;
+ await BrowserTestUtils.browserLoaded(browser);
+
+ const navigableId = TabManager.getIdForBrowser(browser);
+
+ navigationManager.startMonitoring();
+ is(
+ navigationManager.getNavigationForBrowsingContext(browser.browsingContext),
+ null,
+ "No navigation recorded yet"
+ );
+ is(events.length, 0, "No event recorded");
+
+ await loadURL(browser, SECOND_URL);
+
+ const firstNavigation = navigationManager.getNavigationForBrowsingContext(
+ browser.browsingContext
+ );
+ assertNavigation(firstNavigation, SECOND_URL);
+
+ is(events.length, 2, "Two events recorded");
+ assertNavigationEvents(
+ events,
+ SECOND_URL,
+ firstNavigation.navigationId,
+ navigableId
+ );
+
+ await loadURL(browser, THIRD_URL);
+
+ const secondNavigation = navigationManager.getNavigationForBrowsingContext(
+ browser.browsingContext
+ );
+ assertNavigation(secondNavigation, THIRD_URL);
+ assertUniqueNavigationIds(firstNavigation, secondNavigation);
+
+ is(events.length, 4, "Two new events recorded");
+ assertNavigationEvents(
+ events,
+ THIRD_URL,
+ secondNavigation.navigationId,
+ navigableId
+ );
+
+ navigationManager.stopMonitoring();
+
+ // Navigate again to the first URL
+ await loadURL(browser, FIRST_URL);
+ is(events.length, 4, "No new event recorded");
+ is(
+ navigationManager.getNavigationForBrowsingContext(browser.browsingContext),
+ null,
+ "No navigation recorded"
+ );
+
+ navigationManager.off("navigation-started", onEvent);
+ navigationManager.off("navigation-stopped", onEvent);
+});
+
+add_task(async function test_loadTwoTabsSimultaneously() {
+ const events = [];
+ const onEvent = (name, data) => events.push({ name, data });
+
+ const navigationManager = new NavigationManager();
+ navigationManager.on("navigation-started", onEvent);
+ navigationManager.on("navigation-stopped", onEvent);
+
+ navigationManager.startMonitoring();
+
+ info("Add two tabs simultaneously");
+ const tab1 = addTab(gBrowser, FIRST_URL);
+ const browser1 = tab1.linkedBrowser;
+ const navigableId1 = TabManager.getIdForBrowser(browser1);
+ const onLoad1 = BrowserTestUtils.browserLoaded(browser1, false, FIRST_URL);
+
+ const tab2 = addTab(gBrowser, SECOND_URL);
+ const browser2 = tab2.linkedBrowser;
+ const navigableId2 = TabManager.getIdForBrowser(browser2);
+ const onLoad2 = BrowserTestUtils.browserLoaded(browser2, false, SECOND_URL);
+
+ info("Wait for the tabs to load");
+ await Promise.all([onLoad1, onLoad2]);
+
+ is(events.length, 4, "Recorded 4 navigation events");
+
+ info("Check navigation monitored for tab1");
+ const nav1 = navigationManager.getNavigationForBrowsingContext(
+ browser1.browsingContext
+ );
+ assertNavigation(nav1, FIRST_URL);
+ assertNavigationEvents(events, FIRST_URL, nav1.navigationId, navigableId1);
+
+ info("Check navigation monitored for tab2");
+ const nav2 = navigationManager.getNavigationForBrowsingContext(
+ browser2.browsingContext
+ );
+ assertNavigation(nav2, SECOND_URL);
+ assertNavigationEvents(events, SECOND_URL, nav2.navigationId, navigableId2);
+ assertUniqueNavigationIds(nav1, nav2);
+
+ info("Reload the two tabs simultaneously");
+ await Promise.all([
+ BrowserTestUtils.reloadTab(tab1),
+ BrowserTestUtils.reloadTab(tab2),
+ ]);
+
+ is(events.length, 8, "Recorded 8 navigation events");
+
+ info("Check the second navigation for tab1");
+ const nav3 = navigationManager.getNavigationForBrowsingContext(
+ browser1.browsingContext
+ );
+ assertNavigation(nav3, FIRST_URL);
+ assertNavigationEvents(events, FIRST_URL, nav3.navigationId, navigableId1);
+
+ info("Check the second navigation monitored for tab2");
+ const nav4 = navigationManager.getNavigationForBrowsingContext(
+ browser2.browsingContext
+ );
+ assertNavigation(nav4, SECOND_URL);
+ assertNavigationEvents(events, SECOND_URL, nav4.navigationId, navigableId2);
+ assertUniqueNavigationIds(nav1, nav2, nav3, nav4);
+
+ navigationManager.off("navigation-started", onEvent);
+ navigationManager.off("navigation-stopped", onEvent);
+ navigationManager.stopMonitoring();
+});
+
+add_task(async function test_loadPageWithIframes() {
+ const events = [];
+ const onEvent = (name, data) => events.push({ name, data });
+
+ const navigationManager = new NavigationManager();
+ navigationManager.on("navigation-started", onEvent);
+ navigationManager.on("navigation-stopped", onEvent);
+
+ navigationManager.startMonitoring();
+
+ info("Add a tab with iframes");
+ const testUrl = createTestPageWithFrames();
+ const tab = addTab(gBrowser, testUrl);
+ const browser = tab.linkedBrowser;
+ await BrowserTestUtils.browserLoaded(browser, false, testUrl);
+
+ is(events.length, 8, "Recorded 8 navigation events");
+ const contexts = browser.browsingContext.getAllBrowsingContextsInSubtree();
+
+ const navigations = [];
+ for (const context of contexts) {
+ const navigation =
+ navigationManager.getNavigationForBrowsingContext(context);
+ const navigable = TabManager.getIdForBrowsingContext(context);
+
+ const url = context.currentWindowGlobal.documentURI.spec;
+ assertNavigation(navigation, url);
+ assertNavigationEvents(events, url, navigation.navigationId, navigable);
+ navigations.push(navigation);
+ }
+ assertUniqueNavigationIds(...navigations);
+
+ await BrowserTestUtils.reloadTab(tab);
+
+ is(events.length, 16, "Recorded 8 additional navigation events");
+ const newContexts = browser.browsingContext.getAllBrowsingContextsInSubtree();
+
+ for (const context of newContexts) {
+ const navigation =
+ navigationManager.getNavigationForBrowsingContext(context);
+ const navigable = TabManager.getIdForBrowsingContext(context);
+
+ const url = context.currentWindowGlobal.documentURI.spec;
+ assertNavigation(navigation, url);
+ assertNavigationEvents(events, url, navigation.navigationId, navigable);
+ navigations.push(navigation);
+ }
+ assertUniqueNavigationIds(...navigations);
+
+ navigationManager.off("navigation-started", onEvent);
+ navigationManager.off("navigation-stopped", onEvent);
+ navigationManager.stopMonitoring();
+});
+
+add_task(async function test_loadPageWithCoop() {
+ const tab = addTab(gBrowser, FIRST_COOP_URL);
+ const browser = tab.linkedBrowser;
+ await BrowserTestUtils.browserLoaded(browser, false, FIRST_COOP_URL);
+
+ const events = [];
+ const onEvent = (name, data) => events.push({ name, data });
+
+ const navigationManager = new NavigationManager();
+ navigationManager.on("navigation-started", onEvent);
+ navigationManager.on("navigation-stopped", onEvent);
+
+ navigationManager.startMonitoring();
+
+ const navigableId = TabManager.getIdForBrowser(browser);
+ await loadURL(browser, SECOND_COOP_URL);
+
+ const coopNavigation = navigationManager.getNavigationForBrowsingContext(
+ browser.browsingContext
+ );
+ assertNavigation(coopNavigation, SECOND_COOP_URL);
+
+ is(events.length, 2, "Two events recorded");
+ assertNavigationEvents(
+ events,
+ SECOND_COOP_URL,
+ coopNavigation.navigationId,
+ navigableId
+ );
+
+ navigationManager.off("navigation-started", onEvent);
+ navigationManager.off("navigation-stopped", onEvent);
+ navigationManager.stopMonitoring();
+});
+
+add_task(async function test_sameDocumentNavigation() {
+ const events = [];
+ const onEvent = (name, data) => events.push({ name, data });
+
+ const navigationManager = new NavigationManager();
+ navigationManager.on("navigation-started", onEvent);
+ navigationManager.on("location-changed", onEvent);
+ navigationManager.on("navigation-stopped", onEvent);
+
+ const url = "https://example.com/document-builder.sjs?html=test";
+ const tab = addTab(gBrowser, url);
+ const browser = tab.linkedBrowser;
+ await BrowserTestUtils.browserLoaded(browser);
+
+ navigationManager.startMonitoring();
+ const navigableId = TabManager.getIdForBrowser(browser);
+
+ is(events.length, 0, "No event recorded");
+
+ info("Perform a same-document navigation");
+ let onLocationChanged = navigationManager.once("location-changed");
+ BrowserTestUtils.startLoadingURIString(browser, url + "#hash");
+ await onLocationChanged;
+
+ const hashNavigation = navigationManager.getNavigationForBrowsingContext(
+ browser.browsingContext
+ );
+ is(events.length, 1, "Recorded 1 navigation event");
+ assertNavigationEvents(
+ events,
+ url + "#hash",
+ hashNavigation.navigationId,
+ navigableId,
+ true
+ );
+
+ // Navigate from `url + "#hash"` to `url`, this will trigger a regular
+ // navigation and we can use `loadURL` to properly wait for the navigation to
+ // complete.
+ info("Perform a regular navigation");
+ await loadURL(browser, url);
+
+ const regularNavigation = navigationManager.getNavigationForBrowsingContext(
+ browser.browsingContext
+ );
+ is(events.length, 3, "Recorded 2 additional navigation events");
+ assertNavigationEvents(
+ events,
+ url,
+ regularNavigation.navigationId,
+ navigableId
+ );
+
+ info("Perform another same-document navigation");
+ onLocationChanged = navigationManager.once("location-changed");
+ BrowserTestUtils.startLoadingURIString(browser, url + "#foo");
+ await onLocationChanged;
+
+ const otherHashNavigation = navigationManager.getNavigationForBrowsingContext(
+ browser.browsingContext
+ );
+
+ is(events.length, 4, "Recorded 1 additional navigation event");
+
+ info("Perform a same-hash navigation");
+ onLocationChanged = navigationManager.once("location-changed");
+ BrowserTestUtils.startLoadingURIString(browser, url + "#foo");
+ await onLocationChanged;
+
+ const sameHashNavigation = navigationManager.getNavigationForBrowsingContext(
+ browser.browsingContext
+ );
+
+ is(events.length, 5, "Recorded 1 additional navigation event");
+ assertNavigationEvents(
+ events,
+ url + "#foo",
+ sameHashNavigation.navigationId,
+ navigableId,
+ true
+ );
+
+ assertUniqueNavigationIds([
+ hashNavigation,
+ regularNavigation,
+ otherHashNavigation,
+ sameHashNavigation,
+ ]);
+
+ navigationManager.off("navigation-started", onEvent);
+ navigationManager.off("location-changed", onEvent);
+ navigationManager.off("navigation-stopped", onEvent);
+
+ navigationManager.stopMonitoring();
+});
+
+add_task(async function test_startNavigationAndCloseTab() {
+ const events = [];
+ const onEvent = (name, data) => events.push({ name, data });
+
+ const navigationManager = new NavigationManager();
+ navigationManager.on("navigation-started", onEvent);
+ navigationManager.on("navigation-stopped", onEvent);
+
+ const tab = addTab(gBrowser, FIRST_URL);
+ const browser = tab.linkedBrowser;
+ await BrowserTestUtils.browserLoaded(browser);
+
+ navigationManager.startMonitoring();
+ loadURL(browser, SECOND_URL);
+ gBrowser.removeTab(tab);
+
+ // On top of the assertions below, the test also validates that there is no
+ // unhandled promise rejection related to handling the navigation-started event
+ // for the destroyed browsing context.
+ is(events.length, 0, "No event was received");
+ is(
+ navigationManager.getNavigationForBrowsingContext(browser.browsingContext),
+ null,
+ "No navigation was recorded for the destroyed tab"
+ );
+ navigationManager.stopMonitoring();
+
+ navigationManager.off("navigation-started", onEvent);
+ navigationManager.off("navigation-stopped", onEvent);
+});
diff --git a/remote/shared/test/browser/browser_NavigationManager_failed_navigation.js b/remote/shared/test/browser/browser_NavigationManager_failed_navigation.js
new file mode 100644
index 0000000000..70c695b7ac
--- /dev/null
+++ b/remote/shared/test/browser/browser_NavigationManager_failed_navigation.js
@@ -0,0 +1,99 @@
+/* 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 { NavigationManager } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/NavigationManager.sys.mjs"
+);
+const { TabManager } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/TabManager.sys.mjs"
+);
+
+const TEST_URL = "https://example.com/document-builder.sjs?html=test1";
+const TEST_URL_CLOSED_PORT = "http://127.0.0.1:36325/";
+const TEST_URL_WRONG_URI = "https://www.wronguri.wronguri/";
+
+add_task(async function testClosedPort() {
+ const events = [];
+ const onEvent = (name, data) => events.push({ name, data });
+
+ const navigationManager = new NavigationManager();
+ navigationManager.on("navigation-started", onEvent);
+ navigationManager.on("navigation-stopped", onEvent);
+
+ const tab = addTab(gBrowser, TEST_URL);
+ const browser = tab.linkedBrowser;
+ await BrowserTestUtils.browserLoaded(browser);
+
+ const navigableId = TabManager.getIdForBrowser(browser);
+
+ navigationManager.startMonitoring();
+ is(
+ navigationManager.getNavigationForBrowsingContext(browser.browsingContext),
+ null,
+ "No navigation recorded yet"
+ );
+ is(events.length, 0, "No event recorded");
+
+ await loadURL(browser, TEST_URL_CLOSED_PORT, { maybeErrorPage: true });
+
+ const firstNavigation = navigationManager.getNavigationForBrowsingContext(
+ browser.browsingContext
+ );
+ assertNavigation(firstNavigation, TEST_URL_CLOSED_PORT);
+
+ is(events.length, 2, "Two events recorded");
+ assertNavigationEvents(
+ events,
+ TEST_URL_CLOSED_PORT,
+ firstNavigation.navigationId,
+ navigableId
+ );
+
+ navigationManager.off("navigation-started", onEvent);
+ navigationManager.off("navigation-stopped", onEvent);
+ navigationManager.stopMonitoring();
+});
+
+add_task(async function testWrongURI() {
+ const events = [];
+ const onEvent = (name, data) => events.push({ name, data });
+
+ const navigationManager = new NavigationManager();
+ navigationManager.on("navigation-started", onEvent);
+ navigationManager.on("navigation-stopped", onEvent);
+
+ const tab = addTab(gBrowser, TEST_URL);
+ const browser = tab.linkedBrowser;
+ await BrowserTestUtils.browserLoaded(browser);
+
+ const navigableId = TabManager.getIdForBrowser(browser);
+
+ navigationManager.startMonitoring();
+
+ is(
+ navigationManager.getNavigationForBrowsingContext(browser.browsingContext),
+ null,
+ "No navigation recorded yet"
+ );
+ is(events.length, 0, "No event recorded");
+
+ await loadURL(browser, TEST_URL_WRONG_URI, { maybeErrorPage: true });
+
+ const firstNavigation = navigationManager.getNavigationForBrowsingContext(
+ browser.browsingContext
+ );
+ assertNavigation(firstNavigation, TEST_URL_WRONG_URI);
+
+ is(events.length, 2, "Two events recorded");
+ assertNavigationEvents(
+ events,
+ TEST_URL_WRONG_URI,
+ firstNavigation.navigationId,
+ navigableId
+ );
+
+ navigationManager.off("navigation-started", onEvent);
+ navigationManager.off("navigation-stopped", onEvent);
+ navigationManager.stopMonitoring();
+});
diff --git a/remote/shared/test/browser/browser_NavigationManager_no_navigation.js b/remote/shared/test/browser/browser_NavigationManager_no_navigation.js
new file mode 100644
index 0000000000..370c09d351
--- /dev/null
+++ b/remote/shared/test/browser/browser_NavigationManager_no_navigation.js
@@ -0,0 +1,60 @@
+/* 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 { NavigationManager } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/NavigationManager.sys.mjs"
+);
+const { TabManager } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/TabManager.sys.mjs"
+);
+
+add_task(async function testDocumentOpenWriteClose() {
+ const events = [];
+ const onEvent = (name, data) => events.push({ name, data });
+
+ const navigationManager = new NavigationManager();
+ navigationManager.on("location-changed", onEvent);
+ navigationManager.on("navigation-started", onEvent);
+ navigationManager.on("navigation-stopped", onEvent);
+
+ const url = "https://example.com/document-builder.sjs?html=test";
+
+ const tab = addTab(gBrowser, url);
+ const browser = tab.linkedBrowser;
+ await BrowserTestUtils.browserLoaded(browser);
+
+ navigationManager.startMonitoring();
+ is(events.length, 0, "No event recorded");
+
+ info("Replace the document");
+ await SpecialPowers.spawn(browser, [], async () => {
+ // Note: we need to use eval here to have reduced permissions and avoid
+ // security errors.
+ content.eval(`
+ document.open();
+ document.write("<h1 class='replaced'>Replaced</h1>");
+ document.close();
+ `);
+
+ await ContentTaskUtils.waitForCondition(() =>
+ content.document.querySelector(".replaced")
+ );
+ });
+
+ // See Bug 1844517.
+ // document.open/write/close is identical to same-url + same-hash navigations.
+ todo_is(events.length, 0, "No event recorded after replacing the document");
+
+ info("Reload the page, which should trigger a navigation");
+ await loadURL(browser, url);
+
+ // See Bug 1844517.
+ // document.open/write/close is identical to same-url + same-hash navigations.
+ todo_is(events.length, 2, "Recorded navigation events");
+
+ navigationManager.off("location-changed", onEvent);
+ navigationManager.off("navigation-started", onEvent);
+ navigationManager.off("navigation-stopped", onEvent);
+ navigationManager.stopMonitoring();
+});
diff --git a/remote/shared/test/browser/browser_NavigationManager_notify.js b/remote/shared/test/browser/browser_NavigationManager_notify.js
new file mode 100644
index 0000000000..4dca0f7b4e
--- /dev/null
+++ b/remote/shared/test/browser/browser_NavigationManager_notify.js
@@ -0,0 +1,170 @@
+/* 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 { NavigationManager, notifyNavigationStarted, notifyNavigationStopped } =
+ ChromeUtils.importESModule(
+ "chrome://remote/content/shared/NavigationManager.sys.mjs"
+ );
+const { TabManager } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/TabManager.sys.mjs"
+);
+
+const FIRST_URL = "https://example.com/document-builder.sjs?html=first";
+const SECOND_URL = "https://example.com/document-builder.sjs?html=second";
+
+add_task(async function test_notifyNavigationStartedStopped() {
+ const tab = addTab(gBrowser, FIRST_URL);
+ const browser = tab.linkedBrowser;
+ await BrowserTestUtils.browserLoaded(browser, false, FIRST_URL);
+
+ const events = [];
+ const onEvent = (name, data) => events.push({ name, data });
+
+ const navigationManager = new NavigationManager();
+ navigationManager.on("navigation-started", onEvent);
+ navigationManager.on("navigation-stopped", onEvent);
+
+ navigationManager.startMonitoring();
+
+ const navigableId = TabManager.getIdForBrowser(browser);
+
+ info("Programmatically start a navigation");
+ const startedNavigation = notifyNavigationStarted({
+ contextDetails: {
+ context: browser.browsingContext,
+ },
+ url: SECOND_URL,
+ });
+
+ const navigation = navigationManager.getNavigationForBrowsingContext(
+ browser.browsingContext
+ );
+ assertNavigation(navigation, SECOND_URL);
+
+ is(
+ startedNavigation,
+ navigation,
+ "notifyNavigationStarted returned the expected navigation"
+ );
+ is(events.length, 1, "Only one event recorded");
+
+ info("Attempt to start a navigation while another one is in progress");
+ const alreadyStartedNavigation = notifyNavigationStarted({
+ contextDetails: {
+ context: browser.browsingContext,
+ },
+ url: SECOND_URL,
+ });
+ is(
+ alreadyStartedNavigation,
+ navigation,
+ "notifyNavigationStarted returned the ongoing navigation"
+ );
+ is(events.length, 1, "Still only one event recorded");
+
+ info("Programmatically stop the navigation");
+ const stoppedNavigation = notifyNavigationStopped({
+ contextDetails: {
+ context: browser.browsingContext,
+ },
+ url: SECOND_URL,
+ });
+ is(
+ stoppedNavigation,
+ navigation,
+ "notifyNavigationStopped returned the expected navigation"
+ );
+
+ is(events.length, 2, "Two events recorded");
+ assertNavigationEvents(
+ events,
+ SECOND_URL,
+ navigation.navigationId,
+ navigableId
+ );
+
+ info("Attempt to stop an already stopped navigation");
+ const alreadyStoppedNavigation = notifyNavigationStopped({
+ contextDetails: {
+ context: browser.browsingContext,
+ },
+ url: SECOND_URL,
+ });
+ is(
+ alreadyStoppedNavigation,
+ navigation,
+ "notifyNavigationStopped returned the already stopped navigation"
+ );
+ is(events.length, 2, "Still only two events recorded");
+
+ navigationManager.off("navigation-started", onEvent);
+ navigationManager.off("navigation-stopped", onEvent);
+ navigationManager.stopMonitoring();
+});
+
+add_task(async function test_notifyNavigationWithContextDetails() {
+ const tab = addTab(gBrowser, FIRST_URL);
+ const browser = tab.linkedBrowser;
+ await BrowserTestUtils.browserLoaded(browser, false, FIRST_URL);
+
+ const events = [];
+ const onEvent = (name, data) => events.push({ name, data });
+
+ const navigationManager = new NavigationManager();
+ navigationManager.on("navigation-started", onEvent);
+ navigationManager.on("navigation-stopped", onEvent);
+
+ navigationManager.startMonitoring();
+
+ const navigableId = TabManager.getIdForBrowser(browser);
+
+ info("Programmatically start a navigation using browsing context details");
+ const startedNavigation = notifyNavigationStarted({
+ contextDetails: {
+ browsingContextId: browser.browsingContext.id,
+ browserId: browser.browsingContext.browserId,
+ isTopBrowsingContext: browser.browsingContext.parent === null,
+ },
+ url: SECOND_URL,
+ });
+
+ const navigation = navigationManager.getNavigationForBrowsingContext(
+ browser.browsingContext
+ );
+ assertNavigation(navigation, SECOND_URL);
+
+ is(
+ startedNavigation,
+ navigation,
+ "notifyNavigationStarted returned the expected navigation"
+ );
+ is(events.length, 1, "Only one event recorded");
+
+ info("Programmatically stop the navigation using browsing context details");
+ const stoppedNavigation = notifyNavigationStopped({
+ contextDetails: {
+ browsingContextId: browser.browsingContext.id,
+ browserId: browser.browsingContext.browserId,
+ isTopBrowsingContext: browser.browsingContext.parent === null,
+ },
+ url: SECOND_URL,
+ });
+ is(
+ stoppedNavigation,
+ navigation,
+ "notifyNavigationStopped returned the expected navigation"
+ );
+
+ is(events.length, 2, "Two events recorded");
+ assertNavigationEvents(
+ events,
+ SECOND_URL,
+ navigation.navigationId,
+ navigableId
+ );
+
+ navigationManager.off("navigation-started", onEvent);
+ navigationManager.off("navigation-stopped", onEvent);
+ navigationManager.stopMonitoring();
+});
diff --git a/remote/shared/test/browser/browser_TabManager.js b/remote/shared/test/browser/browser_TabManager.js
new file mode 100644
index 0000000000..fdc0d5c8b1
--- /dev/null
+++ b/remote/shared/test/browser/browser_TabManager.js
@@ -0,0 +1,178 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { TabManager } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/TabManager.sys.mjs"
+);
+
+const FRAME_URL = "https://example.com/document-builder.sjs?html=frame";
+const FRAME_MARKUP = `<iframe src="${encodeURI(FRAME_URL)}"></iframe>`;
+const TEST_URL = `https://example.com/document-builder.sjs?html=${encodeURI(
+ FRAME_MARKUP
+)}`;
+
+add_task(async function test_getBrowsingContextById() {
+ const browser = gBrowser.selectedBrowser;
+
+ is(TabManager.getBrowsingContextById(null), null);
+ is(TabManager.getBrowsingContextById(undefined), null);
+ is(TabManager.getBrowsingContextById("wrong-id"), null);
+
+ info(`Navigate to ${TEST_URL}`);
+ await loadURL(browser, TEST_URL);
+
+ const contexts = browser.browsingContext.getAllBrowsingContextsInSubtree();
+ is(contexts.length, 2, "Top context has 1 child");
+
+ const topContextId = TabManager.getIdForBrowsingContext(contexts[0]);
+ is(TabManager.getBrowsingContextById(topContextId), contexts[0]);
+ const childContextId = TabManager.getIdForBrowsingContext(contexts[1]);
+ is(TabManager.getBrowsingContextById(childContextId), contexts[1]);
+});
+
+add_task(async function test_addTab_focus() {
+ let tabsCount = gBrowser.tabs.length;
+
+ let newTab1, newTab2, newTab3;
+ try {
+ newTab1 = await TabManager.addTab({ focus: true });
+
+ ok(gBrowser.tabs.includes(newTab1), "A new tab was created");
+ is(gBrowser.tabs.length, tabsCount + 1);
+ is(gBrowser.selectedTab, newTab1, "Tab added with focus: true is selected");
+
+ newTab2 = await TabManager.addTab({ focus: false });
+
+ ok(gBrowser.tabs.includes(newTab2), "A new tab was created");
+ is(gBrowser.tabs.length, tabsCount + 2);
+ is(
+ gBrowser.selectedTab,
+ newTab1,
+ "Tab added with focus: false is not selected"
+ );
+
+ newTab3 = await TabManager.addTab();
+
+ ok(gBrowser.tabs.includes(newTab3), "A new tab was created");
+ is(gBrowser.tabs.length, tabsCount + 3);
+ is(
+ gBrowser.selectedTab,
+ newTab1,
+ "Tab added with no focus parameter is not selected (defaults to false)"
+ );
+ } finally {
+ gBrowser.removeTab(newTab1);
+ gBrowser.removeTab(newTab2);
+ gBrowser.removeTab(newTab3);
+ }
+});
+
+add_task(async function test_addTab_referenceTab() {
+ let tab1, tab2, tab3, tab4;
+ try {
+ tab1 = await TabManager.addTab();
+ // Add a second tab with no referenceTab, should be added at the end.
+ tab2 = await TabManager.addTab();
+ // Add a third tab with tab1 as referenceTab, should be added right after tab1.
+ tab3 = await TabManager.addTab({ referenceTab: tab1 });
+ // Add a fourth tab with tab2 as referenceTab, should be added right after tab2.
+ tab4 = await TabManager.addTab({ referenceTab: tab2 });
+
+ // Check that the tab order is as expected: tab1 > tab3 > tab2 > tab4
+ const tab1Index = gBrowser.tabs.indexOf(tab1);
+ is(gBrowser.tabs[tab1Index + 1], tab3);
+ is(gBrowser.tabs[tab1Index + 2], tab2);
+ is(gBrowser.tabs[tab1Index + 3], tab4);
+ } finally {
+ gBrowser.removeTab(tab1);
+ gBrowser.removeTab(tab2);
+ gBrowser.removeTab(tab3);
+ gBrowser.removeTab(tab4);
+ }
+});
+
+add_task(async function test_addTab_window() {
+ const win1 = await BrowserTestUtils.openNewBrowserWindow();
+ const win2 = await BrowserTestUtils.openNewBrowserWindow();
+ try {
+ // openNewBrowserWindow should ensure the new window is focused.
+ is(Services.wm.getMostRecentBrowserWindow(null), win2);
+
+ const newTab1 = await TabManager.addTab({ window: win1 });
+ is(
+ newTab1.ownerGlobal,
+ win1,
+ "The new tab was opened in the specified window"
+ );
+
+ const newTab2 = await TabManager.addTab({ window: win2 });
+ is(
+ newTab2.ownerGlobal,
+ win2,
+ "The new tab was opened in the specified window"
+ );
+
+ const newTab3 = await TabManager.addTab();
+ is(
+ newTab3.ownerGlobal,
+ win2,
+ "The new tab was opened in the foreground window"
+ );
+ } finally {
+ await BrowserTestUtils.closeWindow(win1);
+ await BrowserTestUtils.closeWindow(win2);
+ }
+});
+
+add_task(async function test_getNavigableForBrowsingContext() {
+ const browser = gBrowser.selectedBrowser;
+
+ info(`Navigate to ${TEST_URL}`);
+ await loadURL(browser, TEST_URL);
+
+ const contexts = browser.browsingContext.getAllBrowsingContextsInSubtree();
+ is(contexts.length, 2, "Top context has 1 child");
+
+ // For a top-level browsing context the content browser is returned.
+ const topContext = contexts[0];
+ is(
+ TabManager.getNavigableForBrowsingContext(topContext),
+ browser,
+ "Top-Level browsing context has the content browser as navigable"
+ );
+
+ // For child browsing contexts the browsing context itself is returned.
+ const childContext = contexts[1];
+ is(
+ TabManager.getNavigableForBrowsingContext(childContext),
+ childContext,
+ "Child browsing context has itself as navigable"
+ );
+
+ const invalidValues = [undefined, null, 1, "test", {}, []];
+ for (const invalidValue of invalidValues) {
+ Assert.throws(
+ () => TabManager.getNavigableForBrowsingContext(invalidValue),
+ /Expected browsingContext to be a CanonicalBrowsingContext/
+ );
+ }
+});
+
+add_task(async function test_getTabForBrowsingContext() {
+ const tab = await TabManager.addTab();
+ try {
+ const browser = tab.linkedBrowser;
+
+ info(`Navigate to ${TEST_URL}`);
+ await loadURL(browser, TEST_URL);
+
+ const contexts = browser.browsingContext.getAllBrowsingContextsInSubtree();
+ is(TabManager.getTabForBrowsingContext(contexts[0]), tab);
+ is(TabManager.getTabForBrowsingContext(contexts[1]), tab);
+ is(TabManager.getTabForBrowsingContext(null), null);
+ } finally {
+ gBrowser.removeTab(tab);
+ }
+});
diff --git a/remote/shared/test/browser/browser_UserContextManager.js b/remote/shared/test/browser/browser_UserContextManager.js
new file mode 100644
index 0000000000..2060c2bacd
--- /dev/null
+++ b/remote/shared/test/browser/browser_UserContextManager.js
@@ -0,0 +1,236 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { UserContextManagerClass } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/UserContextManager.sys.mjs"
+);
+
+add_task(async function test_invalid() {
+ const userContextManager = new UserContextManagerClass();
+
+ // Check invalid types for hasUserContextId/getInternalIdById which expects
+ // a string.
+ for (const value of [null, undefined, 1, [], {}]) {
+ is(userContextManager.hasUserContextId(value), false);
+ is(userContextManager.getInternalIdById(value), null);
+ }
+
+ // Check an invalid value for hasUserContextId/getInternalIdById which expects
+ // either "default" or a UUID from Services.uuid.generateUUID.
+ is(userContextManager.hasUserContextId("foo"), false);
+ is(userContextManager.getInternalIdById("foo"), null);
+
+ // Check invalid types for getIdByInternalId which expects a number.
+ for (const value of [null, undefined, "foo", [], {}]) {
+ is(userContextManager.getIdByInternalId(value), null);
+ }
+
+ userContextManager.destroy();
+});
+
+add_task(async function test_default_context() {
+ const userContextManager = new UserContextManagerClass();
+ ok(
+ userContextManager.hasUserContextId("default"),
+ `Context id default is known by the manager`
+ );
+ ok(
+ userContextManager.getUserContextIds().includes("default"),
+ `Context id default is listed by the manager`
+ );
+ is(
+ userContextManager.getInternalIdById("default"),
+ 0,
+ "Default user context has the expected internal id"
+ );
+
+ userContextManager.destroy();
+});
+
+add_task(async function test_new_internal_contexts() {
+ info("Create a new user context with ContextualIdentityService");
+ const beforeInternalId =
+ ContextualIdentityService.create("before").userContextId;
+
+ info("Create the UserContextManager");
+ const userContextManager = new UserContextManagerClass();
+
+ const beforeContextId =
+ userContextManager.getIdByInternalId(beforeInternalId);
+ assertContextAvailable(userContextManager, beforeContextId, beforeInternalId);
+
+ info("Create another user context with ContextualIdentityService");
+ const afterInternalId =
+ ContextualIdentityService.create("after").userContextId;
+ const afterContextId = userContextManager.getIdByInternalId(afterInternalId);
+ assertContextAvailable(userContextManager, afterContextId, afterInternalId);
+
+ info("Delete both user contexts");
+ ContextualIdentityService.remove(beforeInternalId);
+ ContextualIdentityService.remove(afterInternalId);
+ assertContextRemoved(userContextManager, afterContextId, afterInternalId);
+ assertContextRemoved(userContextManager, beforeContextId, beforeInternalId);
+
+ userContextManager.destroy();
+});
+
+add_task(async function test_create_remove_context() {
+ const userContextManager = new UserContextManagerClass();
+
+ for (const closeContextTabs of [true, false]) {
+ info("Create two contexts via createContext");
+ const userContextId1 = userContextManager.createContext();
+ const internalId1 = userContextManager.getInternalIdById(userContextId1);
+ assertContextAvailable(userContextManager, userContextId1);
+
+ const userContextId2 = userContextManager.createContext();
+ const internalId2 = userContextManager.getInternalIdById(userContextId2);
+ assertContextAvailable(userContextManager, userContextId2);
+
+ info("Create tabs in various user contexts");
+ const url = "https://example.com/document-builder.sjs?html=tab";
+ const tabDefault = await addTab(gBrowser, url);
+ const tabContext1 = await addTab(gBrowser, url, {
+ userContextId: internalId1,
+ });
+ const tabContext2 = await addTab(gBrowser, url, {
+ userContextId: internalId2,
+ });
+
+ info("Remove the user context 1 via removeUserContext");
+ userContextManager.removeUserContext(userContextId1, { closeContextTabs });
+
+ assertContextRemoved(userContextManager, userContextId1, internalId1);
+ if (closeContextTabs) {
+ ok(!gBrowser.tabs.includes(tabContext1), "Tab context 1 is closed");
+ } else {
+ ok(gBrowser.tabs.includes(tabContext1), "Tab context 1 is not closed");
+ }
+ ok(gBrowser.tabs.includes(tabDefault), "Tab default is not closed");
+ ok(gBrowser.tabs.includes(tabContext2), "Tab context 2 is not closed");
+
+ info("Remove the user context 2 via removeUserContext");
+ userContextManager.removeUserContext(userContextId2, { closeContextTabs });
+ assertContextRemoved(userContextManager, userContextId2, internalId2);
+ if (closeContextTabs) {
+ ok(!gBrowser.tabs.includes(tabContext2), "Tab context 2 is closed");
+ } else {
+ ok(gBrowser.tabs.includes(tabContext2), "Tab context 2 is not closed");
+ }
+ ok(gBrowser.tabs.includes(tabDefault), "Tab default is not closed");
+ }
+
+ userContextManager.destroy();
+});
+
+add_task(async function test_create_context_prefix() {
+ const userContextManager = new UserContextManagerClass();
+
+ info("Create a context with a custom prefix via createContext");
+ const userContextId = userContextManager.createContext("test_prefix");
+ const internalId = userContextManager.getInternalIdById(userContextId);
+ const identity =
+ ContextualIdentityService.getPublicIdentityFromId(internalId);
+ ok(
+ identity.name.startsWith("test_prefix"),
+ "The new identity used the provided prefix"
+ );
+
+ userContextManager.removeUserContext(userContextId);
+ userContextManager.destroy();
+});
+
+add_task(async function test_several_managers() {
+ const manager1 = new UserContextManagerClass();
+ const manager2 = new UserContextManagerClass();
+
+ info("Create a context via manager1");
+ const contextId1 = manager1.createContext();
+ const internalId = manager1.getInternalIdById(contextId1);
+ assertContextUnknown(manager2, contextId1);
+
+ info("Retrieve the corresponding user context id in manager2");
+ const contextId2 = manager2.getIdByInternalId(internalId);
+ is(
+ typeof contextId2,
+ "string",
+ "manager2 has a valid id for the user context created by manager 1"
+ );
+
+ ok(
+ contextId1 != contextId2,
+ "manager1 and manager2 have different ids for the same internal context id"
+ );
+
+ info("Remove the user context via manager2");
+ manager2.removeUserContext(contextId2);
+
+ info("Check that the user context is removed from both managers");
+ assertContextRemoved(manager1, contextId1, internalId);
+ assertContextRemoved(manager2, contextId2, internalId);
+
+ manager1.destroy();
+ manager2.destroy();
+});
+
+function assertContextAvailable(manager, contextId, expectedInternalId = null) {
+ ok(
+ manager.getUserContextIds().includes(contextId),
+ `Context id ${contextId} is listed by the manager`
+ );
+ ok(
+ manager.hasUserContextId(contextId),
+ `Context id ${contextId} is known by the manager`
+ );
+
+ const internalId = manager.getInternalIdById(contextId);
+ if (expectedInternalId != null) {
+ is(internalId, expectedInternalId, "Internal id has the expected value");
+ }
+
+ is(
+ typeof internalId,
+ "number",
+ `Context id ${contextId} corresponds to a valid internal id (${internalId})`
+ );
+ is(
+ manager.getIdByInternalId(internalId),
+ contextId,
+ `Context id ${contextId} is returned for internal id ${internalId}`
+ );
+ ok(
+ ContextualIdentityService.getPublicUserContextIds().includes(internalId),
+ `User context for context id ${contextId} is found by ContextualIdentityService`
+ );
+}
+
+function assertContextUnknown(manager, contextId) {
+ ok(
+ !manager.getUserContextIds().includes(contextId),
+ `Context id ${contextId} is not listed by the manager`
+ );
+ ok(
+ !manager.hasUserContextId(contextId),
+ `Context id ${contextId} is not known by the manager`
+ );
+ is(
+ manager.getInternalIdById(contextId),
+ null,
+ `Context id ${contextId} does not match any internal id`
+ );
+}
+
+function assertContextRemoved(manager, contextId, internalId) {
+ assertContextUnknown(manager, contextId);
+ is(
+ manager.getIdByInternalId(internalId),
+ null,
+ `Internal id ${internalId} cannot be converted to user context id`
+ );
+ ok(
+ !ContextualIdentityService.getPublicUserContextIds().includes(internalId),
+ `Internal id ${internalId} is not found in ContextualIdentityService`
+ );
+}
diff --git a/remote/shared/test/browser/head.js b/remote/shared/test/browser/head.js
new file mode 100644
index 0000000000..7960d99c9c
--- /dev/null
+++ b/remote/shared/test/browser/head.js
@@ -0,0 +1,205 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Add a new tab in a given browser, pointing to a given URL and automatically
+ * register the cleanup function to remove it at the end of the test.
+ *
+ * @param {Browser} browser
+ * The browser element where the tab should be added.
+ * @param {string} url
+ * The URL for the tab.
+ * @param {object=} options
+ * Options object to forward to BrowserTestUtils.addTab.
+ * @returns {Tab}
+ * The created tab.
+ */
+function addTab(browser, url, options) {
+ const tab = BrowserTestUtils.addTab(browser, url, options);
+ registerCleanupFunction(() => browser.removeTab(tab));
+ return tab;
+}
+
+/**
+ * Check if a given navigation is valid and has the expected url.
+ *
+ * @param {object} navigation
+ * The navigation to validate.
+ * @param {string} expectedUrl
+ * The expected url for the navigation.
+ */
+function assertNavigation(navigation, expectedUrl) {
+ ok(!!navigation, "Retrieved a navigation");
+ is(navigation.url, expectedUrl, "Navigation has the expected URL");
+ is(
+ typeof navigation.navigationId,
+ "string",
+ "Navigation has a string navigationId"
+ );
+}
+
+/**
+ * Check a pair of navigation events have the expected URL, navigation id and
+ * navigable id. The pair is expected to be ordered as follows: navigation-started
+ * and then navigation-stopped.
+ *
+ * @param {Array<object>} events
+ * The pair of events to validate.
+ * @param {string} url
+ * The expected url for the navigation.
+ * @param {string} navigationId
+ * The expected navigation id.
+ * @param {string} navigableId
+ * The expected navigable id.
+ * @param {boolean} isSameDocument
+ * If the navigation should be a same document navigation.
+ */
+function assertNavigationEvents(
+ events,
+ url,
+ navigationId,
+ navigableId,
+ isSameDocument
+) {
+ const expectedEvents = isSameDocument ? 1 : 2;
+
+ const navigationEvents = events.filter(
+ e => e.data.navigationId == navigationId
+ );
+ is(
+ navigationEvents.length,
+ expectedEvents,
+ `Found ${expectedEvents} events for navigationId ${navigationId}`
+ );
+
+ if (isSameDocument) {
+ // Check there are no navigation-started/stopped events.
+ ok(!navigationEvents.some(e => e.name === "navigation-started"));
+ ok(!navigationEvents.some(e => e.name === "navigation-stopped"));
+
+ const locationChanged = navigationEvents.find(
+ e => e.name === "location-changed"
+ );
+ is(locationChanged.name, "location-changed", "event has the expected name");
+ is(locationChanged.data.url, url, "event has the expected url");
+ is(
+ locationChanged.data.navigableId,
+ navigableId,
+ "event has the expected navigable"
+ );
+ } else {
+ // Check there is no location-changed event.
+ ok(!navigationEvents.some(e => e.name === "location-changed"));
+
+ const started = navigationEvents.find(e => e.name === "navigation-started");
+ const stopped = navigationEvents.find(e => e.name === "navigation-stopped");
+
+ // Check navigation-started
+ is(started.name, "navigation-started", "event has the expected name");
+ is(started.data.url, url, "event has the expected url");
+ is(
+ started.data.navigableId,
+ navigableId,
+ "event has the expected navigable"
+ );
+
+ // Check navigation-stopped
+ is(stopped.name, "navigation-stopped", "event has the expected name");
+ is(stopped.data.url, url, "event has the expected url");
+ is(
+ stopped.data.navigableId,
+ navigableId,
+ "event has the expected navigable"
+ );
+ }
+}
+
+/**
+ * Assert that the given navigations all have unique/different ids.
+ *
+ * @param {Array<object>} navigations
+ * The navigations to validate.
+ */
+function assertUniqueNavigationIds(...navigations) {
+ const ids = navigations.map(navigation => navigation.navigationId);
+ is(new Set(ids).size, ids.length, "Navigation ids are all different");
+}
+
+/**
+ * Create a document-builder based page with an iframe served by a given domain.
+ *
+ * @param {string} domain
+ * The domain which should serve the page.
+ * @returns {string}
+ * The URI for the page.
+ */
+function createFrame(domain) {
+ return createFrameForUri(
+ `https://${domain}/document-builder.sjs?html=frame-${domain}`
+ );
+}
+
+/**
+ * Create the markup for an iframe pointing to a given URI.
+ *
+ * @param {string} uri
+ * The uri for the iframe.
+ * @returns {string}
+ * The iframe markup.
+ */
+function createFrameForUri(uri) {
+ return `<iframe src="${encodeURI(uri)}"></iframe>`;
+}
+
+/**
+ * Create the URL for a test page containing nested iframes
+ *
+ * @returns {string}
+ * The test page url.
+ */
+function createTestPageWithFrames() {
+ // Create the markup for an example.net frame nested in an example.com frame.
+ const NESTED_FRAME_MARKUP = createFrameForUri(
+ `https://example.org/document-builder.sjs?html=${createFrame(
+ "example.net"
+ )}`
+ );
+
+ // Combine the nested frame markup created above with an example.com frame.
+ const TEST_URI_MARKUP = `${NESTED_FRAME_MARKUP}${createFrame("example.com")}`;
+
+ // Create the test page URI on example.org.
+ return `https://example.org/document-builder.sjs?html=${encodeURI(
+ TEST_URI_MARKUP
+ )}`;
+}
+
+/**
+ * Load the provided url in an existing browser.
+ *
+ * @param {Browser} browser
+ * The browser element where the URL should be loaded.
+ * @param {string} url
+ * The URL to load.
+ * @param {object=} options
+ * @param {boolean} options.includeSubFrames
+ * Whether we should monitor load of sub frames. Defaults to false.
+ * @param {boolean} options.maybeErrorPage
+ * Whether we might reach an error page or not. Defaults to false.
+ * @returns {Promise}
+ * Promise which will resolve when the page is loaded with the expected url.
+ */
+async function loadURL(browser, url, options = {}) {
+ const { includeSubFrames = false, maybeErrorPage = false } = options;
+ const loaded = BrowserTestUtils.browserLoaded(
+ browser,
+ includeSubFrames,
+ url,
+ maybeErrorPage
+ );
+ BrowserTestUtils.startLoadingURIString(browser, url);
+ return loaded;
+}
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"]