diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /remote/shared/test | |
parent | Initial commit. (diff) | |
download | firefox-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')
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"] |