diff options
Diffstat (limited to 'remote/shared/test/browser')
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; +} |