summaryrefslogtreecommitdiffstats
path: root/remote/shared/test/browser
diff options
context:
space:
mode:
Diffstat (limited to '')
-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
8 files changed, 1336 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;
+}