diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /dom/tests/browser | |
parent | Initial commit. (diff) | |
download | firefox-esr-upstream.tar.xz firefox-esr-upstream.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/tests/browser')
98 files changed, 8630 insertions, 0 deletions
diff --git a/dom/tests/browser/.eslintrc.js b/dom/tests/browser/.eslintrc.js new file mode 100644 index 0000000000..e247fd78e9 --- /dev/null +++ b/dom/tests/browser/.eslintrc.js @@ -0,0 +1,12 @@ +"use strict"; + +module.exports = { + overrides: [ + { + files: ["file_module_loaded.js", "file_module_loaded2.js"], + parserOptions: { + sourceType: "module", + }, + }, + ], +}; diff --git a/dom/tests/browser/beforeunload_test_page.html b/dom/tests/browser/beforeunload_test_page.html new file mode 100644 index 0000000000..12dc636e23 --- /dev/null +++ b/dom/tests/browser/beforeunload_test_page.html @@ -0,0 +1,92 @@ +<!doctype html> +<html> +<body> + +<p> + There are currently <span id="totalInnerHandlers">0</span> beforeunload handlers registered in this frame. +</p> +<p> + There are currently <span id="totalOuterHandlers">0</span> beforeunload handlers registered on my subframe. +</p> + +<iframe src="about:blank" id="subframe"></iframe> + +<script> + this.BeforeUnloader = { + _innerEventHandlers: [], + _outerEventHandlers: [], + + get $totalInner() { + delete this.$totalInner; + return this.$totalInner = document.getElementById("totalInnerHandlers"); + }, + + get $totalOuter() { + delete this.$totalOuter; + return this.$totalOuter = document.getElementById("totalOuterHandlers"); + }, + + get $subframe() { + delete this.$subframe; + return this.$subframe = document.getElementById("subframe"); + }, + + pushInner(howMany) { + for (let i = 0; i < howMany; ++i) { + let func = () => {}; + this._innerEventHandlers.push(func); + addEventListener("beforeunload", func); + } + + this.$totalInner.textContent = this._innerEventHandlers.length; + }, + + popInner(howMany) { + for (let i = 0; i < howMany; ++i) { + let func = this._innerEventHandlers.pop(); + if (func) { + removeEventListener("beforeunload", func); + } + } + + this.$totalInner.textContent = this._innerEventHandlers.length; + }, + + pushOuter(howMany) { + if (!this.$subframe.parentNode) { + throw new Error("Subframe I'm holding a reference to detached!"); + } + + for (let i = 0; i < howMany; ++i) { + let func = () => {}; + this._outerEventHandlers.push(func); + this.$subframe.contentWindow.addEventListener("beforeunload", func); + } + + this.$totalOuter.textContent = this._outerEventHandlers.length; + }, + + popOuter(howMany) { + if (!this.$subframe.parentNode) { + throw new Error("Subframe I'm holding a reference to detached!"); + } + + for (let i = 0; i < howMany; ++i) { + let func = this._outerEventHandlers.pop(); + if (func) { + this.$subframe.contentWindow.removeEventListener("beforeunload", func); + } + } + + this.$totalOuter.textContent = this._outerEventHandlers.length; + }, + + reset() { + this.popInner(this._innerEventHandlers.length); + this.popOuter(this._outerEventHandlers.length); + }, + }; +</script> + +</body> +</html> diff --git a/dom/tests/browser/browser.ini b/dom/tests/browser/browser.ini new file mode 100644 index 0000000000..2fae2c0f4a --- /dev/null +++ b/dom/tests/browser/browser.ini @@ -0,0 +1,140 @@ +[DEFAULT] +support-files = + browser_frame_elements.html + page_privatestorageevent.html + page_localStorage.js + page_localstorage.html + page_localstorage_coop+coep.html + page_localstorage_coop+coep.html^headers^ + page_localstorage_snapshotting.html + position.html + test-console-api.html + test_bug1004814.html + worker_bug1004814.js + geo_leak_test.html + dummy.html + dummy.png + helper_localStorage.js + !/dom/tests/mochitest/geolocation/network_geolocation.sjs + +[browser_autofocus_background.js] +[browser_autofocus_preference.js] +[browser_beforeunload_between_chrome_content.js] +https_first_disabled = true +[browser_bug396843.js] +[browser_bug1004814.js] +[browser_bug1008941_dismissGeolocationHanger.js] +tags = geolocation +[browser_bug1236512.js] +skip-if = os != "mac" +[browser_bug1238427.js] +[browser_bug1316330.js] +[browser_bug1563629.js] +support-files = + file_postMessage_parent.html +[browser_bug1685807.js] +support-files = + file_bug1685807.html +skip-if = + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure +[browser_cancel_keydown_keypress_event.js] +support-files = + prevent_return_key.html +[browser_ConsoleAPI_originAttributes.js] +[browser_ConsoleStorageAPITests.js] +[browser_ConsoleStoragePBTest_perwindowpb.js] +[browser_data_document_crossOriginIsolated.js] +[browser_focus_steal_from_chrome.js] +[browser_focus_steal_from_chrome_during_mousedown.js] +[browser_form_associated_custom_elements_validity.js] +support-files = + file_empty.html +[browser_frame_elements.js] +[browser_hasbeforeunload.js] +https_first_disabled = true +support-files = + beforeunload_test_page.html +[browser_hasActivePeerConnections.js] +support-files = + create_webrtc_peer_connection.html +skip-if = + os == "linux" && bits == 64 # Bug 1742012 + os == "win" && debug # Bug 1742012 + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure +[browser_localStorage_e10s.js] +https_first_disabled = true +fail-if = fission +skip-if = + verify + tsan # Times out on TSan intermittently. +[browser_localStorage_fis.js] +skip-if = + verify + tsan + !fission +[browser_localStorage_privatestorageevent.js] +[browser_localStorage_snapshotting.js] +[browser_persist_cookies.js] +support-files = + set-samesite-cookies-and-redirect.sjs + mimeme.sjs +[browser_persist_image_accept.js] +[browser_persist_mixed_content_image.js] +support-files = + test_mixed_content_image.html +[browser_pointerlock_warning.js] +[browser_sessionStorage_navigation.js] +skip-if = + os == "linux" && bits == 64 && !debug # Bug 1712961 +support-files = + file_empty.html + file_coop_coep.html + file_coop_coep.html^headers^ +[browser_test_focus_after_modal_state.js] +skip-if = verify +support-files = + focus_after_prompt.html +[browser_test_new_window_from_content.js] +tags = openwindow +skip-if = toolkit == 'android' + (os == "linux" && debug) # see bug 1261495 for Linux debug time outs +support-files = + test_new_window_from_content_child.html +[browser_test_toolbars_visibility.js] +https_first_disabled = true +support-files = + test_new_window_from_content_child.html +skip-if = + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure +[browser_unlinkable_about_page_can_load_module_scripts.js] +support-files = + file_load_module_script.html + file_module_loaded.js + file_module_loaded2.js +[browser_xhr_sandbox.js] +[browser_noopener.js] +skip-if = verify && debug && (os == 'linux') +support-files = + test_noopener_source.html + test_noopener_target.html +[browser_noopener_null_uri.js] +[browser_wakelock.js] +[browser_keypressTelemetry.js] +skip-if = true +[browser_windowProxy_transplant.js] +support-files = + file_postMessage_parent.html +[browser_navigate_replace_browsingcontext.js] +[browser_persist_cross_origin_iframe.js] +skip-if = + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure +support-files = + image.html +[browser_bytecode_cache_asm_js.js] +support-files = + page_bytecode_cache_asm_js.html + page_bytecode_cache_asm_js.js +[browser_bug1709346.js] +support-files = + load_forever.sjs + file_empty_cross_site_frame.html diff --git a/dom/tests/browser/browser_ConsoleAPI_originAttributes.js b/dom/tests/browser/browser_ConsoleAPI_originAttributes.js new file mode 100644 index 0000000000..9e96dba728 --- /dev/null +++ b/dom/tests/browser/browser_ConsoleAPI_originAttributes.js @@ -0,0 +1,108 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const ConsoleAPIStorage = Cc["@mozilla.org/consoleAPI-storage;1"].getService( + Ci.nsIConsoleAPIStorage +); + +const { WebExtensionPolicy } = Cu.getGlobalForObject( + ChromeUtils.importESModule("resource://gre/modules/AppConstants.sys.mjs") +); + +const FAKE_ADDON_ID = "test-webext-addon@mozilla.org"; +const EXPECTED_CONSOLE_ID = `addon/${FAKE_ADDON_ID}`; +const EXPECTED_CONSOLE_MESSAGE_CONTENT = "fake-webext-addon-test-log-message"; + +const ConsoleObserver = { + init() { + ConsoleAPIStorage.addLogEventListener( + this.observe, + Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal) + ); + }, + + uninit() { + ConsoleAPIStorage.removeLogEventListener(this.observe); + }, + + observe(aSubject) { + let consoleAPIMessage = aSubject.wrappedJSObject; + + is( + consoleAPIMessage.arguments[0], + EXPECTED_CONSOLE_MESSAGE_CONTENT, + "the consoleAPIMessage contains the expected message" + ); + + is( + consoleAPIMessage.addonId, + FAKE_ADDON_ID, + "the consoleAPImessage originAttributes contains the expected addonId" + ); + + let cachedMessages = ConsoleAPIStorage.getEvents().filter(msg => { + return msg.addonId == FAKE_ADDON_ID; + }); + + is( + cachedMessages.length, + 1, + "found the expected cached console messages from the addon" + ); + is( + cachedMessages[0] && cachedMessages[0].addonId, + FAKE_ADDON_ID, + "the cached message originAttributes contains the expected addonId" + ); + + finish(); + }, +}; + +function test() { + ConsoleObserver.init(); + + waitForExplicitFinish(); + + let uuidGenerator = Services.uuid; + let uuid = uuidGenerator.generateUUID().number; + uuid = uuid.slice(1, -1); // Strip { and } off the UUID. + + const url = `moz-extension://${uuid}/`; + let policy = new WebExtensionPolicy({ + id: FAKE_ADDON_ID, + mozExtensionHostname: uuid, + baseURL: "file:///", + allowedOrigins: new MatchPatternSet([]), + localizeCallback() {}, + }); + policy.active = true; + + let baseURI = Services.io.newURI(url); + let principal = Services.scriptSecurityManager.createContentPrincipal( + baseURI, + {} + ); + + let chromeWebNav = Services.appShell.createWindowlessBrowser(true); + let docShell = chromeWebNav.docShell; + docShell.createAboutBlankContentViewer(principal, principal); + + info("fake webextension docShell created"); + + registerCleanupFunction(function () { + policy.active = false; + if (chromeWebNav) { + chromeWebNav.close(); + chromeWebNav = null; + } + ConsoleObserver.uninit(); + }); + + let window = docShell.contentViewer.DOMDocument.defaultView; + window.eval(`console.log("${EXPECTED_CONSOLE_MESSAGE_CONTENT}");`); + chromeWebNav.close(); + chromeWebNav = null; + + info("fake webextension page logged a console api message"); +} diff --git a/dom/tests/browser/browser_ConsoleStorageAPITests.js b/dom/tests/browser/browser_ConsoleStorageAPITests.js new file mode 100644 index 0000000000..f28db48a91 --- /dev/null +++ b/dom/tests/browser/browser_ConsoleStorageAPITests.js @@ -0,0 +1,108 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const TEST_URI = + "http://example.com/browser/dom/tests/browser/test-console-api.html"; + +function tearDown() { + while (gBrowser.tabs.length > 1) { + gBrowser.removeCurrentTab(); + } +} + +add_task(async function () { + // Don't cache removed tabs, so "clear console cache on tab close" triggers. + await SpecialPowers.pushPrefEnv({ set: [["browser.tabs.max_tabs_undo", 0]] }); + + registerCleanupFunction(tearDown); + + info( + "Open a keepalive tab in the background to make sure we don't accidentally kill the content process" + ); + var keepaliveTab = await BrowserTestUtils.addTab( + gBrowser, + "data:text/html,<meta charset=utf8>Keep Alive Tab" + ); + + info("Open the main tab to run the test in"); + var tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URI); + var browser = gBrowser.selectedBrowser; + + const windowId = await ContentTask.spawn(browser, null, async function (opt) { + let ConsoleAPIStorage = Cc["@mozilla.org/consoleAPI-storage;1"].getService( + Ci.nsIConsoleAPIStorage + ); + + let observerPromise = new Promise(resolve => { + let apiCallCount = 0; + function observe(aSubject) { + apiCallCount++; + info(`Received ${apiCallCount} console log events`); + if (apiCallCount == 4) { + ConsoleAPIStorage.removeLogEventListener(observe); + resolve(); + } + } + + info("Setting up observer"); + ConsoleAPIStorage.addLogEventListener( + observe, + Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal) + ); + }); + + info("Emit a few console API logs"); + content.console.log("this", "is", "a", "log", "message"); + content.console.info("this", "is", "a", "info", "message"); + content.console.warn("this", "is", "a", "warn", "message"); + content.console.error("this", "is", "a", "error", "message"); + + info("Wait for the corresponding log event"); + await observerPromise; + + const innerWindowId = content.windowGlobalChild.innerWindowId; + const events = ConsoleAPIStorage.getEvents(innerWindowId).filter( + message => + message.arguments[0] === "this" && + message.arguments[1] === "is" && + message.arguments[2] === "a" && + message.arguments[4] === "message" + ); + is(events.length, 4, "The storage service got the messages we emitted"); + + info("Ensure clearEvents does remove the events from storage"); + ConsoleAPIStorage.clearEvents(); + is(ConsoleAPIStorage.getEvents(innerWindowId).length, 0, "Cleared Storage"); + + return content.windowGlobalChild.innerWindowId; + }); + + await SpecialPowers.spawn(browser, [], function () { + // make sure a closed window's events are in fact removed from + // the storage cache + content.console.log("adding a new event"); + }); + + info("Close the window"); + gBrowser.removeTab(tab, { animate: false }); + // Ensure actual window destruction is not delayed (too long). + SpecialPowers.DOMWindowUtils.garbageCollect(); + + // Spawn the check in the keepaliveTab, so that we can read the ConsoleAPIStorage correctly + gBrowser.selectedTab = keepaliveTab; + browser = gBrowser.selectedBrowser; + + // Spin the event loop to make sure everything is cleared. + await SpecialPowers.spawn(browser, [], () => {}); + + await SpecialPowers.spawn(browser, [windowId], function (windowId) { + var ConsoleAPIStorage = Cc["@mozilla.org/consoleAPI-storage;1"].getService( + Ci.nsIConsoleAPIStorage + ); + is( + ConsoleAPIStorage.getEvents(windowId).length, + 0, + "tab close is clearing the cache" + ); + }); +}); diff --git a/dom/tests/browser/browser_ConsoleStoragePBTest_perwindowpb.js b/dom/tests/browser/browser_ConsoleStoragePBTest_perwindowpb.js new file mode 100644 index 0000000000..79b5a820ff --- /dev/null +++ b/dom/tests/browser/browser_ConsoleStoragePBTest_perwindowpb.js @@ -0,0 +1,93 @@ +/* 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/. */ +function test() { + // initialization + waitForExplicitFinish(); + let windowsToClose = []; + let innerID; + let beforeEvents; + let afterEvents; + let storageShouldOccur; + let testURI = + "http://example.com/browser/dom/tests/browser/test-console-api.html"; + let ConsoleAPIStorage = Cc["@mozilla.org/consoleAPI-storage;1"].getService( + Ci.nsIConsoleAPIStorage + ); + + function getInnerWindowId(aWindow) { + return aWindow.windowGlobalChild.innerWindowId; + } + + function whenNewWindowLoaded(aOptions, aCallback) { + let win = OpenBrowserWindow(aOptions); + win.addEventListener( + "load", + function () { + aCallback(win); + }, + { once: true } + ); + } + + function doTest(aIsPrivateMode, aWindow, aCallback) { + BrowserTestUtils.browserLoaded(aWindow.gBrowser.selectedBrowser).then( + () => { + function observe(aSubject) { + afterEvents = ConsoleAPIStorage.getEvents(innerID); + is( + beforeEvents.length == afterEvents.length - 1, + storageShouldOccur, + "storage should" + (storageShouldOccur ? "" : " not") + " occur" + ); + + executeSoon(function () { + ConsoleAPIStorage.removeLogEventListener(observe); + aCallback(); + }); + } + + ConsoleAPIStorage.addLogEventListener( + observe, + aWindow.document.nodePrincipal + ); + aWindow.nativeConsole.log( + "foo bar baz (private: " + aIsPrivateMode + ")" + ); + } + ); + + // We expect that console API messages are always stored. + storageShouldOccur = true; + innerID = getInnerWindowId(aWindow); + beforeEvents = ConsoleAPIStorage.getEvents(innerID); + BrowserTestUtils.loadURIString(aWindow.gBrowser.selectedBrowser, testURI); + } + + function testOnWindow(aOptions, aCallback) { + whenNewWindowLoaded(aOptions, function (aWin) { + windowsToClose.push(aWin); + // execute should only be called when need, like when you are opening + // web pages on the test. If calling executeSoon() is not necesary, then + // call whenNewWindowLoaded() instead of testOnWindow() on your test. + executeSoon(() => aCallback(aWin)); + }); + } + + // this function is called after calling finish() on the test. + registerCleanupFunction(function () { + windowsToClose.forEach(function (aWin) { + aWin.close(); + }); + }); + + // test first when not on private mode + testOnWindow({}, function (aWin) { + doTest(false, aWin, function () { + // then test when on private mode + testOnWindow({ private: true }, function (aWin) { + doTest(true, aWin, finish); + }); + }); + }); +} diff --git a/dom/tests/browser/browser_autofocus_background.js b/dom/tests/browser/browser_autofocus_background.js new file mode 100644 index 0000000000..ecac9a9a98 --- /dev/null +++ b/dom/tests/browser/browser_autofocus_background.js @@ -0,0 +1,59 @@ +add_task(async function () { + const URL = + "data:text/html,<!DOCTYPE html><html><body><input autofocus id='target'></body></html>"; + const foregroundTab = gBrowser.selectedTab; + const backgroundTab = BrowserTestUtils.addTab(gBrowser); + + // Ensure tab is still in the foreground. + is( + gBrowser.selectedTab, + foregroundTab, + "foregroundTab should still be selected" + ); + + // Load the second tab in the background. + const loadedPromise = BrowserTestUtils.browserLoaded( + backgroundTab.linkedBrowser, + /* includesubframes */ false, + URL + ); + BrowserTestUtils.loadURIString(backgroundTab.linkedBrowser, URL); + await loadedPromise; + + // Get active element in the tab. + let tagName = await SpecialPowers.spawn( + backgroundTab.linkedBrowser, + [], + async function () { + // Spec asks us to flush autofocus candidates in the + // `update-the-rendering` step, so we need to wait + // for a rAF to ensure autofocus candidates are + // flushed. + await new Promise(r => { + content.requestAnimationFrame(r); + }); + return content.document.activeElement.tagName; + } + ); + + is( + tagName, + "INPUT", + "The background tab's focused element should be the <input>" + ); + + is( + gBrowser.selectedTab, + foregroundTab, + "foregroundTab tab should still be selected, shouldn't cause a tab switch" + ); + + is( + document.activeElement, + foregroundTab.linkedBrowser, + "The background tab's focused element should not cause the tab to be selected" + ); + + // Cleaning up. + BrowserTestUtils.removeTab(backgroundTab); +}); diff --git a/dom/tests/browser/browser_autofocus_preference.js b/dom/tests/browser/browser_autofocus_preference.js new file mode 100644 index 0000000000..fc8b318a17 --- /dev/null +++ b/dom/tests/browser/browser_autofocus_preference.js @@ -0,0 +1,16 @@ +add_task(async function () { + await SpecialPowers.pushPrefEnv({ set: [["browser.autofocus", false]] }); + + const url = + "data:text/html,<!DOCTYPE html><html><body><input autofocus><button autofocus></button><textarea autofocus></textarea><select autofocus></select></body></html>"; + + let loadedPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, url); + await loadedPromise; + + await new Promise(resolve => executeSoon(resolve)); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () { + is(content.document.activeElement, content.document.body, "body focused"); + }); +}); diff --git a/dom/tests/browser/browser_beforeunload_between_chrome_content.js b/dom/tests/browser/browser_beforeunload_between_chrome_content.js new file mode 100644 index 0000000000..67579592ed --- /dev/null +++ b/dom/tests/browser/browser_beforeunload_between_chrome_content.js @@ -0,0 +1,152 @@ +const TEST_URL = "http://www.example.com/browser/dom/tests/browser/dummy.html"; + +const { PromptTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromptTestUtils.sys.mjs" +); + +function pageScript() { + window.addEventListener( + "beforeunload", + function (event) { + var str = "Leaving?"; + event.returnValue = str; + return str; + }, + true + ); +} + +function injectBeforeUnload(browser) { + return ContentTask.spawn(browser, null, async function () { + content.window.addEventListener( + "beforeunload", + function (event) { + sendAsyncMessage("Test:OnBeforeUnloadReceived"); + var str = "Leaving?"; + event.returnValue = str; + return str; + }, + true + ); + }); +} + +// Wait for onbeforeunload dialog, and dismiss it immediately. +function awaitAndCloseBeforeUnloadDialog(browser, doStayOnPage) { + return PromptTestUtils.handleNextPrompt( + browser, + { modalType: Services.prompt.MODAL_TYPE_CONTENT, promptType: "confirmEx" }, + { buttonNumClick: doStayOnPage ? 1 : 0 } + ); +} + +SpecialPowers.pushPrefEnv({ + set: [["dom.require_user_interaction_for_beforeunload", false]], +}); + +/** + * Test navigation from a content page to a chrome page. Also check that only + * one beforeunload event is fired. + */ +/* global messageManager */ +add_task(async function () { + let beforeUnloadCount = 0; + messageManager.addMessageListener("Test:OnBeforeUnloadReceived", function () { + beforeUnloadCount++; + }); + + // Open a content page. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + let browser = tab.linkedBrowser; + + ok(browser.isRemoteBrowser, "Browser should be remote."); + + await injectBeforeUnload(browser); + // Navigate to a chrome page. + let dialogShown1 = awaitAndCloseBeforeUnloadDialog(browser, false); + BrowserTestUtils.loadURIString(browser, "about:support"); + await Promise.all([dialogShown1, BrowserTestUtils.browserLoaded(browser)]); + + is(beforeUnloadCount, 1, "Should have received one beforeunload event."); + ok(!browser.isRemoteBrowser, "Browser should not be remote."); + + // Go back to content page. + ok(gBrowser.webNavigation.canGoBack, "Should be able to go back."); + gBrowser.goBack(); + await BrowserTestUtils.browserLoaded(browser); + await injectBeforeUnload(browser); + + // Test that going forward triggers beforeunload prompt as well. + ok(gBrowser.webNavigation.canGoForward, "Should be able to go forward."); + let dialogShown2 = awaitAndCloseBeforeUnloadDialog(false); + gBrowser.goForward(); + await Promise.all([dialogShown2, BrowserTestUtils.browserLoaded(browser)]); + is(beforeUnloadCount, 2, "Should have received two beforeunload events."); + + BrowserTestUtils.removeTab(tab); +}); + +/** + * Test navigation from a chrome page to a content page. Also check that only + * one beforeunload event is fired. + */ +add_task(async function () { + let beforeUnloadCount = 0; + messageManager.addMessageListener("Test:OnBeforeUnloadReceived", function () { + beforeUnloadCount++; + }); + + // Open a chrome page. + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:support" + ); + let browser = tab.linkedBrowser; + + ok(!browser.isRemoteBrowser, "Browser should not be remote."); + await ContentTask.spawn(browser, null, async function () { + content.window.addEventListener( + "beforeunload", + function (event) { + sendAsyncMessage("Test:OnBeforeUnloadReceived"); + var str = "Leaving?"; + event.returnValue = str; + return str; + }, + true + ); + }); + + // Navigate to a content page. + let dialogShown1 = awaitAndCloseBeforeUnloadDialog(false); + BrowserTestUtils.loadURIString(browser, TEST_URL); + await Promise.all([dialogShown1, BrowserTestUtils.browserLoaded(browser)]); + is(beforeUnloadCount, 1, "Should have received one beforeunload event."); + ok(browser.isRemoteBrowser, "Browser should be remote."); + + // Go back to chrome page. + ok(gBrowser.webNavigation.canGoBack, "Should be able to go back."); + gBrowser.goBack(); + await BrowserTestUtils.browserLoaded(browser); + await ContentTask.spawn(browser, null, async function () { + content.window.addEventListener( + "beforeunload", + function (event) { + sendAsyncMessage("Test:OnBeforeUnloadReceived"); + var str = "Leaving?"; + event.returnValue = str; + return str; + }, + true + ); + }); + + // Test that going forward triggers beforeunload prompt as well. + ok(gBrowser.webNavigation.canGoForward, "Should be able to go forward."); + let dialogShown2 = awaitAndCloseBeforeUnloadDialog(false); + gBrowser.goForward(); + await Promise.all([dialogShown2, BrowserTestUtils.browserLoaded(browser)]); + is(beforeUnloadCount, 2, "Should have received two beforeunload events."); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/dom/tests/browser/browser_bug1004814.js b/dom/tests/browser/browser_bug1004814.js new file mode 100644 index 0000000000..efe3bd44f9 --- /dev/null +++ b/dom/tests/browser/browser_bug1004814.js @@ -0,0 +1,46 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +add_task(async function () { + const TEST_URI = + "http://example.com/browser/dom/tests/browser/test_bug1004814.html"; + + await BrowserTestUtils.withNewTab(TEST_URI, async aBrowser => { + let duration = await SpecialPowers.spawn(aBrowser, [], function (opts) { + const ConsoleAPIStorage = Cc[ + "@mozilla.org/consoleAPI-storage;1" + ].getService(Ci.nsIConsoleAPIStorage); + + return new Promise(resolve => { + function observe(aSubject) { + var obj = aSubject.wrappedJSObject; + if ( + obj.arguments.length != 1 || + obj.arguments[0] != "bug1004814" || + obj.level != "timeEnd" + ) { + return; + } + + ConsoleAPIStorage.removeLogEventListener(observe); + resolve(obj.timer.duration); + } + + ConsoleAPIStorage.addLogEventListener( + observe, + Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal) + ); + + var w = new content.Worker("worker_bug1004814.js"); + w.postMessage(true); + }); + }); + + ok( + duration > 0, + "ConsoleEvent.timer.duration > 0: " + duration + " ~ 200ms" + ); + }); +}); diff --git a/dom/tests/browser/browser_bug1008941_dismissGeolocationHanger.js b/dom/tests/browser/browser_bug1008941_dismissGeolocationHanger.js new file mode 100644 index 0000000000..21f03721c5 --- /dev/null +++ b/dom/tests/browser/browser_bug1008941_dismissGeolocationHanger.js @@ -0,0 +1,39 @@ +/* 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 TEST_URI = + // eslint-disable-next-line no-useless-concat + "https://example.com/" + "browser/dom/tests/browser/position.html"; + +add_task(async function testDismissHanger() { + info( + "Check that location is not shared when dismissing the geolocation hanger" + ); + + let promisePanelShown = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown", + true + ); + await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URI); + await promisePanelShown; + + // click outside the Geolocation hanger to dismiss it + window.document.getElementById("nav-bar").click(); + info("Clicked outside the Geolocation panel to dismiss it"); + + let hasLocation = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + async function () { + return content.document.body.innerHTML.includes("location..."); + } + ); + + ok(hasLocation, "Location is not shared"); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/dom/tests/browser/browser_bug1236512.js b/dom/tests/browser/browser_bug1236512.js new file mode 100644 index 0000000000..18c92e9609 --- /dev/null +++ b/dom/tests/browser/browser_bug1236512.js @@ -0,0 +1,116 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +const testPageURL = + "http://mochi.test:8888/browser/dom/tests/browser/dummy.html"; + +async function testContentVisibilityState(aIsHidden, aBrowser) { + await SpecialPowers.spawn( + aBrowser.selectedBrowser, + [aIsHidden], + aExpectedResult => { + is(content.document.hidden, aExpectedResult, "document.hidden"); + is( + content.document.visibilityState, + aExpectedResult ? "hidden" : "visible", + "document.visibilityState" + ); + } + ); +} + +async function waitContentVisibilityChange(aIsHidden, aBrowser) { + await SpecialPowers.spawn( + aBrowser.selectedBrowser, + [aIsHidden], + async function (aExpectedResult) { + let visibilityState = aExpectedResult ? "hidden" : "visible"; + if ( + content.document.hidden === aExpectedResult && + content.document.visibilityState === visibilityState + ) { + ok(true, "already changed to expected visibility state"); + return; + } + + info("wait visibilitychange event"); + await ContentTaskUtils.waitForEvent( + content.document, + "visibilitychange", + true /* capture */, + aEvent => { + info( + `visibilitychange: ${content.document.hidden} ${content.document.visibilityState}` + ); + return ( + content.document.hidden === aExpectedResult && + content.document.visibilityState === visibilityState + ); + } + ); + } + ); +} + +/** + * This test is to test the visibility state will change to "hidden" when browser + * window is fully covered by another non-translucent application. Note that we + * only support this on Mac for now, other platforms don't support reporting + * occlusion state. + */ +add_task(async function () { + info("creating test window"); + let winTest = await BrowserTestUtils.openNewBrowserWindow(); + // Specify the width, height, left and top, so that the new window can be + // fully covered by "window". + let resizePromise = BrowserTestUtils.waitForEvent( + winTest, + "resize", + false, + e => { + return winTest.innerHeight <= 500 && winTest.innerWidth <= 500; + } + ); + winTest.moveTo(200, 200); + winTest.resizeTo(500, 500); + await resizePromise; + + let browserTest = winTest.gBrowser; + + info(`loading test page: ${testPageURL}`); + BrowserTestUtils.loadURIString(browserTest.selectedBrowser, testPageURL); + await BrowserTestUtils.browserLoaded(browserTest.selectedBrowser); + + info("test init visibility state"); + await testContentVisibilityState(false /* isHidden */, browserTest); + + info( + "test window should report 'hidden' if it is fully covered by another " + + "window" + ); + await new Promise(resolve => waitForFocus(resolve, window)); + await waitContentVisibilityChange(true /* isHidden */, browserTest); + + info( + "test window should still report 'hidden' since it is still fully covered " + + "by another window" + ); + let tab = BrowserTestUtils.addTab(browserTest); + await BrowserTestUtils.switchTab(browserTest, tab); + BrowserTestUtils.removeTab(browserTest.selectedTab); + await testContentVisibilityState(true /* isHidden */, browserTest); + + info( + "test window should report 'visible' if it is not fully covered by " + + "another window" + ); + await new Promise(resolve => waitForFocus(resolve, winTest)); + await waitContentVisibilityChange(false /* isHidden */, browserTest); + + info("closing test window"); + await BrowserTestUtils.closeWindow(winTest); +}); diff --git a/dom/tests/browser/browser_bug1238427.js b/dom/tests/browser/browser_bug1238427.js new file mode 100644 index 0000000000..60cb383e03 --- /dev/null +++ b/dom/tests/browser/browser_bug1238427.js @@ -0,0 +1,40 @@ +/* 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 TEST_URI = + // eslint-disable-next-line no-useless-concat + "http://example.com/" + "browser/dom/tests/browser/geo_leak_test.html"; + +const BASE_GEO_URL = + "http://mochi.test:8888/tests/dom/tests/mochitest/geolocation/network_geolocation.sjs"; + +add_task(async function () { + Services.prefs.setBoolPref("geo.prompt.testing", true); + Services.prefs.setBoolPref("geo.prompt.testing.allow", true); + + // Make the geolocation provider responder very slowly to ensure that + // it does not reply before we close the tab. + Services.prefs.setCharPref( + "geo.provider.network.url", + BASE_GEO_URL + "?delay=100000" + ); + + // Open the test URI and close it. The test harness will make sure that the + // page is cleaned up after some GCs. If geolocation is not shut down properly, + // it will show up as a non-shutdown leak. + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_URI, + }, + function (browser) { + /* ... */ + } + ); + + ok(true, "Need to do something in this test"); +}); diff --git a/dom/tests/browser/browser_bug1316330.js b/dom/tests/browser/browser_bug1316330.js new file mode 100644 index 0000000000..c81d489cf7 --- /dev/null +++ b/dom/tests/browser/browser_bug1316330.js @@ -0,0 +1,52 @@ +const URL = + "data:text/html,<script>" + + "window.focus();" + + "var down = 0; var press = 0;" + + "onkeydown = function(e) {" + + " var startTime = Date.now();" + + " document.body.setAttribute('data-down', ++down);" + + " if (e.keyCode == KeyboardEvent.DOM_VK_D) while (Date.now() - startTime < 500) {}" + + "};" + + "onkeypress = function(e) {" + + " var startTime = Date.now();" + + " document.body.setAttribute('data-press', ++press);" + + " if (e.charCode == 'p'.charCodeAt(0)) while (Date.now() - startTime < 500) {}" + + "};" + + "</script>"; + +add_task(async function () { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, URL); + let browser = tab.linkedBrowser; + + await EventUtils.synthesizeAndWaitKey("d", { repeat: 3 }); + + await SpecialPowers.spawn(browser, [], async function () { + is( + content.document.body.getAttribute("data-down"), + "2", + "Correct number of events" + ); + is( + content.document.body.getAttribute("data-press"), + "2", + "Correct number of events" + ); + }); + + await EventUtils.synthesizeAndWaitKey("p", { repeat: 3 }); + + await SpecialPowers.spawn(browser, [], async function () { + is( + content.document.body.getAttribute("data-down"), + "4", + "Correct number of events" + ); + is( + content.document.body.getAttribute("data-press"), + "4", + "Correct number of events" + ); + }); + + gBrowser.removeCurrentTab(); +}); diff --git a/dom/tests/browser/browser_bug1563629.js b/dom/tests/browser/browser_bug1563629.js new file mode 100644 index 0000000000..2ccbaced1c --- /dev/null +++ b/dom/tests/browser/browser_bug1563629.js @@ -0,0 +1,79 @@ +"use strict"; + +const DIRPATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + "" +); +const PATH = DIRPATH + "file_postMessage_parent.html"; + +const URL1 = `https://example.com/${PATH}`; +const URL2 = `https://example.org/${PATH}`; + +function listenForCrash(win) { + function listener(event) { + ok(false, "a crash occurred"); + } + + win.addEventListener("oop-browser-crashed", listener); + registerCleanupFunction(() => { + win.removeEventListener("oop-browser-crashed", listener); + }); +} + +add_task(async function () { + let win = await BrowserTestUtils.openNewBrowserWindow({ + fission: true, + private: true, + remote: true, + }); + + listenForCrash(win); + + try { + let tab = win.gBrowser.selectedTab; + let browser = tab.linkedBrowser; + + BrowserTestUtils.loadURIString(browser, URL1); + await BrowserTestUtils.browserLoaded(browser, false, URL1); + + async function loadURL(url) { + let iframe = content.document.createElement("iframe"); + content.document.body.appendChild(iframe); + + iframe.contentWindow.location = url; + await new Promise(resolve => + iframe.addEventListener("load", resolve, { once: true }) + ); + + return iframe.browsingContext; + } + + function length() { + return content.length; + } + + let outer = await SpecialPowers.spawn(browser, [URL2], loadURL); + let inner = await SpecialPowers.spawn(outer, [URL2], loadURL); + + is(await SpecialPowers.spawn(outer, [], length), 1, "have 1 inner frame"); + is(await SpecialPowers.spawn(browser, [], length), 1, "have 1 outer frame"); + + // Send a message from the outer iframe to the inner one. + // + // This would've previously crashed the content process that URL2 is running + // in. + await SpecialPowers.spawn(outer, [], () => { + content.frames[0].postMessage("foo", "*"); + }); + + // Now send a message from the inner frame to the outer one. + await SpecialPowers.spawn(inner, [], () => { + content.parent.postMessage("bar", "*"); + }); + + // Assert we've made it this far. + ok(true, "didn't crash"); + } finally { + await BrowserTestUtils.closeWindow(win); + } +}); diff --git a/dom/tests/browser/browser_bug1685807.js b/dom/tests/browser/browser_bug1685807.js new file mode 100644 index 0000000000..0e0dae2b16 --- /dev/null +++ b/dom/tests/browser/browser_bug1685807.js @@ -0,0 +1,78 @@ +/** + * Bug 1685807 - Testing that the window.name won't be reset when loading an + * about:blank page to a window which had loaded non-about:blank + * page. And other case that window.name should be reset if + * the document.domain has changed. + */ + +"use strict"; + +const EMPTY_URI = + "https://test1.example.com/browser/dom/tests/browser/file_empty.html"; +const TEST_URI = + "https://test1.example.com/browser/dom/tests/browser/file_bug1685807.html"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.window.name.update.enabled", true]], + }); +}); + +add_task(async function doTests() { + for (let testDocDomain of [false, true]) { + // Open an empty tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, EMPTY_URI); + let browser = tab.linkedBrowser; + + // Create a promise in order to wait loading of the about:blank page. + let loadedPromise = BrowserTestUtils.browserLoaded( + browser, + false, + "about:blank" + ); + + // Set the window.name and document.domain. + SpecialPowers.spawn( + browser, + [TEST_URI, testDocDomain], + (aTestURI, aTestDocDomain) => { + content.name = "Test"; + + if (aTestDocDomain) { + content.document.domain = "example.com"; + } + + // Open the page which will trigger the loading of the about:blank page. + content.open(aTestURI); + } + ); + + // Wait until the about:blank page is loaded. + await loadedPromise; + + // Check the window.name. + await SpecialPowers.spawn(browser, [testDocDomain], aTestDocDomain => { + if (aTestDocDomain) { + // The window.name should be reset if the document.domain was set to a + // cross-origin. + is(content.name, "", "The window.name should be reset."); + } else { + is(content.name, "Test", "The window.name shouldn't be reset."); + } + }); + + let awaitPageShow = BrowserTestUtils.waitForContentEvent( + browser, + "pageshow" + ); + browser.goBack(); + await awaitPageShow; + + // Check the window.name. + await SpecialPowers.spawn(browser, [], () => { + is(content.name, "Test", "The window.name is correct."); + }); + + BrowserTestUtils.removeTab(tab); + } +}); diff --git a/dom/tests/browser/browser_bug1709346.js b/dom/tests/browser/browser_bug1709346.js new file mode 100644 index 0000000000..bcfe456196 --- /dev/null +++ b/dom/tests/browser/browser_bug1709346.js @@ -0,0 +1,48 @@ +/* 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/. */ + +add_task(async function remove_subframe_in_cross_site_frame() { + await BrowserTestUtils.withNewTab( + "http://mochi.test:8888/browser/dom/tests/browser/file_empty_cross_site_frame.html", + async browser => { + await TestUtils.waitForCondition( + () => !XULBrowserWindow.isBusy, + "browser is not busy after the tab finishes loading" + ); + + // Spawn into the cross-site subframe, and begin loading a slow network + // connection. We'll cancel the load before this navigation completes. + await SpecialPowers.spawn( + browser.browsingContext.children[0], + [], + async () => { + let frame = content.document.createElement("iframe"); + frame.src = "load_forever.sjs"; + content.document.body.appendChild(frame); + + frame.addEventListener("load", function () { + ok(false, "load should not finish before the frame is removed"); + }); + } + ); + + is( + XULBrowserWindow.isBusy, + true, + "browser should be busy after the load starts" + ); + + // Remove the outer iframe, ending the load within this frame's subframe + // early. + await SpecialPowers.spawn(browser, [], async () => { + content.document.querySelector("iframe").remove(); + }); + + await TestUtils.waitForCondition( + () => !XULBrowserWindow.isBusy, + "Browser should no longer be busy after the frame is removed" + ); + } + ); +}); diff --git a/dom/tests/browser/browser_bug396843.js b/dom/tests/browser/browser_bug396843.js new file mode 100644 index 0000000000..ea25bd10b6 --- /dev/null +++ b/dom/tests/browser/browser_bug396843.js @@ -0,0 +1,337 @@ +/** Test for Bug 396843 **/ + +function testInDocument(doc, documentID) { + var allNodes = []; + var XMLNodes = []; + + // HTML + function HTML_TAG(name) { + allNodes.push(doc.createElementNS("http://www.w3.org/1999/xhtml", name)); + } + + /* List copy/pasted from nsHTMLTagList.h */ + HTML_TAG("a", "Anchor"); + HTML_TAG("abbr", "Span"); + HTML_TAG("acronym", "Span"); + HTML_TAG("address", "Span"); + HTML_TAG("applet", "Unknown"); + HTML_TAG("area", "Area"); + HTML_TAG("b", "Span"); + HTML_TAG("base", "Shared"); + HTML_TAG("basefont", "Span"); + HTML_TAG("bdi", ""); + HTML_TAG("bdo", "Span"); + HTML_TAG("bgsound", "Span"); + HTML_TAG("big", "Span"); + HTML_TAG("blockquote", "Shared"); + HTML_TAG("body", "Body"); + HTML_TAG("br", "BR"); + HTML_TAG("button", "Button"); + HTML_TAG("canvas", "Canvas"); + HTML_TAG("caption", "TableCaption"); + HTML_TAG("center", "Span"); + HTML_TAG("cite", "Span"); + HTML_TAG("code", "Span"); + HTML_TAG("col", "TableCol"); + HTML_TAG("colgroup", "TableCol"); + HTML_TAG("dd", "Span"); + HTML_TAG("del", "Mod"); + HTML_TAG("dfn", "Span"); + HTML_TAG("dir", "Shared"); + HTML_TAG("div", "Div"); + HTML_TAG("dl", "SharedList"); + HTML_TAG("dt", "Span"); + HTML_TAG("em", "Span"); + HTML_TAG("embed", "Embed"); + HTML_TAG("fieldset", "FieldSet"); + HTML_TAG("font", "Font"); + HTML_TAG("form", "Form"); + HTML_TAG("frame", "Frame"); + HTML_TAG("frameset", "FrameSet"); + HTML_TAG("h1", "Heading"); + HTML_TAG("h2", "Heading"); + HTML_TAG("h3", "Heading"); + HTML_TAG("h4", "Heading"); + HTML_TAG("h5", "Heading"); + HTML_TAG("h6", "Heading"); + HTML_TAG("head", "Head"); + HTML_TAG("hr", "HR"); + HTML_TAG("html", "Html"); + HTML_TAG("i", "Span"); + HTML_TAG("iframe", "IFrame"); + HTML_TAG("image", ""); + HTML_TAG("img", "Image"); + HTML_TAG("input", "Input"); + HTML_TAG("ins", "Mod"); + HTML_TAG("isindex", "Unknown"); + HTML_TAG("kbd", "Span"); + HTML_TAG("keygen", "Span"); + HTML_TAG("label", "Label"); + HTML_TAG("legend", "Legend"); + HTML_TAG("li", "LI"); + HTML_TAG("link", "Link"); + HTML_TAG("listing", "Span"); + HTML_TAG("map", "Map"); + HTML_TAG("marquee", "Div"); + HTML_TAG("menu", "Shared"); + HTML_TAG("meta", "Meta"); + HTML_TAG("multicol", "Unknown"); + HTML_TAG("nobr", "Span"); + HTML_TAG("noembed", "Div"); + HTML_TAG("noframes", "Div"); + HTML_TAG("noscript", "Div"); + HTML_TAG("object", "Object"); + HTML_TAG("ol", "SharedList"); + HTML_TAG("optgroup", "OptGroup"); + HTML_TAG("option", "Option"); + HTML_TAG("p", "Paragraph"); + HTML_TAG("param", "Shared"); + HTML_TAG("plaintext", "Span"); + HTML_TAG("pre", "Pre"); + HTML_TAG("q", "Shared"); + HTML_TAG("s", "Span"); + HTML_TAG("samp", "Span"); + HTML_TAG("script", "Script"); + HTML_TAG("select", "Select"); + HTML_TAG("small", "Span"); + HTML_TAG("spacer", "Unknown"); + HTML_TAG("span", "Span"); + HTML_TAG("strike", "Span"); + HTML_TAG("strong", "Span"); + HTML_TAG("style", "Style"); + HTML_TAG("sub", "Span"); + HTML_TAG("sup", "Span"); + HTML_TAG("table", "Table"); + HTML_TAG("tbody", "TableSection"); + HTML_TAG("td", "TableCell"); + HTML_TAG("textarea", "TextArea"); + HTML_TAG("tfoot", "TableSection"); + HTML_TAG("th", "TableCell"); + HTML_TAG("thead", "TableSection"); + HTML_TAG("template", "Template"); + HTML_TAG("title", "Title"); + HTML_TAG("tr", "TableRow"); + HTML_TAG("tt", "Span"); + HTML_TAG("u", "Span"); + HTML_TAG("ul", "SharedList"); + HTML_TAG("var", "Span"); + HTML_TAG("wbr", "Shared"); + HTML_TAG("xmp", "Span"); + + function SVG_TAG(name) { + allNodes.push(doc.createElementNS("http://www.w3.org/2000/svg", name)); + } + + // List sorta stolen from SVG element factory. + SVG_TAG("a"); + SVG_TAG("polyline"); + SVG_TAG("polygon"); + SVG_TAG("circle"); + SVG_TAG("ellipse"); + SVG_TAG("line"); + SVG_TAG("rect"); + SVG_TAG("svg"); + SVG_TAG("g"); + SVG_TAG("foreignObject"); + SVG_TAG("path"); + SVG_TAG("text"); + SVG_TAG("tspan"); + SVG_TAG("image"); + SVG_TAG("style"); + SVG_TAG("linearGradient"); + SVG_TAG("metadata"); + SVG_TAG("radialGradient"); + SVG_TAG("stop"); + SVG_TAG("defs"); + SVG_TAG("desc"); + SVG_TAG("script"); + SVG_TAG("use"); + SVG_TAG("symbol"); + SVG_TAG("marker"); + SVG_TAG("title"); + SVG_TAG("clipPath"); + SVG_TAG("textPath"); + SVG_TAG("filter"); + SVG_TAG("feBlend"); + SVG_TAG("feColorMatrix"); + SVG_TAG("feComponentTransfer"); + SVG_TAG("feComposite"); + SVG_TAG("feFuncR"); + SVG_TAG("feFuncG"); + SVG_TAG("feFuncB"); + SVG_TAG("feFuncA"); + SVG_TAG("feGaussianBlur"); + SVG_TAG("feMerge"); + SVG_TAG("feMergeNode"); + SVG_TAG("feMorphology"); + SVG_TAG("feOffset"); + SVG_TAG("feFlood"); + SVG_TAG("feTile"); + SVG_TAG("feTurbulence"); + SVG_TAG("feConvolveMatrix"); + SVG_TAG("feDistantLight"); + SVG_TAG("fePointLight"); + SVG_TAG("feSpotLight"); + SVG_TAG("feDiffuseLighting"); + SVG_TAG("feSpecularLighting"); + SVG_TAG("feDisplacementMap"); + SVG_TAG("feImage"); + SVG_TAG("pattern"); + SVG_TAG("mask"); + SVG_TAG("svgSwitch"); + + // Toss in some other namespaced stuff too, for good measure + // XUL stuff might not be creatable in content documents + try { + allNodes.push( + doc.createElementNS( + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", + "window" + ) + ); + } catch (e) {} + allNodes.push( + doc.createElementNS("http://www.w3.org/1998/Math/MathML", "math") + ); + allNodes.push( + doc.createElementNS("http://www.w3.org/2001/xml-events", "testname") + ); + allNodes.push(doc.createElementNS("bogus.namespace", "testname")); + + var XMLDoc = doc.implementation.createDocument("", "", null); + + // And non-elements + allNodes.push(doc.createTextNode("some text")); + allNodes.push(doc.createComment("some text")); + allNodes.push(doc.createDocumentFragment()); + XMLNodes.push(XMLDoc.createCDATASection("some text")); + XMLNodes.push(XMLDoc.createProcessingInstruction("PI", "data")); + + function runTestUnwrapped() { + if (!("wrappedJSObject" in doc)) { + return; + } + ok( + doc.wrappedJSObject.nodePrincipal === undefined, + "Must not have document principal for " + documentID + ); + ok( + doc.wrappedJSObject.baseURIObject === undefined, + "Must not have document base URI for " + documentID + ); + ok( + doc.wrappedJSObject.documentURIObject === undefined, + "Must not have document URI for " + documentID + ); + + for (var i = 0; i < allNodes.length; ++i) { + ok( + allNodes[i].wrappedJSObject.nodePrincipal === undefined, + "Unexpected principal appears for " + + allNodes[i].nodeName + + " in " + + documentID + ); + ok( + allNodes[i].wrappedJSObject.baseURIObject === undefined, + "Unexpected base URI appears for " + + allNodes[i].nodeName + + " in " + + documentID + ); + } + } + + function runTestProps() { + isnot( + doc.nodePrincipal, + null, + "Must have document principal in " + documentID + ); + is( + doc.nodePrincipal instanceof Ci.nsIPrincipal, + true, + "document principal must be a principal in " + documentID + ); + isnot( + doc.baseURIObject, + null, + "Must have document base URI in" + documentID + ); + is( + doc.baseURIObject instanceof Ci.nsIURI, + true, + "document base URI must be a URI in " + documentID + ); + isnot(doc.documentURIObject, null, "Must have document URI " + documentID); + is( + doc.documentURIObject instanceof Ci.nsIURI, + true, + "document URI must be a URI in " + documentID + ); + is( + doc.documentURIObject.spec, + doc.documentURI, + "document URI must be the right URI in " + documentID + ); + + for (var i = 0; i < allNodes.length; ++i) { + is( + allNodes[i].nodePrincipal, + doc.nodePrincipal, + "Unexpected principal for " + allNodes[i].nodeName + " in " + documentID + ); + is( + allNodes[i].baseURIObject, + doc.baseURIObject, + "Unexpected base URI for " + allNodes[i].nodeName + " in " + documentID + ); + } + + for (i = 0; i < XMLNodes.length; ++i) { + is( + XMLNodes[i].nodePrincipal, + doc.nodePrincipal, + "Unexpected principal for " + XMLNodes[i].nodeName + " in " + documentID + ); + is( + XMLNodes[i].baseURIObject.spec, + "about:blank", + "Unexpected base URI for " + XMLNodes[i].nodeName + " in " + documentID + ); + } + } + + runTestUnwrapped(); + runTestProps(); + runTestUnwrapped(); +} + +add_task(async function test1() { + testInDocument(document, "browser window"); +}); + +async function newTabTest(location) { + await BrowserTestUtils.withNewTab( + { gBrowser, url: location }, + async function (browser) { + await SpecialPowers.spawn( + browser, + [{ location, testInDocument_: testInDocument.toSource() }], + async function ({ location, testInDocument_ }) { + // eslint-disable-next-line no-eval + let testInDocument = eval(`(() => (${testInDocument_}))()`); + testInDocument(content.document, location); + } + ); + } + ); +} + +add_task(async function test2() { + await newTabTest("about:blank"); +}); + +add_task(async function test3() { + await newTabTest("about:config"); +}); diff --git a/dom/tests/browser/browser_bytecode_cache_asm_js.js b/dom/tests/browser/browser_bytecode_cache_asm_js.js new file mode 100644 index 0000000000..74dcdf3b72 --- /dev/null +++ b/dom/tests/browser/browser_bytecode_cache_asm_js.js @@ -0,0 +1,31 @@ +"use strict"; + +const PAGE_URL = + "http://example.com/browser/dom/tests/browser/page_bytecode_cache_asm_js.html"; + +add_task(async function () { + // Eagerly generate bytecode cache. + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.script_loader.bytecode_cache.enabled", true], + ["dom.script_loader.bytecode_cache.strategy", -1], + ], + }); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: PAGE_URL, + waitForLoad: true, + }, + async browser => { + let result = await SpecialPowers.spawn(browser, [], () => { + return content.document.getElementById("result").textContent; + }); + // No error shoud be caught by content. + is(result, "ok"); + } + ); + + await SpecialPowers.popPrefEnv(); +}); diff --git a/dom/tests/browser/browser_cancel_keydown_keypress_event.js b/dom/tests/browser/browser_cancel_keydown_keypress_event.js new file mode 100644 index 0000000000..de08aa17e9 --- /dev/null +++ b/dom/tests/browser/browser_cancel_keydown_keypress_event.js @@ -0,0 +1,41 @@ +const URL = + "https://example.com/browser/dom/tests/browser/prevent_return_key.html"; + +const { PromptTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromptTestUtils.sys.mjs" +); + +// Wait for alert dialog and dismiss it immediately. +function awaitAndCloseAlertDialog(browser) { + return PromptTestUtils.handleNextPrompt( + browser, + { modalType: Services.prompt.MODAL_TYPE_CONTENT, promptType: "alert" }, + { buttonNumClick: 0 } + ); +} + +add_task(async function () { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, URL); + let browser = tab.linkedBrowser; + + // Focus and enter random text on input. + await SpecialPowers.spawn(browser, [], async function () { + let input = content.document.getElementById("input"); + input.focus(); + input.value = "abcd"; + }); + + // Send return key (cross process) to submit the form implicitly. + let dialogShown = awaitAndCloseAlertDialog(browser); + EventUtils.synthesizeKey("KEY_Enter"); + await dialogShown; + + // Check that the form should not have been submitted. + await SpecialPowers.spawn(browser, [], async function () { + let result = content.document.getElementById("result").innerHTML; + info("submit result: " + result); + is(result, "not submitted", "form should not have submitted"); + }); + + gBrowser.removeCurrentTab(); +}); diff --git a/dom/tests/browser/browser_data_document_crossOriginIsolated.js b/dom/tests/browser/browser_data_document_crossOriginIsolated.js new file mode 100644 index 0000000000..d38eda3f0a --- /dev/null +++ b/dom/tests/browser/browser_data_document_crossOriginIsolated.js @@ -0,0 +1,68 @@ +"use strict"; + +const DIRPATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + "" +); +const PATH = DIRPATH + "file_coop_coep.html"; + +const ORIGIN = "https://test1.example.com"; +const URL = `${ORIGIN}/${PATH}`; + +add_task(async function () { + await BrowserTestUtils.withNewTab(URL, async function (browser) { + BrowserTestUtils.loadURIString(browser, URL); + await BrowserTestUtils.browserLoaded(browser); + + await SpecialPowers.spawn(browser, [ORIGIN], async origin => { + is( + content.window.origin, + origin, + `Opened a tab and navigated to ${origin}` + ); + + ok( + content.window.crossOriginIsolated, + `Should have been cross-origin-isolated env` + ); + + let hostIds = []; + function createShadowDOMAndTriggerSlotChange(host) { + var shadow = host.attachShadow({ mode: "closed" }); + + let promise = new Promise(resolve => { + shadow.addEventListener("slotchange", function () { + hostIds.push(host.id); + resolve(); + }); + }); + + shadow.innerHTML = "<slot></slot>"; + + host.appendChild(host.ownerDocument.createElement("span")); + + return promise; + } + + let host1 = content.document.getElementById("host1"); + + let dataDoc = content.document.implementation.createHTMLDocument(); + dataDoc.body.innerHTML = "<div id='host2'></div>"; + let host2 = dataDoc.body.firstChild; + + let host3 = content.document.getElementById("host3"); + + let promises = []; + promises.push(createShadowDOMAndTriggerSlotChange(host1)); + promises.push(createShadowDOMAndTriggerSlotChange(host2)); + promises.push(createShadowDOMAndTriggerSlotChange(host3)); + + await Promise.all(promises); + + is(hostIds.length, 3, `Got 3 slot change events`); + is(hostIds[0], "host1", `The first one was host1`); + is(hostIds[1], "host2", `The second one was host2`); + is(hostIds[2], "host3", `The third one was host3`); + }); + }); +}); diff --git a/dom/tests/browser/browser_focus_steal_from_chrome.js b/dom/tests/browser/browser_focus_steal_from_chrome.js new file mode 100644 index 0000000000..ead2fa0ba1 --- /dev/null +++ b/dom/tests/browser/browser_focus_steal_from_chrome.js @@ -0,0 +1,214 @@ +add_task(async function () { + requestLongerTimeout(2); + + let testingList = [ + { + uri: "data:text/html,<body onload=\"setTimeout(function () { document.getElementById('target').focus(); }, 10);\"><input id='target'></body>", + tagName: "INPUT", + methodName: "focus", + }, + { + uri: "data:text/html,<body onload=\"setTimeout(function () { document.getElementById('target').select(); }, 10);\"><input id='target'></body>", + tagName: "INPUT", + methodName: "select", + }, + { + uri: "data:text/html,<body onload=\"setTimeout(function () { document.getElementById('target').focus(); }, 10);\"><a href='about:blank' id='target'>anchor</a></body>", + tagName: "A", + methodName: "focus", + }, + { + uri: "data:text/html,<body onload=\"setTimeout(function () { document.getElementById('target').focus(); }, 10);\"><button id='target'>button</button></body>", + tagName: "BUTTON", + methodName: "focus", + }, + { + uri: "data:text/html,<body onload=\"setTimeout(function () { document.getElementById('target').focus(); }, 10);\"><select id='target'><option>item1</option></select></body>", + tagName: "SELECT", + methodName: "focus", + }, + { + uri: "data:text/html,<body onload=\"setTimeout(function () { document.getElementById('target').focus(); }, 10);\"><textarea id='target'>textarea</textarea></body>", + tagName: "TEXTAREA", + methodName: "focus", + }, + { + uri: "data:text/html,<body onload=\"setTimeout(function () { document.getElementById('target').select(); }, 10);\"><textarea id='target'>textarea</textarea></body>", + tagName: "TEXTAREA", + methodName: "select", + }, + { + uri: "data:text/html,<body onload=\"setTimeout(function () { document.getElementById('target').focus(); }, 10);\"><label id='target'><input></label></body>", + tagName: "INPUT", + methodName: "focus of label element", + }, + { + uri: "data:text/html,<body onload=\"setTimeout(function () { document.getElementById('target').focus(); }, 10);\"><fieldset><legend id='target'>legend</legend><input></fieldset></body>", + tagName: "INPUT", + methodName: "focus of legend element", + }, + { + uri: + 'data:text/html,<body onload="setTimeout(function () {' + + " var element = document.getElementById('target');" + + " var event = document.createEvent('MouseEvent');" + + " event.initMouseEvent('click', true, true, window," + + " 1, 0, 0, 0, 0, false, false, false, false, 0, element);" + + ' element.dispatchEvent(event); }, 10);">' + + "<label id='target'><input></label></body>", + tagName: "INPUT", + methodName: "click event on the label element", + }, + ]; + + await BrowserTestUtils.withNewTab("about:blank", async function (bg) { + await BrowserTestUtils.withNewTab("about:blank", async function (fg) { + for (let test of testingList) { + // Focus the foreground tab's content + fg.focus(); + + // Load the URIs. + BrowserTestUtils.loadURIString(bg, test.uri); + await BrowserTestUtils.browserLoaded(bg); + BrowserTestUtils.loadURIString(fg, test.uri); + await BrowserTestUtils.browserLoaded(fg); + + ok(true, "Test1: Both of the tabs are loaded"); + + // Confirm that the contents should be able to steal focus from content. + await SpecialPowers.spawn(fg, [test], test => { + return new Promise(res => { + function f() { + let e = content.document.activeElement; + if (e.tagName != test.tagName) { + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + content.setTimeout(f, 10); + } else { + is( + Services.focus.focusedElement, + e, + "the foreground tab's " + + test.tagName + + " element isn't focused by the " + + test.methodName + + " (Test1: content can steal focus)" + ); + res(); + } + } + f(); + }); + }); + + await SpecialPowers.spawn(bg, [test], test => { + return new Promise(res => { + function f() { + let e = content.document.activeElement; + if (e.tagName != test.tagName) { + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + content.setTimeout(f, 10); + } else { + isnot( + Services.focus.focusedElement, + e, + "the background tab's " + + test.tagName + + " element is focused by the " + + test.methodName + + " (Test1: content can steal focus)" + ); + res(); + } + } + f(); + }); + }); + + if (fg.isRemoteBrowser) { + is( + Services.focus.focusedElement, + fg, + "Focus should be on the content in the parent process" + ); + } + + // Focus chrome + gURLBar.focus(); + let originalFocus = Services.focus.focusedElement; + + // Load about:blank just to make sure that everything works nicely + BrowserTestUtils.loadURIString(bg, "about:blank"); + await BrowserTestUtils.browserLoaded(bg); + BrowserTestUtils.loadURIString(fg, "about:blank"); + await BrowserTestUtils.browserLoaded(fg); + + // Load the URIs. + BrowserTestUtils.loadURIString(bg, test.uri); + await BrowserTestUtils.browserLoaded(bg); + BrowserTestUtils.loadURIString(fg, test.uri); + await BrowserTestUtils.browserLoaded(fg); + + ok(true, "Test2: Both of the tabs are loaded"); + + // Confirm that the contents should be able to steal focus from content. + await SpecialPowers.spawn(fg, [test], test => { + return new Promise(res => { + function f() { + let e = content.document.activeElement; + if (e.tagName != test.tagName) { + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + content.setTimeout(f, 10); + } else { + isnot( + Services.focus.focusedElement, + e, + "the foreground tab's " + + test.tagName + + " element is focused by the " + + test.methodName + + " (Test2: content can NOT steal focus)" + ); + res(); + } + } + f(); + }); + }); + + await SpecialPowers.spawn(bg, [test], test => { + return new Promise(res => { + function f() { + let e = content.document.activeElement; + if (e.tagName != test.tagName) { + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + content.setTimeout(f, 10); + } else { + isnot( + Services.focus.focusedElement, + e, + "the background tab's " + + test.tagName + + " element is focused by the " + + test.methodName + + " (Test2: content can NOT steal focus)" + ); + res(); + } + } + f(); + }); + }); + + is( + Services.focus.focusedElement, + originalFocus, + "The parent process's focus has shifted " + + "(methodName = " + + test.methodName + + ")" + + " (Test2: content can NOT steal focus)" + ); + } + }); + }); +}); diff --git a/dom/tests/browser/browser_focus_steal_from_chrome_during_mousedown.js b/dom/tests/browser/browser_focus_steal_from_chrome_during_mousedown.js new file mode 100644 index 0000000000..61c36f2436 --- /dev/null +++ b/dom/tests/browser/browser_focus_steal_from_chrome_during_mousedown.js @@ -0,0 +1,76 @@ +add_task(async function test() { + const kTestURI = + "data:text/html," + + '<script type="text/javascript">' + + " function onMouseDown(aEvent) {" + + " document.getElementById('willBeFocused').focus();" + + " aEvent.preventDefault();" + + " }" + + "</script>" + + '<body id="body">' + + '<button onmousedown="onMouseDown(event);" style="width: 100px; height: 100px;">click here</button>' + + '<input id="willBeFocused"></body>'; + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, kTestURI); + + let fm = Services.focus; + + for (var button = 0; button < 3; button++) { + // Set focus to a chrome element before synthesizing a mouse down event. + gURLBar.focus(); + + is( + fm.focusedElement, + gURLBar.inputField, + "Failed to move focus to search bar: button=" + button + ); + + // Synthesize mouse down event on browser object over the button, such that + // the event propagates through both processes. + EventUtils.synthesizeMouse(tab.linkedBrowser, 20, 20, { button }); + + isnot( + fm.focusedElement, + gURLBar.inputField, + "Failed to move focus away from search bar: button=" + button + ); + + await SpecialPowers.spawn( + tab.linkedBrowser, + [button], + async function (button) { + let fm = Services.focus; + + let attempts = 10; + await new Promise(resolve => { + function check() { + if ( + attempts > 0 && + content.document.activeElement.id != "willBeFocused" + ) { + attempts--; + content.window.setTimeout(check, 100); + return; + } + + Assert.equal( + content.document.activeElement.id, + "willBeFocused", + "The input element isn't active element: button=" + button + ); + Assert.equal( + fm.focusedElement, + content.document.activeElement, + "The active element isn't focused element in App level: button=" + + button + ); + resolve(); + } + check(); + }); + } + ); + } + + gBrowser.removeTab(tab); +}); diff --git a/dom/tests/browser/browser_form_associated_custom_elements_validity.js b/dom/tests/browser/browser_form_associated_custom_elements_validity.js new file mode 100644 index 0000000000..3765405735 --- /dev/null +++ b/dom/tests/browser/browser_form_associated_custom_elements_validity.js @@ -0,0 +1,111 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +add_task(async function report_validity() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: `data:text/html,<my-control></my-control>`, + }, + async function (aBrowser) { + let promisePopupShown = BrowserTestUtils.waitForEvent( + window, + "popupshown" + ); + + let message = "valueMissing message"; + await SpecialPowers.spawn(aBrowser, [message], function (aMessage) { + class MyControl extends content.HTMLElement { + static get formAssociated() { + return true; + } + constructor() { + super(); + let shadow = this.attachShadow({ mode: "open" }); + let input = content.document.createElement("input"); + shadow.appendChild(input); + + let internals = this.attachInternals(); + internals.setValidity({ valueMissing: true }, aMessage, input); + internals.reportValidity(); + } + } + content.customElements.define("my-control", MyControl); + + let myControl = content.document.querySelector("my-control"); + content.customElements.upgrade(myControl); + }); + await promisePopupShown; + + let invalidFormPopup = + window.document.getElementById("invalid-form-popup"); + is(invalidFormPopup.state, "open", "invalid-form-popup should be opened"); + is(invalidFormPopup.firstChild.textContent, message, "check message"); + + let promisePopupHidden = BrowserTestUtils.waitForEvent( + invalidFormPopup, + "popuphidden" + ); + invalidFormPopup.hidePopup(); + await promisePopupHidden; + } + ); +}); + +add_task(async function form_report_validity() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: `data:text/html,<form><my-control></my-control></form>`, + }, + async function (aBrowser) { + let promisePopupShown = BrowserTestUtils.waitForEvent( + window, + "popupshown" + ); + + let message = "valueMissing message"; + await SpecialPowers.spawn(aBrowser, [message], function (aMessage) { + class MyControl extends content.HTMLElement { + static get formAssociated() { + return true; + } + constructor() { + super(); + let shadow = this.attachShadow({ mode: "open" }); + let input = content.document.createElement("input"); + shadow.appendChild(input); + + let internals = this.attachInternals(); + internals.setValidity({ valueMissing: true }, aMessage, input); + } + } + content.customElements.define("my-control", MyControl); + + let myControl = content.document.querySelector("my-control"); + content.customElements.upgrade(myControl); + + let form = content.document.querySelector("form"); + is(form.length, "1", "check form.length"); + form.reportValidity(); + }); + await promisePopupShown; + + let invalidFormPopup = + window.document.getElementById("invalid-form-popup"); + is(invalidFormPopup.state, "open", "invalid-form-popup should be opened"); + is(invalidFormPopup.firstChild.textContent, message, "check message"); + + let promisePopupHidden = BrowserTestUtils.waitForEvent( + invalidFormPopup, + "popuphidden" + ); + invalidFormPopup.hidePopup(); + await promisePopupHidden; + } + ); +}); diff --git a/dom/tests/browser/browser_frame_elements.html b/dom/tests/browser/browser_frame_elements.html new file mode 100644 index 0000000000..4843173f57 --- /dev/null +++ b/dom/tests/browser/browser_frame_elements.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<head> + <meta charset="utf-8"> + <title>Frame Element Tests</title> +</head> +<body> + <h1>Frame Element Tests</h1> + + <iframe id="iframe-blank" src="about:blank"></iframe> + + <iframe id="iframe-data-url" src="data:text/html;charset=utf-8,%3Chtml%3E%3Cbody%3Eiframe%3C/body%3E%3C/html%3E"></iframe> + + <object id="object-data-url" type="text/html" data="data:text/html;charset=utf-8,%3Chtml%3E%3Cbody%3Eobject%3C/body%3E%3C/html%3E"></object> + +</body> diff --git a/dom/tests/browser/browser_frame_elements.js b/dom/tests/browser/browser_frame_elements.js new file mode 100644 index 0000000000..13f79edbfd --- /dev/null +++ b/dom/tests/browser/browser_frame_elements.js @@ -0,0 +1,74 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* 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 TEST_URI = + "http://example.com/browser/dom/tests/browser/browser_frame_elements.html"; + +add_task(async function test() { + await BrowserTestUtils.withNewTab( + { gBrowser, url: TEST_URI }, + async function (browser) { + // Confirm its embedder is the browser: + is( + browser.browsingContext.embedderElement, + browser, + "Embedder element for main window is xul:browser" + ); + + await SpecialPowers.spawn(browser, [], startTests); + } + ); +}); + +function startTests() { + info("Frame tests started"); + + info("Checking top window"); + let gWindow = content; + Assert.equal(gWindow.top, gWindow, "gWindow is top"); + Assert.equal(gWindow.parent, gWindow, "gWindow is parent"); + + info("Checking about:blank iframe"); + let iframeBlank = gWindow.document.querySelector("#iframe-blank"); + Assert.ok(iframeBlank, "Iframe exists on page"); + Assert.equal( + iframeBlank.browsingContext.embedderElement, + iframeBlank, + "Embedder element for iframe window is iframe" + ); + Assert.equal(iframeBlank.contentWindow.top, gWindow, "gWindow is top"); + Assert.equal(iframeBlank.contentWindow.parent, gWindow, "gWindow is parent"); + + info("Checking iframe with data url src"); + let iframeDataUrl = gWindow.document.querySelector("#iframe-data-url"); + Assert.ok(iframeDataUrl, "Iframe exists on page"); + Assert.equal( + iframeDataUrl.browsingContext.embedderElement, + iframeDataUrl, + "Embedder element for iframe window is iframe" + ); + Assert.equal(iframeDataUrl.contentWindow.top, gWindow, "gWindow is top"); + Assert.equal( + iframeDataUrl.contentWindow.parent, + gWindow, + "gWindow is parent" + ); + + info("Checking object with data url data attribute"); + let objectDataUrl = gWindow.document.querySelector("#object-data-url"); + Assert.ok(objectDataUrl, "Object exists on page"); + Assert.equal( + objectDataUrl.browsingContext.embedderElement, + objectDataUrl, + "Embedder element for object window is the object" + ); + Assert.equal(objectDataUrl.contentWindow.top, gWindow, "gWindow is top"); + Assert.equal( + objectDataUrl.contentWindow.parent, + gWindow, + "gWindow is parent" + ); +} diff --git a/dom/tests/browser/browser_hasActivePeerConnections.js b/dom/tests/browser/browser_hasActivePeerConnections.js new file mode 100644 index 0000000000..91d51efbfc --- /dev/null +++ b/dom/tests/browser/browser_hasActivePeerConnections.js @@ -0,0 +1,134 @@ +const TEST_URI1 = + "http://mochi.test:8888/browser/dom/tests/browser/" + + "create_webrtc_peer_connection.html"; + +const TEST_URI2 = + "https://example.com/browser/dom/tests/browser/" + + "create_webrtc_peer_connection.html"; + +add_task(async () => { + await BrowserTestUtils.withNewTab(TEST_URI1, async browser => { + const windowGlobal = browser.browsingContext.currentWindowGlobal; + Assert.ok(windowGlobal); + + Assert.strictEqual( + windowGlobal.hasActivePeerConnections(), + false, + "No active connections at the beginning" + ); + + await SpecialPowers.spawn(browser, [], async () => { + content.postMessage("push-peer-connection", "*"); + return new Promise(resolve => + content.addEventListener("message", function onMessage(event) { + if (event.data == "ack") { + content.removeEventListener(event.type, onMessage); + resolve(); + } + }) + ); + }); + + Assert.strictEqual( + windowGlobal.hasActivePeerConnections(), + true, + "One connection in the top window" + ); + + await SpecialPowers.spawn(browser, [], async () => { + content.postMessage("pop-peer-connection", "*"); + return new Promise(resolve => + content.addEventListener("message", function onMessage(event) { + if (event.data == "ack") { + content.removeEventListener(event.type, onMessage); + resolve(); + } + }) + ); + }); + + Assert.strictEqual( + windowGlobal.hasActivePeerConnections(), + false, + "All connections have been closed" + ); + + await SpecialPowers.spawn( + browser, + [TEST_URI1, TEST_URI2], + async (TEST_URI1, TEST_URI2) => { + // Create a promise that is fulfilled when the "ack" message is received + // |targetCount| times. + const createWaitForAckPromise = (eventTarget, targetCount) => { + let counter = 0; + return new Promise(resolve => { + eventTarget.addEventListener("message", function onMsg(event) { + if (event.data == "ack") { + ++counter; + if (counter == targetCount) { + eventTarget.removeEventListener(event.type, onMsg); + resolve(); + } + } + }); + }); + }; + + const addFrame = (id, url) => { + const iframe = content.document.createElement("iframe"); + iframe.id = id; + iframe.src = url; + content.document.body.appendChild(iframe); + return iframe; + }; + + // Create two iframes hosting a same-origin page and a cross-origin page + const iframe1 = addFrame("iframe-same-origin", TEST_URI1); + const iframe2 = addFrame("iframe-cross-origin", TEST_URI2); + await ContentTaskUtils.waitForEvent(iframe1, "load"); + await ContentTaskUtils.waitForEvent(iframe2, "load"); + + // Make sure the counter is not messed up after successive push/pop + // messages + const kLoopCount = 100; + for (let i = 0; i < kLoopCount; ++i) { + content.postMessage("push-peer-connection", "*"); + iframe1.contentWindow.postMessage("push-peer-connection", "*"); + iframe2.contentWindow.postMessage("push-peer-connection", "*"); + iframe1.contentWindow.postMessage("pop-peer-connection", "*"); + iframe2.contentWindow.postMessage("pop-peer-connection", "*"); + content.postMessage("pop-peer-connection", "*"); + } + iframe2.contentWindow.postMessage("push-peer-connection", "*"); + + return createWaitForAckPromise(content, kLoopCount * 6 + 1); + } + ); + + Assert.strictEqual( + windowGlobal.hasActivePeerConnections(), + true, + "#iframe-cross-origin still has an active connection" + ); + + await SpecialPowers.spawn(browser, [], async () => { + content.document + .getElementById("iframe-cross-origin") + .contentWindow.postMessage("pop-peer-connection", "*"); + return new Promise(resolve => + content.addEventListener("message", function onMessage(event) { + if (event.data == "ack") { + content.removeEventListener(event.type, onMessage); + resolve(); + } + }) + ); + }); + + Assert.strictEqual( + windowGlobal.hasActivePeerConnections(), + false, + "All connections have been closed" + ); + }); +}); diff --git a/dom/tests/browser/browser_hasbeforeunload.js b/dom/tests/browser/browser_hasbeforeunload.js new file mode 100644 index 0000000000..1ffc06656e --- /dev/null +++ b/dom/tests/browser/browser_hasbeforeunload.js @@ -0,0 +1,875 @@ +"use strict"; + +const PAGE_URL = + "http://example.com/browser/dom/tests/browser/beforeunload_test_page.html"; + +/** + * Adds 1 or more inert beforeunload event listeners in this browser. + * By default, will target the top-level content window, but callers + * can specify the index of a subframe to target. See prepareSubframes + * for an idea of how the subframes are structured. + * + * @param {<xul:browser>} browser + * The browser to add the beforeunload event listener in. + * @param {int} howMany + * How many beforeunload event listeners to add. Note that these + * beforeunload event listeners are inert and will not actually + * prevent the host window from navigating. + * @param {optional int} frameDepth + * The depth of the frame to add the event listener to. Defaults + * to 0, which is the top-level content window. + * @return {Promise} + */ +function addBeforeUnloadListeners(browser, howMany = 1, frameDepth = 0) { + return controlFrameAt(browser, frameDepth, { + name: "AddBeforeUnload", + howMany, + }); +} + +/** + * Adds 1 or more inert beforeunload event listeners in this browser on + * a particular subframe. By default, this will target the first subframe + * under the top-level content window, but callers can specify the index + * of a subframe to target. See prepareSubframes for an idea of how the + * subframes are structured. + * + * Note that this adds the beforeunload event listener on the "outer" window, + * by doing: + * + * iframe.addEventListener("beforeunload", ...); + * + * @param {<xul:browser>} browser + * The browser to add the beforeunload event listener in. + * @param {int} howMany + * How many beforeunload event listeners to add. Note that these + * beforeunload event listeners are inert and will not actually + * prevent the host window from navigating. + * @param {optional int} frameDepth + * The depth of the frame to add the event listener to. Defaults + * to 1, which is the first subframe inside the top-level content + * window. Setting this to 0 will throw. + * @return {Promise} + */ +function addOuterBeforeUnloadListeners(browser, howMany = 1, frameDepth = 1) { + if (frameDepth == 0) { + throw new Error( + "When adding a beforeunload listener on an outer " + + "window, the frame you're targeting needs to be at " + + "depth > 0." + ); + } + + return controlFrameAt(browser, frameDepth, { + name: "AddOuterBeforeUnload", + howMany, + }); +} + +/** + * Removes 1 or more inert beforeunload event listeners in this browser. + * This assumes that addBeforeUnloadListeners has been called previously + * for the target frame. + * + * By default, will target the top-level content window, but callers + * can specify the index of a subframe to target. See prepareSubframes + * for an idea of how the subframes are structured. + * + * @param {<xul:browser>} browser + * The browser to remove the beforeunload event listener from. + * @param {int} howMany + * How many beforeunload event listeners to remove. + * @param {optional int} frameDepth + * The depth of the frame to remove the event listener from. Defaults + * to 0, which is the top-level content window. + * @return {Promise} + */ +function removeBeforeUnloadListeners(browser, howMany = 1, frameDepth = 0) { + return controlFrameAt(browser, frameDepth, { + name: "RemoveBeforeUnload", + howMany, + }); +} + +/** + * Removes 1 or more inert beforeunload event listeners in this browser on + * a particular subframe. By default, this will target the first subframe + * under the top-level content window, but callers can specify the index + * of a subframe to target. See prepareSubframes for an idea of how the + * subframes are structured. + * + * Note that this removes the beforeunload event listener on the "outer" window, + * by doing: + * + * iframe.removeEventListener("beforeunload", ...); + * + * @param {<xul:browser>} browser + * The browser to remove the beforeunload event listener from. + * @param {int} howMany + * How many beforeunload event listeners to remove. + * @param {optional int} frameDepth + * The depth of the frame to remove the event listener from. Defaults + * to 1, which is the first subframe inside the top-level content + * window. Setting this to 0 will throw. + * @return {Promise} + */ +function removeOuterBeforeUnloadListeners( + browser, + howMany = 1, + frameDepth = 1 +) { + if (frameDepth == 0) { + throw new Error( + "When removing a beforeunload listener from an outer " + + "window, the frame you're targeting needs to be at " + + "depth > 0." + ); + } + + return controlFrameAt(browser, frameDepth, { + name: "RemoveOuterBeforeUnload", + howMany, + }); +} + +/** + * Navigates a content window to a particular URL and waits for it to + * finish loading that URL. + * + * By default, will target the top-level content window, but callers + * can specify the index of a subframe to target. See prepareSubframes + * for an idea of how the subframes are structured. + * + * @param {<xul:browser>} browser + * The browser that will have the navigation occur within it. + * @param {string} url + * The URL to send the content window to. + * @param {optional int} frameDepth + * The depth of the frame to navigate. Defaults to 0, which is + * the top-level content window. + * @return {Promise} + */ +function navigateSubframe(browser, url, frameDepth = 0) { + let navigatePromise = controlFrameAt(browser, frameDepth, { + name: "Navigate", + url, + }); + let subframeLoad = BrowserTestUtils.browserLoaded(browser, true); + return Promise.all([navigatePromise, subframeLoad]); +} + +/** + * Removes the <iframe> from a content window pointed at PAGE_URL. + * + * By default, will target the top-level content window, but callers + * can specify the index of a subframe to target. See prepareSubframes + * for an idea of how the subframes are structured. + * + * @param {<xul:browser>} browser + * The browser that will have removal occur within it. + * @param {optional int} frameDepth + * The depth of the frame that will have the removal occur within + * it. Defaults to 0, which is the top-level content window, meaning + * that the first subframe will be removed. + * @return {Promise} + */ +function removeSubframeFrom(browser, frameDepth = 0) { + return controlFrameAt(browser, frameDepth, { + name: "RemoveSubframe", + }); +} + +/** + * Sends a command to a frame pointed at PAGE_URL. There are utility + * functions defined in this file that call this function. You should + * use those instead. + * + * @param {<xul:browser>} browser + * The browser to send the command to. + * @param {int} frameDepth + * The depth of the frame that we'll send the command to. 0 means + * sending it to the top-level content window. + * @param {object} command + * An object with the following structure: + * + * { + * name: (string), + * <arbitrary arguments to send with the command> + * } + * + * Here are the commands that can be sent: + * + * AddBeforeUnload + * {int} howMany + * How many beforeunload event listeners to add. + * + * AddOuterBeforeUnload + * {int} howMany + * How many beforeunload event listeners to add to + * the iframe in the document at this depth. + * + * RemoveBeforeUnload + * {int} howMany + * How many beforeunload event listeners to remove. + * + * RemoveOuterBeforeUnload + * {int} howMany + * How many beforeunload event listeners to remove from + * the iframe in the document at this depth. + * + * Navigate + * {string} url + * The URL to send the frame to. + * + * RemoveSubframe + * + * @return {Promise} + */ +function controlFrameAt(browser, frameDepth, command) { + return SpecialPowers.spawn( + browser, + [{ frameDepth, command }], + async function (args) { + const { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" + ); + + let { command: contentCommand, frameDepth: contentFrameDepth } = args; + + let targetContent = content; + let targetSubframe = content.document.getElementById("subframe"); + + // We want to not only find the frame that maps to the + // target frame depth that we've been given, but we also want + // to count the total depth so that if a middle frame is removed + // or navigated, then we know how many outer-window-destroyed + // observer notifications to expect. + let currentContent = targetContent; + let currentSubframe = targetSubframe; + + let depth = 0; + + do { + currentContent = currentSubframe.contentWindow; + currentSubframe = currentContent.document.getElementById("subframe"); + depth++; + if (depth == contentFrameDepth) { + targetContent = currentContent; + targetSubframe = currentSubframe; + } + } while (currentSubframe); + + switch (contentCommand.name) { + case "AddBeforeUnload": { + let BeforeUnloader = targetContent.wrappedJSObject.BeforeUnloader; + Assert.ok(BeforeUnloader, "Found BeforeUnloader in the test page."); + BeforeUnloader.pushInner(contentCommand.howMany); + break; + } + case "AddOuterBeforeUnload": { + let BeforeUnloader = targetContent.wrappedJSObject.BeforeUnloader; + Assert.ok(BeforeUnloader, "Found BeforeUnloader in the test page."); + BeforeUnloader.pushOuter(contentCommand.howMany); + break; + } + case "RemoveBeforeUnload": { + let BeforeUnloader = targetContent.wrappedJSObject.BeforeUnloader; + Assert.ok(BeforeUnloader, "Found BeforeUnloader in the test page."); + BeforeUnloader.popInner(contentCommand.howMany); + break; + } + case "RemoveOuterBeforeUnload": { + let BeforeUnloader = targetContent.wrappedJSObject.BeforeUnloader; + Assert.ok(BeforeUnloader, "Found BeforeUnloader in the test page."); + BeforeUnloader.popOuter(contentCommand.howMany); + break; + } + case "Navigate": { + // How many frames are going to be destroyed when we do this? We + // need to wait for that many window destroyed notifications. + targetContent.location = contentCommand.url; + + let destroyedOuterWindows = depth - contentFrameDepth; + if (destroyedOuterWindows) { + await TestUtils.topicObserved("outer-window-destroyed", () => { + destroyedOuterWindows--; + return !destroyedOuterWindows; + }); + } + break; + } + case "RemoveSubframe": { + let subframe = targetContent.document.getElementById("subframe"); + Assert.ok( + subframe, + "Found subframe at frame depth of " + contentFrameDepth + ); + subframe.remove(); + + let destroyedOuterWindows = depth - contentFrameDepth; + if (destroyedOuterWindows) { + await TestUtils.topicObserved("outer-window-destroyed", () => { + destroyedOuterWindows--; + return !destroyedOuterWindows; + }); + } + break; + } + } + } + ).catch(console.error); +} + +/** + * Sets up a structure where a page at PAGE_URL will host an + * <iframe> also pointed at PAGE_URL, and does this repeatedly + * until we've achieved the desired frame depth. Note that this + * will cause the top-level browser to reload, and wipe out any + * previous changes to the DOM under it. + * + * @param {<xul:browser>} browser + * The browser in which we'll load our structure at the + * top level. + * @param {Array<object>} options + * Set-up options for each subframe. The following properties + * are accepted: + * + * {string} sandboxAttributes + * The value to set the sandbox attribute to. If null, no sandbox + * attribute will be set (and any pre-existing sandbox attributes) + * on the <iframe> will be removed. + * + * The number of entries on the options Array corresponds to how many + * subframes are under the top-level content window. + * + * Example: + * + * yield prepareSubframes(browser, [ + * { sandboxAttributes: null }, + * { sandboxAttributes: "allow-modals" }, + * ]); + * + * This would create the following structure: + * + * <top-level content window at PAGE_URL> + * | + * |--> <iframe at PAGE_URL, no sandbox attributes> + * | + * |--> <iframe at PAGE_URL, sandbox="allow-modals"> + * + * @return {Promise} + */ +async function prepareSubframes(browser, options) { + browser.reload(); + await BrowserTestUtils.browserLoaded(browser); + + await SpecialPowers.spawn( + browser, + [{ options, PAGE_URL }], + async function (args) { + let { options: allSubframeOptions, PAGE_URL: contentPageURL } = args; + function loadBeforeUnloadHelper(doc, subframeOptions) { + let subframe = doc.getElementById("subframe"); + subframe.remove(); + if (subframeOptions.sandboxAttributes === null) { + subframe.removeAttribute("sandbox"); + } else { + subframe.setAttribute("sandbox", subframeOptions.sandboxAttributes); + } + doc.body.appendChild(subframe); + subframe.contentWindow.location = contentPageURL; + return ContentTaskUtils.waitForEvent(subframe, "load").then(() => { + return subframe.contentDocument; + }); + } + + let currentDoc = content.document; + for (let subframeOptions of allSubframeOptions) { + currentDoc = await loadBeforeUnloadHelper(currentDoc, subframeOptions); + } + } + ); +} + +/** + * Ensures that a browser's nsIRemoteTab hasBeforeUnload attribute + * is set to the expected value. + * + * @param {<xul:browser>} browser + * The browser whose nsIRemoteTab we will check. + * @param {bool} expected + * True if hasBeforeUnload is expected to be true. + */ +function assertHasBeforeUnload(browser, expected) { + Assert.equal(browser.hasBeforeUnload, expected); +} + +/** + * Tests that the MozBrowser hasBeforeUnload property works under + * a number of different scenarios on inner windows. At a high-level, + * we test that hasBeforeUnload works properly during page / iframe + * navigation, or when an <iframe> with a beforeunload listener on its + * inner window is removed from the DOM. + */ +add_task(async function test_inner_window_scenarios() { + // Turn this off because the test expects the page to be not bfcached. + await SpecialPowers.pushPrefEnv({ + set: [ + ["docshell.shistory.bfcache.ship_allow_beforeunload_listeners", false], + ], + }); + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: PAGE_URL, + }, + async function (browser) { + Assert.ok( + browser.isRemoteBrowser, + "This test only makes sense with out of process browsers." + ); + assertHasBeforeUnload(browser, false); + + // Test the simple case on the top-level window by adding a single + // beforeunload event listener on the inner window and then removing + // it. + await addBeforeUnloadListeners(browser); + assertHasBeforeUnload(browser, true); + await removeBeforeUnloadListeners(browser); + assertHasBeforeUnload(browser, false); + + // Now let's add several beforeunload listeners, and + // ensure that we only set hasBeforeUnload to false once + // the last listener is removed. + await addBeforeUnloadListeners(browser, 3); + assertHasBeforeUnload(browser, true); + await removeBeforeUnloadListeners(browser); // 2 left... + assertHasBeforeUnload(browser, true); + await removeBeforeUnloadListeners(browser); // 1 left... + assertHasBeforeUnload(browser, true); + await removeBeforeUnloadListeners(browser); // None left! + + assertHasBeforeUnload(browser, false); + + // Now let's have the top-level content window navigate + // away with a beforeunload listener set, and ensure + // that we clear the hasBeforeUnload value. + await addBeforeUnloadListeners(browser, 5); + await navigateSubframe(browser, "http://example.com"); + assertHasBeforeUnload(browser, false); + + // Now send the page back to the test page for + // the next few tests. + BrowserTestUtils.loadURIString(browser, PAGE_URL); + await BrowserTestUtils.browserLoaded(browser); + + // We want to test hasBeforeUnload works properly with + // beforeunload event listeners in <iframe> elements too. + // We prepare a structure like this with 3 content windows + // to exercise: + // + // <top-level content window at PAGE_URL> (TOP) + // | + // |--> <iframe at PAGE_URL> (MIDDLE) + // | + // |--> <iframe at PAGE_URL> (BOTTOM) + // + await prepareSubframes(browser, [ + { sandboxAttributes: null }, + { sandboxAttributes: null }, + ]); + // These constants are just to make it easier to know which + // frame we're referring to without having to remember the + // exact indices. + const TOP = 0; + const MIDDLE = 1; + const BOTTOM = 2; + + // We should initially start with hasBeforeUnload set to false. + assertHasBeforeUnload(browser, false); + + // Tests that if there are beforeunload event listeners on + // all levels of our window structure, that we only set + // hasBeforeUnload to false once the last beforeunload + // listener has been unset. + await addBeforeUnloadListeners(browser, 2, MIDDLE); + assertHasBeforeUnload(browser, true); + await addBeforeUnloadListeners(browser, 1, TOP); + assertHasBeforeUnload(browser, true); + await addBeforeUnloadListeners(browser, 5, BOTTOM); + assertHasBeforeUnload(browser, true); + + await removeBeforeUnloadListeners(browser, 1, TOP); + assertHasBeforeUnload(browser, true); + await removeBeforeUnloadListeners(browser, 5, BOTTOM); + assertHasBeforeUnload(browser, true); + await removeBeforeUnloadListeners(browser, 2, MIDDLE); + assertHasBeforeUnload(browser, false); + + // Tests that if a beforeunload event listener is set on + // an iframe that navigates away to a page without a + // beforeunload listener, that hasBeforeUnload is set + // to false. + await addBeforeUnloadListeners(browser, 5, BOTTOM); + assertHasBeforeUnload(browser, true); + + await navigateSubframe(browser, "http://example.com", BOTTOM); + assertHasBeforeUnload(browser, false); + + // Reset our window structure now. + await prepareSubframes(browser, [ + { sandboxAttributes: null }, + { sandboxAttributes: null }, + ]); + + // This time, add beforeunload event listeners to both the + // MIDDLE and BOTTOM frame, and then navigate the MIDDLE + // away. This should set hasBeforeUnload to false. + await addBeforeUnloadListeners(browser, 3, MIDDLE); + await addBeforeUnloadListeners(browser, 1, BOTTOM); + assertHasBeforeUnload(browser, true); + await navigateSubframe(browser, "http://example.com", MIDDLE); + assertHasBeforeUnload(browser, false); + + // Tests that if the MIDDLE and BOTTOM frames have beforeunload + // event listeners, and if we remove the BOTTOM <iframe> and the + // MIDDLE <iframe>, that hasBeforeUnload is set to false. + await prepareSubframes(browser, [ + { sandboxAttributes: null }, + { sandboxAttributes: null }, + ]); + await addBeforeUnloadListeners(browser, 3, MIDDLE); + await addBeforeUnloadListeners(browser, 1, BOTTOM); + assertHasBeforeUnload(browser, true); + await removeSubframeFrom(browser, MIDDLE); + assertHasBeforeUnload(browser, true); + await removeSubframeFrom(browser, TOP); + assertHasBeforeUnload(browser, false); + + // Tests that if the MIDDLE and BOTTOM frames have beforeunload + // event listeners, and if we remove just the MIDDLE <iframe>, that + // hasBeforeUnload is set to false. + await prepareSubframes(browser, [ + { sandboxAttributes: null }, + { sandboxAttributes: null }, + ]); + await addBeforeUnloadListeners(browser, 3, MIDDLE); + await addBeforeUnloadListeners(browser, 1, BOTTOM); + assertHasBeforeUnload(browser, true); + await removeSubframeFrom(browser, TOP); + assertHasBeforeUnload(browser, false); + + // Test that two sandboxed iframes, _without_ the allow-modals + // permission, do not result in the hasBeforeUnload attribute + // being set to true when beforeunload event listeners are added. + await prepareSubframes(browser, [ + { sandboxAttributes: "allow-scripts" }, + { sandboxAttributes: "allow-scripts" }, + ]); + + await addBeforeUnloadListeners(browser, 3, MIDDLE); + await addBeforeUnloadListeners(browser, 1, BOTTOM); + assertHasBeforeUnload(browser, false); + + await removeBeforeUnloadListeners(browser, 3, MIDDLE); + await removeBeforeUnloadListeners(browser, 1, BOTTOM); + assertHasBeforeUnload(browser, false); + + // Test that two sandboxed iframes, both with the allow-modals + // permission, cause the hasBeforeUnload attribute to be set + // to true when beforeunload event listeners are added. + await prepareSubframes(browser, [ + { sandboxAttributes: "allow-scripts allow-modals" }, + { sandboxAttributes: "allow-scripts allow-modals" }, + ]); + + await addBeforeUnloadListeners(browser, 3, MIDDLE); + await addBeforeUnloadListeners(browser, 1, BOTTOM); + assertHasBeforeUnload(browser, true); + + await removeBeforeUnloadListeners(browser, 1, BOTTOM); + assertHasBeforeUnload(browser, true); + await removeBeforeUnloadListeners(browser, 3, MIDDLE); + assertHasBeforeUnload(browser, false); + } + ); +}); + +/** + * Tests that the nsIRemoteTab hasBeforeUnload attribute works under + * a number of different scenarios on outer windows. Very similar to + * the above set of tests, except that we add the beforeunload listeners + * to the iframe DOM nodes instead of the inner windows. + */ +add_task(async function test_outer_window_scenarios() { + // Turn this off because the test expects the page to be not bfcached. + await SpecialPowers.pushPrefEnv({ + set: [ + ["docshell.shistory.bfcache.ship_allow_beforeunload_listeners", false], + ], + }); + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: PAGE_URL, + }, + async function (browser) { + Assert.ok( + browser.isRemoteBrowser, + "This test only makes sense with out of process browsers." + ); + assertHasBeforeUnload(browser, false); + + // We want to test hasBeforeUnload works properly with + // beforeunload event listeners in <iframe> elements. + // We prepare a structure like this with 3 content windows + // to exercise: + // + // <top-level content window at PAGE_URL> (TOP) + // | + // |--> <iframe at PAGE_URL> (MIDDLE) + // | + // |--> <iframe at PAGE_URL> (BOTTOM) + // + await prepareSubframes(browser, [ + { sandboxAttributes: null }, + { sandboxAttributes: null }, + ]); + + // These constants are just to make it easier to know which + // frame we're referring to without having to remember the + // exact indices. + const TOP = 0; + const MIDDLE = 1; + const BOTTOM = 2; + + // Test the simple case on the top-level window by adding a single + // beforeunload event listener on the outer window of the iframe + // in the TOP document. + await addOuterBeforeUnloadListeners(browser); + assertHasBeforeUnload(browser, true); + + await removeOuterBeforeUnloadListeners(browser); + assertHasBeforeUnload(browser, false); + + // Now let's add several beforeunload listeners, and + // ensure that we only set hasBeforeUnload to false once + // the last listener is removed. + await addOuterBeforeUnloadListeners(browser, 3); + assertHasBeforeUnload(browser, true); + await removeOuterBeforeUnloadListeners(browser); // 2 left... + assertHasBeforeUnload(browser, true); + await removeOuterBeforeUnloadListeners(browser); // 1 left... + assertHasBeforeUnload(browser, true); + await removeOuterBeforeUnloadListeners(browser); // None left! + + assertHasBeforeUnload(browser, false); + + // Now let's have the top-level content window navigate away + // with a beforeunload listener set on the outer window of the + // iframe inside it, and ensure that we clear the hasBeforeUnload + // value. + await addOuterBeforeUnloadListeners(browser, 5); + await navigateSubframe(browser, "http://example.com", TOP); + assertHasBeforeUnload(browser, false); + + // Now send the page back to the test page for + // the next few tests. + BrowserTestUtils.loadURIString(browser, PAGE_URL); + await BrowserTestUtils.browserLoaded(browser); + + // We should initially start with hasBeforeUnload set to false. + assertHasBeforeUnload(browser, false); + + await prepareSubframes(browser, [ + { sandboxAttributes: null }, + { sandboxAttributes: null }, + ]); + + // Tests that if there are beforeunload event listeners on + // all levels of our window structure, that we only set + // hasBeforeUnload to false once the last beforeunload + // listener has been unset. + await addOuterBeforeUnloadListeners(browser, 3, MIDDLE); + assertHasBeforeUnload(browser, true); + await addOuterBeforeUnloadListeners(browser, 7, BOTTOM); + assertHasBeforeUnload(browser, true); + + await removeOuterBeforeUnloadListeners(browser, 7, BOTTOM); + assertHasBeforeUnload(browser, true); + await removeOuterBeforeUnloadListeners(browser, 3, MIDDLE); + assertHasBeforeUnload(browser, false); + + // Tests that if a beforeunload event listener is set on + // an iframe that navigates away to a page without a + // beforeunload listener, that hasBeforeUnload is set + // to false. We're setting the event listener on the + // outer window on the <iframe> in the MIDDLE, which + // itself contains the BOTTOM frame it our structure. + await addOuterBeforeUnloadListeners(browser, 5, BOTTOM); + assertHasBeforeUnload(browser, true); + + // Now navigate that BOTTOM frame. + await navigateSubframe(browser, "http://example.com", BOTTOM); + assertHasBeforeUnload(browser, false); + + // Reset our window structure now. + await prepareSubframes(browser, [ + { sandboxAttributes: null }, + { sandboxAttributes: null }, + ]); + + // This time, add beforeunload event listeners to the outer + // windows for MIDDLE and BOTTOM. Then navigate the MIDDLE + // frame. This should set hasBeforeUnload to false. + await addOuterBeforeUnloadListeners(browser, 3, MIDDLE); + await addOuterBeforeUnloadListeners(browser, 1, BOTTOM); + assertHasBeforeUnload(browser, true); + await navigateSubframe(browser, "http://example.com", MIDDLE); + assertHasBeforeUnload(browser, false); + + // Adds beforeunload event listeners to the outer windows of + // MIDDLE and BOTOTM, and then removes those iframes. Removing + // both iframes should set hasBeforeUnload to false. + await prepareSubframes(browser, [ + { sandboxAttributes: null }, + { sandboxAttributes: null }, + ]); + await addOuterBeforeUnloadListeners(browser, 3, MIDDLE); + await addOuterBeforeUnloadListeners(browser, 1, BOTTOM); + assertHasBeforeUnload(browser, true); + await removeSubframeFrom(browser, BOTTOM); + assertHasBeforeUnload(browser, true); + await removeSubframeFrom(browser, MIDDLE); + assertHasBeforeUnload(browser, false); + + // Adds beforeunload event listeners to the outer windows of MIDDLE + // and BOTTOM, and then removes just the MIDDLE iframe (which will + // take the bottom one with it). This should set hasBeforeUnload to + // false. + await prepareSubframes(browser, [ + { sandboxAttributes: null }, + { sandboxAttributes: null }, + ]); + await addOuterBeforeUnloadListeners(browser, 3, MIDDLE); + await addOuterBeforeUnloadListeners(browser, 1, BOTTOM); + assertHasBeforeUnload(browser, true); + await removeSubframeFrom(browser, TOP); + assertHasBeforeUnload(browser, false); + + // Test that two sandboxed iframes, _without_ the allow-modals + // permission, do not result in the hasBeforeUnload attribute + // being set to true when beforeunload event listeners are added + // to the outer windows. Note that this requires the + // allow-same-origin permission, otherwise a cross-origin + // security exception is thrown. + await prepareSubframes(browser, [ + { sandboxAttributes: "allow-same-origin allow-scripts" }, + { sandboxAttributes: "allow-same-origin allow-scripts" }, + ]); + + await addOuterBeforeUnloadListeners(browser, 3, MIDDLE); + await addOuterBeforeUnloadListeners(browser, 1, BOTTOM); + assertHasBeforeUnload(browser, false); + + await removeOuterBeforeUnloadListeners(browser, 3, MIDDLE); + await removeOuterBeforeUnloadListeners(browser, 1, BOTTOM); + assertHasBeforeUnload(browser, false); + + // Test that two sandboxed iframes, both with the allow-modals + // permission, cause the hasBeforeUnload attribute to be set + // to true when beforeunload event listeners are added. Note + // that this requires the allow-same-origin permission, + // otherwise a cross-origin security exception is thrown. + await prepareSubframes(browser, [ + { sandboxAttributes: "allow-same-origin allow-scripts allow-modals" }, + { sandboxAttributes: "allow-same-origin allow-scripts allow-modals" }, + ]); + + await addOuterBeforeUnloadListeners(browser, 3, MIDDLE); + await addOuterBeforeUnloadListeners(browser, 1, BOTTOM); + assertHasBeforeUnload(browser, true); + + await removeOuterBeforeUnloadListeners(browser, 1, BOTTOM); + assertHasBeforeUnload(browser, true); + await removeOuterBeforeUnloadListeners(browser, 3, MIDDLE); + assertHasBeforeUnload(browser, false); + } + ); +}); + +/** + * Tests hasBeforeUnload behaviour when beforeunload event listeners + * are added on both inner and outer windows. + */ +add_task(async function test_mixed_inner_and_outer_window_scenarios() { + // Turn this off because the test expects the page to be not bfcached. + await SpecialPowers.pushPrefEnv({ + set: [ + ["docshell.shistory.bfcache.ship_allow_beforeunload_listeners", false], + ], + }); + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: PAGE_URL, + }, + async function (browser) { + Assert.ok( + browser.isRemoteBrowser, + "This test only makes sense with out of process browsers." + ); + assertHasBeforeUnload(browser, false); + + // We want to test hasBeforeUnload works properly with + // beforeunload event listeners in <iframe> elements. + // We prepare a structure like this with 3 content windows + // to exercise: + // + // <top-level content window at PAGE_URL> (TOP) + // | + // |--> <iframe at PAGE_URL> (MIDDLE) + // | + // |--> <iframe at PAGE_URL> (BOTTOM) + // + await prepareSubframes(browser, [ + { sandboxAttributes: null }, + { sandboxAttributes: null }, + ]); + + // These constants are just to make it easier to know which + // frame we're referring to without having to remember the + // exact indices. + const TOP = 0; + const MIDDLE = 1; + const BOTTOM = 2; + + await addBeforeUnloadListeners(browser, 1, TOP); + assertHasBeforeUnload(browser, true); + await addBeforeUnloadListeners(browser, 2, MIDDLE); + assertHasBeforeUnload(browser, true); + await addBeforeUnloadListeners(browser, 5, BOTTOM); + assertHasBeforeUnload(browser, true); + + await addOuterBeforeUnloadListeners(browser, 3, MIDDLE); + assertHasBeforeUnload(browser, true); + await addOuterBeforeUnloadListeners(browser, 7, BOTTOM); + assertHasBeforeUnload(browser, true); + + await removeBeforeUnloadListeners(browser, 5, BOTTOM); + assertHasBeforeUnload(browser, true); + + await removeBeforeUnloadListeners(browser, 2, MIDDLE); + assertHasBeforeUnload(browser, true); + + await removeOuterBeforeUnloadListeners(browser, 3, MIDDLE); + assertHasBeforeUnload(browser, true); + + await removeBeforeUnloadListeners(browser, 1, TOP); + assertHasBeforeUnload(browser, true); + + await removeOuterBeforeUnloadListeners(browser, 7, BOTTOM); + assertHasBeforeUnload(browser, false); + } + ); +}); diff --git a/dom/tests/browser/browser_keypressTelemetry.js b/dom/tests/browser/browser_keypressTelemetry.js new file mode 100644 index 0000000000..02d5ddfe19 --- /dev/null +++ b/dom/tests/browser/browser_keypressTelemetry.js @@ -0,0 +1,69 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +var EventUtils = {}; +var PaintListener = {}; +Services.scriptloader.loadSubScript( + "chrome://mochikit/content/tests/SimpleTest/EventUtils.js", + EventUtils +); + +function getRecordedKeypressCount() { + let snapshot = Services.telemetry.getSnapshotForHistograms("main", false); + + var totalCount = 0; + for (var prop in snapshot) { + if (snapshot[prop].KEYPRESS_PRESENT_LATENCY) { + dump("found snapshot"); + totalCount += Object.values( + snapshot[prop].KEYPRESS_PRESENT_LATENCY.values + ).reduce((a, b) => a + b, 0); + } + } + + return totalCount; +} + +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["toolkit.telemetry.ipcBatchTimeout", 10]], + }); + let histogram = Services.telemetry.getHistogramById( + "KEYPRESS_PRESENT_LATENCY" + ); + histogram.clear(); + + waitForExplicitFinish(); + + gURLBar.focus(); + await SimpleTest.promiseFocus(window); + EventUtils.sendChar("x"); + + await ContentTaskUtils.waitForCondition( + () => { + return getRecordedKeypressCount() > 0; + }, + "waiting for telemetry", + 200, + 600 + ); + let result = getRecordedKeypressCount(); + ok(result == 1, "One keypress recorded"); + + gURLBar.focus(); + await SimpleTest.promiseFocus(window); + EventUtils.sendChar("x"); + + await ContentTaskUtils.waitForCondition( + () => { + return getRecordedKeypressCount() > 1; + }, + "waiting for telemetry", + 200, + 600 + ); + result = getRecordedKeypressCount(); + ok(result == 2, "Two keypresses recorded"); +}); diff --git a/dom/tests/browser/browser_localStorage_e10s.js b/dom/tests/browser/browser_localStorage_e10s.js new file mode 100644 index 0000000000..0ecc49fe49 --- /dev/null +++ b/dom/tests/browser/browser_localStorage_e10s.js @@ -0,0 +1,284 @@ +const HELPER_PAGE_URL = + "http://example.com/browser/dom/tests/browser/page_localstorage.html"; +const HELPER_PAGE_ORIGIN = "http://example.com/"; + +let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/")); +Services.scriptloader.loadSubScript(testDir + "/helper_localStorage.js", this); + +/* import-globals-from helper_localStorage.js */ + +// We spin up a ton of child processes. +requestLongerTimeout(4); + +/** + * Verify the basics of our multi-e10s localStorage support. We are focused on + * whitebox testing two things. When this is being written, broadcast filtering + * is not in place, but the test is intended to attempt to verify that its + * implementation does not break things. + * + * 1) That pages see the same localStorage state in a timely fashion when + * engaging in non-conflicting operations. We are not testing races or + * conflict resolution; the spec does not cover that. + * + * 2) That there are no edge-cases related to when the Storage instance is + * created for the page or the StorageCache for the origin. (StorageCache is + * what actually backs the Storage binding exposed to the page.) This + * matters because the following reasons can exist for them to be created: + * - Preload, on the basis of knowing the origin uses localStorage. The + * interesting edge case is when we have the same origin open in different + * processes and the origin starts using localStorage when it did not + * before. Preload will not have instantiated bindings, which could impact + * correctness. + * - The page accessing localStorage for read or write purposes. This is the + * obvious, boring one. + * - The page adding a "storage" listener. This is less obvious and + * interacts with the preload edge-case mentioned above. The page needs to + * hear "storage" events even if the page has not touched localStorage + * itself and its origin had nothing stored in localStorage when the page + * was created. + * + * We use the same simple child page in all tabs that: + * - can be instructed to listen for and record "storage" events + * - can be instructed to issue a series of localStorage writes + * - can be instructed to return the current entire localStorage contents + * + * We open the 5 following tabs: + * - Open a "writer" tab that does not listen for "storage" events and will + * issue only writes. + * - Open a "listener" tab instructed to listen for "storage" events + * immediately. We expect it to capture all events. + * - Open an "reader" tab that does not listen for "storage" events and will + * only issue reads when instructed. + * - Open a "lateWriteThenListen" tab that initially does nothing. We will + * later tell it to issue a write and then listen for events to make sure it + * captures the later events. + * - Open "lateOpenSeesPreload" tab after we've done everything and ensure that + * it preloads/precaches the data without us having touched localStorage or + * added an event listener. + */ +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + // Stop the preallocated process manager from speculatively creating + // processes. Our test explicitly asserts on whether preload happened or + // not for each tab's process. This information is loaded and latched by + // the StorageDBParent constructor which the child process's + // LocalStorageManager() constructor causes to be created via a call to + // LocalStorageCache::StartDatabase(). Although the service is lazily + // created and should not have been created prior to our opening the tab, + // it's safest to ensure the process simply didn't exist before we ask for + // it. + // + // This is done in conjunction with our use of forceNewProcess when + // opening tabs. There would be no point if we weren't also requesting a + // new process. + ["dom.ipc.processPrelaunch.enabled", false], + // Enable LocalStorage's testing API so we can explicitly trigger a flush + // when needed. + ["dom.storage.testing", true], + ], + }); + + // Ensure that there is no localstorage data or potential false positives for + // localstorage preloads by forcing the origin to be cleared prior to the + // start of our test. + await clearOriginStorageEnsuringNoPreload(HELPER_PAGE_ORIGIN); + + // Make sure mOriginsHavingData gets updated. + await triggerAndWaitForLocalStorageFlush(); + + // - Open tabs. Don't configure any of them yet. + const knownTabs = new KnownTabs(); + const writerTab = await openTestTab( + HELPER_PAGE_URL, + "writer", + knownTabs, + true + ); + const listenerTab = await openTestTab( + HELPER_PAGE_URL, + "listener", + knownTabs, + true + ); + const readerTab = await openTestTab( + HELPER_PAGE_URL, + "reader", + knownTabs, + true + ); + const lateWriteThenListenTab = await openTestTab( + HELPER_PAGE_URL, + "lateWriteThenListen", + knownTabs, + true + ); + + // Sanity check that preloading did not occur in the tabs. + await verifyTabPreload(writerTab, false, HELPER_PAGE_ORIGIN); + await verifyTabPreload(listenerTab, false, HELPER_PAGE_ORIGIN); + await verifyTabPreload(readerTab, false, HELPER_PAGE_ORIGIN); + + // - Configure the tabs. + const initialSentinel = "initial"; + const noSentinelCheck = null; + await recordTabStorageEvents(listenerTab, initialSentinel); + + // - Issue the initial batch of writes and verify. + info("initial writes"); + const initialWriteMutations = [ + // [key (null=clear), newValue (null=delete), oldValue (verification)] + ["getsCleared", "1", null], + ["alsoGetsCleared", "2", null], + [null, null, null], + ["stays", "3", null], + ["clobbered", "pre", null], + ["getsDeletedLater", "4", null], + ["getsDeletedImmediately", "5", null], + ["getsDeletedImmediately", null, "5"], + ["alsoStays", "6", null], + ["getsDeletedLater", null, "4"], + ["clobbered", "post", "pre"], + ]; + const initialWriteState = { + stays: "3", + clobbered: "post", + alsoStays: "6", + }; + + await mutateTabStorage(writerTab, initialWriteMutations, initialSentinel); + + // We expect the writer tab to have the correct state because it just did the + // writes. We do not perform a sentinel-check because the writes should be + // locally available and consistent. + await verifyTabStorageState(writerTab, initialWriteState, noSentinelCheck); + // We expect the listener tab to have heard all events despite preload not + // having occurred and despite not issuing any reads or writes itself. We + // intentionally check the events before the state because we're most + // interested in adding the listener having had a side-effect of subscribing + // to changes for the process. + // + // We ensure it had a chance to hear all of the events because we told + // recordTabStorageEvents to listen for the given sentinel. The state check + // then does not need to do a sentinel check. + await verifyTabStorageEvents( + listenerTab, + initialWriteMutations, + initialSentinel + ); + await verifyTabStorageState(listenerTab, initialWriteState, noSentinelCheck); + // We expect the reader tab to retrieve the current localStorage state from + // the database. Because of the above checks, we are confident that the + // writes have hit PBackground and therefore that the (synchronous) state + // retrieval contains all the data we need. No sentinel-check is required. + await verifyTabStorageState(readerTab, initialWriteState, noSentinelCheck); + + // - Issue second set of writes from lateWriteThenListen + // This tests that our new tab that begins by issuing only writes is building + // on top of the existing state (although we don't verify that until after the + // next set of mutations). We also verify that the initial "writerTab" that + // was our first tab and started with only writes sees the writes, even though + // it did not add an event listener. + + info("late writes"); + const lateWriteSentinel = "lateWrite"; + const lateWriteMutations = [ + ["lateStays", "10", null], + ["lateClobbered", "latePre", null], + ["lateDeleted", "11", null], + ["lateClobbered", "lastPost", "latePre"], + ["lateDeleted", null, "11"], + ]; + const lateWriteState = Object.assign({}, initialWriteState, { + lateStays: "10", + lateClobbered: "lastPost", + }); + + await recordTabStorageEvents(listenerTab, lateWriteSentinel); + + await mutateTabStorage( + lateWriteThenListenTab, + lateWriteMutations, + lateWriteSentinel + ); + + // Verify the writer tab saw the writes. It has to wait for the sentinel to + // appear before checking. + await verifyTabStorageState(writerTab, lateWriteState, lateWriteSentinel); + // Wait for the sentinel event before checking the events and then the state. + await verifyTabStorageEvents( + listenerTab, + lateWriteMutations, + lateWriteSentinel + ); + await verifyTabStorageState(listenerTab, lateWriteState, noSentinelCheck); + // We need to wait for the sentinel to show up for the reader. + await verifyTabStorageState(readerTab, lateWriteState, lateWriteSentinel); + + // - Issue last set of writes from writerTab. + info("last set of writes"); + const lastWriteSentinel = "lastWrite"; + const lastWriteMutations = [ + ["lastStays", "20", null], + ["lastDeleted", "21", null], + ["lastClobbered", "lastPre", null], + ["lastClobbered", "lastPost", "lastPre"], + ["lastDeleted", null, "21"], + ]; + const lastWriteState = Object.assign({}, lateWriteState, { + lastStays: "20", + lastClobbered: "lastPost", + }); + + await recordTabStorageEvents(listenerTab, lastWriteSentinel); + await recordTabStorageEvents(lateWriteThenListenTab, lastWriteSentinel); + + await mutateTabStorage(writerTab, lastWriteMutations, lastWriteSentinel); + + // The writer performed the writes, no need to wait for the sentinel. + await verifyTabStorageState(writerTab, lastWriteState, noSentinelCheck); + // Wait for the sentinel event to be received, then check. + await verifyTabStorageEvents( + listenerTab, + lastWriteMutations, + lastWriteSentinel + ); + await verifyTabStorageState(listenerTab, lastWriteState, noSentinelCheck); + // We need to wait for the sentinel to show up for the reader. + await verifyTabStorageState(readerTab, lastWriteState, lastWriteSentinel); + // Wait for the sentinel event to be received, then check. + await verifyTabStorageEvents( + lateWriteThenListenTab, + lastWriteMutations, + lastWriteSentinel + ); + await verifyTabStorageState( + lateWriteThenListenTab, + lastWriteState, + noSentinelCheck + ); + + // - Force a LocalStorage DB flush so mOriginsHavingData is updated. + // mOriginsHavingData is only updated when the storage thread runs its + // accumulated operations during the flush. If we don't initiate and ensure + // that a flush has occurred before moving on to the next step, + // mOriginsHavingData may not include our origin when it's sent down to the + // child process. + info("flush to make preload check work"); + await triggerAndWaitForLocalStorageFlush(); + + // - Open a fresh tab and make sure it sees the precache/preload + info("late open preload check"); + const lateOpenSeesPreload = await openTestTab( + HELPER_PAGE_URL, + "lateOpenSeesPreload", + knownTabs, + true + ); + await verifyTabPreload(lateOpenSeesPreload, true, HELPER_PAGE_ORIGIN); + + // - Clean up. + await cleanupTabs(knownTabs); + + clearOriginStorageEnsuringNoPreload(HELPER_PAGE_ORIGIN); +}); diff --git a/dom/tests/browser/browser_localStorage_fis.js b/dom/tests/browser/browser_localStorage_fis.js new file mode 100644 index 0000000000..5db8ec5eb5 --- /dev/null +++ b/dom/tests/browser/browser_localStorage_fis.js @@ -0,0 +1,529 @@ +const HELPER_PAGE_URL = + "https://example.com/browser/dom/tests/browser/page_localstorage.html"; +const HELPER_PAGE_COOP_COEP_URL = + "https://example.com/browser/dom/tests/browser/page_localstorage_coop+coep.html"; +const HELPER_PAGE_ORIGIN = "https://example.com/"; + +let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/")); +Services.scriptloader.loadSubScript(testDir + "/helper_localStorage.js", this); + +/* import-globals-from helper_localStorage.js */ + +// We spin up a ton of child processes. +requestLongerTimeout(4); + +/** + * Verify the basics of our multi-e10s localStorage support with fission. + * We are focused on whitebox testing two things. + * When this is being written, broadcast filtering is not in place, but the test + * is intended to attempt to verify that its implementation does not break things. + * + * 1) That pages see the same localStorage state in a timely fashion when + * engaging in non-conflicting operations. We are not testing races or + * conflict resolution; the spec does not cover that. + * + * 2) That there are no edge-cases related to when the Storage instance is + * created for the page or the StorageCache for the origin. (StorageCache is + * what actually backs the Storage binding exposed to the page.) This + * matters because the following reasons can exist for them to be created: + * - Preload, on the basis of knowing the origin uses localStorage. The + * interesting edge case is when we have the same origin open in different + * processes and the origin starts using localStorage when it did not + * before. Preload will not have instantiated bindings, which could impact + * correctness. + * - The page accessing localStorage for read or write purposes. This is the + * obvious, boring one. + * - The page adding a "storage" listener. This is less obvious and + * interacts with the preload edge-case mentioned above. The page needs to + * hear "storage" events even if the page has not touched localStorage + * itself and its origin had nothing stored in localStorage when the page + * was created. + * + * According to current fission implementation, same origin pages will be loaded + * by the same process, which process type is webIsolated=. And thanks to + * Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy headers support, + * it is possible to load the same origin page by a special process, which type + * is webCOOP+COEP=. These are the only two processes can be used to test + * localStroage consistency between tabs in different tabs. + * + * We use the two child pages for testing, page_localstorage.html and + * page_localstorage_coop+coep.html. Their content are the same, but + * page_localstorage_coop+coep.html will be loaded with its ^headers^ file. + * These pages provide followings + * - can be instructed to listen for and record "storage" events + * - can be instructed to issue a series of localStorage writes + * - can be instructed to return the current entire localStorage contents + * + * To test localStorage consistency, four subtests are used. + * Test case 1: one writer tab and one reader tab + * The writer tab issues a series of write operations, then verify the + * localStorage contents from the reader tab. + * + * Test case 2: one writer tab and one listener tab + * The writer tab issues a series of write operations, then verify the recorded + * storage events from the listener tab. + * + * Test case 3: one writeThenRead tab and one readThenWrite tab + * The writeThenRead first issues a series write of operations, and then verify + * the recorded storage events and localStorage contents from readThenWrite + * tab. After that readThenWrite tab issues a series of write operations, then + * verify the results from writeThenRead tab. + * + * Test case 4: one writer tab and one lateOpenSeesPreload tab + * The writer tab issues a series write of operations. Then open the + * lateOpenSeesPreload tab to make sure preloads exists. + */ + +/** + * Shared constants for test cases + */ +const noSentinelCheck = null; +const initialSentinel = "initial"; +const initialWriteMutations = [ + // [key (null=clear), newValue (null=delete), oldValue (verification)] + ["getsCleared", "1", null], + ["alsoGetsCleared", "2", null], + [null, null, null], + ["stays", "3", null], + ["clobbered", "pre", null], + ["getsDeletedLater", "4", null], + ["getsDeletedImmediately", "5", null], + ["getsDeletedImmediately", null, "5"], + ["alsoStays", "6", null], + ["getsDeletedLater", null, "4"], + ["clobbered", "post", "pre"], +]; +const initialWriteState = { + stays: "3", + clobbered: "post", + alsoStays: "6", +}; + +const lastWriteSentinel = "lastWrite"; +const lastWriteMutations = [ + ["lastStays", "20", null], + ["lastDeleted", "21", null], + ["lastClobbered", "lastPre", null], + ["lastClobbered", "lastPost", "lastPre"], + ["lastDeleted", null, "21"], +]; +const lastWriteState = Object.assign({}, initialWriteState, { + lastStays: "20", + lastClobbered: "lastPost", +}); + +/** + * Test case 1: one writer tab and one reader tab + * Test steps + * 1. Clear origin storage to make sure no data and preloads. + * 2. Open the writer and reader tabs and verify preloads do not exist. + * Open writer tab in webIsolated= process + * Open reader tab in webCOOP+COEP= process + * 3. Issue a series write operations in the writer tab, and then verify the + * storage state on the tab. + * 4. Verify the storage state on the reader tab. + * 5. Issue another series write operations in the writer tab, and then verify + * the storage state on the tab. + * 6. Verify the storage state on the reader tab. + * 7. Close tabs and clear origin storage. + */ +add_task(async function () { + if (!Services.domStorageManager.nextGenLocalStorageEnabled) { + ok(true, "Test ignored when the next gen local storage is not enabled."); + return; + } + + await SpecialPowers.pushPrefEnv({ + set: [ + // Stop the preallocated process manager from speculatively creating + // processes. Our test explicitly asserts on whether preload happened or + // not for each tab's process. This information is loaded and latched by + // the StorageDBParent constructor which the child process's + // LocalStorageManager() constructor causes to be created via a call to + // LocalStorageCache::StartDatabase(). Although the service is lazily + // created and should not have been created prior to our opening the tab, + // it's safest to ensure the process simply didn't exist before we ask for + // it. + // + // This is done in conjunction with our use of forceNewProcess when + // opening tabs. There would be no point if we weren't also requesting a + // new process. + ["dom.ipc.processPrelaunch.enabled", false], + // Enable LocalStorage's testing API so we can explicitly trigger a flush + // when needed. + ["dom.storage.testing", true], + ], + }); + + // Ensure that there is no localstorage data or potential false positives for + // localstorage preloads by forcing the origin to be cleared prior to the + // start of our test. + await clearOriginStorageEnsuringNoPreload(HELPER_PAGE_ORIGIN); + + // Make sure mOriginsHavingData gets updated. + await triggerAndWaitForLocalStorageFlush(); + + // - Open tabs. Don't configure any of them yet. + const knownTabs = new KnownTabs(); + const writerTab = await openTestTab( + HELPER_PAGE_URL, + "writer", + knownTabs, + true + ); + const readerTab = await openTestTab( + HELPER_PAGE_COOP_COEP_URL, + "reader", + knownTabs, + true + ); + // Sanity check that preloading did not occur in the tabs. + await verifyTabPreload(writerTab, false, HELPER_PAGE_ORIGIN); + await verifyTabPreload(readerTab, false, HELPER_PAGE_ORIGIN); + + // - Issue the initial batch of writes and verify. + info("initial writes"); + await mutateTabStorage(writerTab, initialWriteMutations, initialSentinel); + + // We expect the writer tab to have the correct state because it just did the + // writes. We do not perform a sentinel-check because the writes should be + // locally available and consistent. + await verifyTabStorageState(writerTab, initialWriteState, noSentinelCheck); + // We expect the reader tab to retrieve the current localStorage state from + // the database. + await verifyTabStorageState(readerTab, initialWriteState, initialSentinel); + + // - Issue last set of writes from writerTab. + info("last set of writes"); + await mutateTabStorage(writerTab, lastWriteMutations, lastWriteSentinel); + + // The writer performed the writes, no need to wait for the sentinel. + await verifyTabStorageState(writerTab, lastWriteState, noSentinelCheck); + // We need to wait for the sentinel to show up for the reader. + await verifyTabStorageState(readerTab, lastWriteState, lastWriteSentinel); + + // - Clean up. + await cleanupTabs(knownTabs); + + clearOriginStorageEnsuringNoPreload(HELPER_PAGE_ORIGIN); +}); + +/** + * Test case 2: one writer tab and one linsener tab + * Test steps + * 1. Clear origin storage to make sure no data and preloads. + * 2. Open the writer and listener tabs and verify preloads do not exist. + * Open writer tab in webIsolated= process + * Open listener tab in webCOOP+COEP= process + * 3. Ask the listener tab to listen and record storage events. + * 4. Issue a series write operations in the writer tab, and then verify the + * storage state on the tab. + * 5. Verify the storage events record from the listener tab is as expected. + * 6. Verify the storage state on the listener tab. + * 7. Ask the listener tab to listen and record storage events. + * 8. Issue another series write operations in the writer tab, and then verify + * the storage state on the tab. + * 9. Verify the storage events record from the listener tab is as expected. + * 10. Verify the storage state on the listener tab. + * 11. Close tabs and clear origin storage. + */ +add_task(async function () { + if (!Services.domStorageManager.nextGenLocalStorageEnabled) { + ok(true, "Test ignored when the next gen local storage is not enabled."); + return; + } + + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.ipc.processPrelaunch.enabled", false], + ["dom.storage.testing", true], + ], + }); + + // Ensure that there is no localstorage data or potential false positives for + // localstorage preloads by forcing the origin to be cleared prior to the + // start of our test. + await clearOriginStorageEnsuringNoPreload(HELPER_PAGE_ORIGIN); + + // Make sure mOriginsHavingData gets updated. + await triggerAndWaitForLocalStorageFlush(); + + // - Open tabs. Don't configure any of them yet. + const knownTabs = new KnownTabs(); + const writerTab = await openTestTab( + HELPER_PAGE_URL, + "writer", + knownTabs, + true + ); + const listenerTab = await openTestTab( + HELPER_PAGE_COOP_COEP_URL, + "listener", + knownTabs, + true + ); + // Sanity check that preloading did not occur in the tabs. + await verifyTabPreload(writerTab, false, HELPER_PAGE_ORIGIN); + await verifyTabPreload(listenerTab, false, HELPER_PAGE_ORIGIN); + + // - Ask the listener tab to listen and record the storage events.. + await recordTabStorageEvents(listenerTab, initialSentinel); + + // - Issue the initial batch of writes and verify. + info("initial writes"); + await mutateTabStorage(writerTab, initialWriteMutations, initialSentinel); + + // We expect the writer tab to have the correct state because it just did the + // writes. We do not perform a sentinel-check because the writes should be + // locally available and consistent. + await verifyTabStorageState(writerTab, initialWriteState, noSentinelCheck); + // We expect the listener tab to have heard all events despite preload not + // having occurred and despite not issuing any reads or writes itself. We + // intentionally check the events before the state because we're most + // interested in adding the listener having had a side-effect of subscribing + // to changes for the process. + // + // We ensure it had a chance to hear all of the events because we told + // recordTabStorageEvents to listen for the given sentinel. The state check + // then does not need to do a sentinel check. + await verifyTabStorageEvents( + listenerTab, + initialWriteMutations, + initialSentinel + ); + await verifyTabStorageState(listenerTab, initialWriteState, noSentinelCheck); + + // - Ask the listener tab to listen and record the storage events. + await recordTabStorageEvents(listenerTab, lastWriteSentinel); + + // - Issue last set of writes from writerTab. + info("last set of writes"); + await mutateTabStorage(writerTab, lastWriteMutations, lastWriteSentinel); + + // The writer performed the writes, no need to wait for the sentinel. + await verifyTabStorageState(writerTab, lastWriteState, noSentinelCheck); + // Wait for the sentinel event to be received, then check. + await verifyTabStorageEvents( + listenerTab, + lastWriteMutations, + lastWriteSentinel + ); + await verifyTabStorageState(listenerTab, lastWriteState, noSentinelCheck); + + // - Clean up. + await cleanupTabs(knownTabs); + + clearOriginStorageEnsuringNoPreload(HELPER_PAGE_ORIGIN); +}); + +/** + * Test case 3: one writeThenRead tab and one readThenWrite tab + * Test steps + * 1. Clear origin storage to make sure no data and preloads. + * 2. Open the writeThenRead and readThenWrite tabs and verify preloads do not + * exist. + * Open writeThenRead tab in webIsolated= process + * Open readThenWrite tab in webCOOP+COEP= process + * 3. Ask the readThenWrite tab to listen and record storage events. + * 4. Issue a series write operations in the writeThenRead tab, and then verify + * the storage state on the tab. + * 5. Verify the storage events record from the readThenWrite tab is as + * expected. + * 6. Verify the storage state on the readThenWrite tab. + * 7. Ask the writeThenRead tab to listen and record storage events. + * 8. Issue another series write operations in the readThenWrite tab, and then + * verify the storage state on the tab. + * 9. Verify the storage events record from the writeThenRead tab is as + * expected. + * 10. Verify the storage state on the writeThenRead tab. + * 11. Close tabs and clear origin storage. + **/ +add_task(async function () { + if (!Services.domStorageManager.nextGenLocalStorageEnabled) { + ok(true, "Test ignored when the next gen local storage is not enabled."); + return; + } + + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.ipc.processPrelaunch.enabled", false], + ["dom.storage.testing", true], + ], + }); + + // Ensure that there is no localstorage data or potential false positives for + // localstorage preloads by forcing the origin to be cleared prior to the + // start of our test. + await clearOriginStorageEnsuringNoPreload(HELPER_PAGE_ORIGIN); + + // Make sure mOriginsHavingData gets updated. + await triggerAndWaitForLocalStorageFlush(); + + // - Open tabs. Don't configure any of them yet. + const knownTabs = new KnownTabs(); + const writeThenReadTab = await openTestTab( + HELPER_PAGE_URL, + "writerthenread", + knownTabs, + true + ); + const readThenWriteTab = await openTestTab( + HELPER_PAGE_COOP_COEP_URL, + "readthenwrite", + knownTabs, + true + ); + // Sanity check that preloading did not occur in the tabs. + await verifyTabPreload(writeThenReadTab, false, HELPER_PAGE_ORIGIN); + await verifyTabPreload(readThenWriteTab, false, HELPER_PAGE_ORIGIN); + + // - Ask readThenWrite tab to listen and record storageEvents. + await recordTabStorageEvents(readThenWriteTab, initialSentinel); + + // - Issue the initial batch of writes and verify. + info("initial writes"); + await mutateTabStorage( + writeThenReadTab, + initialWriteMutations, + initialSentinel + ); + + // We expect the writer tab to have the correct state because it just did the + // writes. We do not perform a sentinel-check because the writes should be + // locally available and consistent. + await verifyTabStorageState( + writeThenReadTab, + initialWriteState, + noSentinelCheck + ); + + // We expect the listener tab to have heard all events despite preload not + // having occurred and despite not issuing any reads or writes itself. We + // intentionally check the events before the state because we're most + // interested in adding the listener having had a side-effect of subscribing + // to changes for the process. + // + // We ensure it had a chance to hear all of the events because we told + // recordTabStorageEvents to listen for the given sentinel. The state check + // then does not need to do a sentinel check. + await verifyTabStorageEvents( + readThenWriteTab, + initialWriteMutations, + initialSentinel + ); + await verifyTabStorageState( + readThenWriteTab, + initialWriteState, + noSentinelCheck + ); + + // - Issue last set of writes from writerTab. + info("last set of writes"); + await recordTabStorageEvents(writeThenReadTab, lastWriteSentinel); + + await mutateTabStorage( + readThenWriteTab, + lastWriteMutations, + lastWriteSentinel + ); + + // The writer performed the writes, no need to wait for the sentinel. + await verifyTabStorageState( + readThenWriteTab, + lastWriteState, + noSentinelCheck + ); + // Wait for the sentinel event to be received, then check. + await verifyTabStorageEvents( + writeThenReadTab, + lastWriteMutations, + lastWriteSentinel + ); + await verifyTabStorageState( + writeThenReadTab, + lastWriteState, + noSentinelCheck + ); + + // - Clean up. + await cleanupTabs(knownTabs); + + clearOriginStorageEnsuringNoPreload(HELPER_PAGE_ORIGIN); +}); + +/** + * Test case 4: one writerRead tab and one lateOpenSeesPreload tab + * Test steps + * 1. Clear origin storage to make sure no data and preloads. + * 2. Open the writer tab and verify preloads do not exist. + * Open writer tab in webIsolated= process + * 3. Issue a series write operations in the writer tab, and then verify the + * storage state on the tab. + * 4. Issue another series write operations in the writer tab, and then verify + * the storage state on the tab. + * 5. Open lateOpenSeesPreload tab in webCOOP+COEP process + * 6. Verify the preloads on the lateOpenSeesPreload tab + * 7. Close tabs and clear origin storage. + */ +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.ipc.processPrelaunch.enabled", false], + ["dom.storage.testing", true], + ], + }); + + // Ensure that there is no localstorage data or potential false positives for + // localstorage preloads by forcing the origin to be cleared prior to the + // start of our test. + await clearOriginStorageEnsuringNoPreload(HELPER_PAGE_ORIGIN); + + // Make sure mOriginsHavingData gets updated. + await triggerAndWaitForLocalStorageFlush(); + + // - Open tabs. Don't configure any of them yet. + const knownTabs = new KnownTabs(); + const writerTab = await openTestTab( + HELPER_PAGE_URL, + "writer", + knownTabs, + true + ); + // Sanity check that preloading did not occur in the tabs. + await verifyTabPreload(writerTab, false, HELPER_PAGE_ORIGIN); + + // - Configure the tabs. + + // - Issue the initial batch of writes and verify. + info("initial writes"); + await mutateTabStorage(writerTab, initialWriteMutations, initialSentinel); + + // We expect the writer tab to have the correct state because it just did the + // writes. We do not perform a sentinel-check because the writes should be + // locally available and consistent. + await verifyTabStorageState(writerTab, initialWriteState, noSentinelCheck); + + // - Force a LocalStorage DB flush so mOriginsHavingData is updated. + // mOriginsHavingData is only updated when the storage thread runs its + // accumulated operations during the flush. If we don't initiate and ensure + // that a flush has occurred before moving on to the next step, + // mOriginsHavingData may not include our origin when it's sent down to the + // child process. + info("flush to make preload check work"); + await triggerAndWaitForLocalStorageFlush(); + + // - Open a fresh tab and make sure it sees the precache/preload + info("late open preload check"); + const lateOpenSeesPreload = await openTestTab( + HELPER_PAGE_COOP_COEP_URL, + "lateOpenSeesPreload", + knownTabs, + true + ); + await verifyTabPreload(lateOpenSeesPreload, true, HELPER_PAGE_ORIGIN); + + // - Clean up. + await cleanupTabs(knownTabs); + + clearOriginStorageEnsuringNoPreload(HELPER_PAGE_ORIGIN); +}); diff --git a/dom/tests/browser/browser_localStorage_privatestorageevent.js b/dom/tests/browser/browser_localStorage_privatestorageevent.js new file mode 100644 index 0000000000..7c81fadf2d --- /dev/null +++ b/dom/tests/browser/browser_localStorage_privatestorageevent.js @@ -0,0 +1,87 @@ +add_task(async function () { + var privWin = OpenBrowserWindow({ private: true }); + await new privWin.Promise(resolve => { + privWin.addEventListener( + "load", + function () { + resolve(); + }, + { once: true } + ); + }); + + var pubWin = OpenBrowserWindow({ private: false }); + await new pubWin.Promise(resolve => { + pubWin.addEventListener( + "load", + function () { + resolve(); + }, + { once: true } + ); + }); + + var URL = + "http://mochi.test:8888/browser/dom/tests/browser/page_privatestorageevent.html"; + + var privTab = BrowserTestUtils.addTab(privWin.gBrowser, URL); + await BrowserTestUtils.browserLoaded( + privWin.gBrowser.getBrowserForTab(privTab) + ); + var privBrowser = gBrowser.getBrowserForTab(privTab); + + var pubTab = BrowserTestUtils.addTab(pubWin.gBrowser, URL); + await BrowserTestUtils.browserLoaded( + pubWin.gBrowser.getBrowserForTab(pubTab) + ); + var pubBrowser = gBrowser.getBrowserForTab(pubTab); + + // Check if pubWin can see privWin's storage events + await SpecialPowers.spawn(pubBrowser, [], function (opts) { + content.window.gotStorageEvent = false; + content.window.addEventListener("storage", ev => { + content.window.gotStorageEvent = true; + }); + }); + + await SpecialPowers.spawn(privBrowser, [], function (opts) { + content.window.localStorage.key = "ablooabloo"; + }); + + let pubSaw = await SpecialPowers.spawn(pubBrowser, [], function (opts) { + return content.window.gotStorageEvent; + }); + + ok(!pubSaw, "pubWin shouldn't be able to see privWin's storage events"); + + await SpecialPowers.spawn(privBrowser, [], function (opts) { + content.window.gotStorageEvent = false; + content.window.addEventListener("storage", ev => { + content.window.gotStorageEvent = true; + }); + }); + + // Check if privWin can see pubWin's storage events + await SpecialPowers.spawn(privBrowser, [], function (opts) { + content.window.gotStorageEvent = false; + content.window.addEventListener("storage", ev => { + content.window.gotStorageEvent = true; + }); + }); + + await SpecialPowers.spawn(pubBrowser, [], function (opts) { + content.window.localStorage.key = "ablooabloo"; + }); + + let privSaw = await SpecialPowers.spawn(privBrowser, [], function (opts) { + return content.window.gotStorageEvent; + }); + + ok(!privSaw, "privWin shouldn't be able to see pubWin's storage events"); + + BrowserTestUtils.removeTab(privTab); + await BrowserTestUtils.closeWindow(privWin); + + BrowserTestUtils.removeTab(pubTab); + await BrowserTestUtils.closeWindow(pubWin); +}); diff --git a/dom/tests/browser/browser_localStorage_snapshotting.js b/dom/tests/browser/browser_localStorage_snapshotting.js new file mode 100644 index 0000000000..f03fa187b1 --- /dev/null +++ b/dom/tests/browser/browser_localStorage_snapshotting.js @@ -0,0 +1,774 @@ +const HELPER_PAGE_URL = + "http://example.com/browser/dom/tests/browser/page_localstorage_snapshotting.html"; +const HELPER_PAGE_ORIGIN = "http://example.com/"; + +/* import-globals-from helper_localStorage.js */ + +let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/")); +Services.scriptloader.loadSubScript(testDir + "/helper_localStorage.js", this); + +function clearOrigin() { + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin( + HELPER_PAGE_ORIGIN + ); + let request = Services.qms.clearStoragesForPrincipal( + principal, + "default", + "ls" + ); + let promise = new Promise(resolve => { + request.callback = () => { + resolve(); + }; + }); + return promise; +} + +async function applyMutations(knownTab, mutations) { + await SpecialPowers.spawn( + knownTab.tab.linkedBrowser, + [mutations], + function (mutations) { + return content.wrappedJSObject.applyMutations( + Cu.cloneInto(mutations, content) + ); + } + ); +} + +async function verifyState(knownTab, expectedState) { + let actualState = await SpecialPowers.spawn( + knownTab.tab.linkedBrowser, + [], + function () { + return content.wrappedJSObject.getState(); + } + ); + + for (let [expectedKey, expectedValue] of Object.entries(expectedState)) { + ok(actualState.hasOwnProperty(expectedKey), "key present: " + expectedKey); + is(actualState[expectedKey], expectedValue, "value correct"); + } + for (let actualKey of Object.keys(actualState)) { + if (!expectedState.hasOwnProperty(actualKey)) { + ok(false, "actual state has key it shouldn't have: " + actualKey); + } + } +} + +async function getKeys(knownTab) { + let keys = await SpecialPowers.spawn( + knownTab.tab.linkedBrowser, + [], + function () { + return content.wrappedJSObject.getKeys(); + } + ); + return keys; +} + +async function beginExplicitSnapshot(knownTab) { + await SpecialPowers.spawn(knownTab.tab.linkedBrowser, [], function () { + return content.wrappedJSObject.beginExplicitSnapshot(); + }); +} + +async function checkpointExplicitSnapshot(knownTab) { + await SpecialPowers.spawn(knownTab.tab.linkedBrowser, [], function () { + return content.wrappedJSObject.checkpointExplicitSnapshot(); + }); +} + +async function endExplicitSnapshot(knownTab) { + await SpecialPowers.spawn(knownTab.tab.linkedBrowser, [], function () { + return content.wrappedJSObject.endExplicitSnapshot(); + }); +} + +async function verifyHasSnapshot(knownTab, expectedHasSnapshot) { + let hasSnapshot = await SpecialPowers.spawn( + knownTab.tab.linkedBrowser, + [], + function () { + return content.wrappedJSObject.getHasSnapshot(); + } + ); + is(hasSnapshot, expectedHasSnapshot, "Correct has snapshot"); +} + +async function verifySnapshotUsage(knownTab, expectedSnapshotUsage) { + let snapshotUsage = await SpecialPowers.spawn( + knownTab.tab.linkedBrowser, + [], + function () { + return content.wrappedJSObject.getSnapshotUsage(); + } + ); + is(snapshotUsage, expectedSnapshotUsage, "Correct snapshot usage"); +} + +async function verifyParentState(expectedState) { + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin( + HELPER_PAGE_ORIGIN + ); + + let actualState = await Services.domStorageManager.getState(principal); + + for (let [expectedKey, expectedValue] of Object.entries(expectedState)) { + ok(actualState.hasOwnProperty(expectedKey), "key present: " + expectedKey); + is(actualState[expectedKey], expectedValue, "value correct"); + } + for (let actualKey of Object.keys(actualState)) { + if (!expectedState.hasOwnProperty(actualKey)) { + ok(false, "actual state has key it shouldn't have: " + actualKey); + } + } +} + +// We spin up a ton of child processes. +requestLongerTimeout(4); + +/** + * Verify snapshotting of our localStorage implementation in multi-e10s setup. + */ +add_task(async function () { + if (!Services.domStorageManager.nextGenLocalStorageEnabled) { + ok(true, "Test ignored when the next gen local storage is not enabled."); + return; + } + + await SpecialPowers.pushPrefEnv({ + set: [ + // Enable LocalStorage's testing API so we can explicitly create + // snapshots when needed. + ["dom.storage.testing", true], + // Force multiple web and webIsolated content processes so that the + // multi-e10s logic works correctly. + ["dom.ipc.processCount", 8], + ["dom.ipc.processCount.webIsolated", 4], + ], + }); + + // Ensure that there is no localstorage data by forcing the origin to be + // cleared prior to the start of our test.. + await clearOrigin(); + + // - Open tabs. Don't configure any of them yet. + const knownTabs = new KnownTabs(); + const writerTab1 = await openTestTab( + HELPER_PAGE_URL, + "writer1", + knownTabs, + true + ); + const writerTab2 = await openTestTab( + HELPER_PAGE_URL, + "writer2", + knownTabs, + true + ); + const readerTab1 = await openTestTab( + HELPER_PAGE_URL, + "reader1", + knownTabs, + true + ); + const readerTab2 = await openTestTab( + HELPER_PAGE_URL, + "reader2", + knownTabs, + true + ); + + const initialMutations = [ + [null, null], + ["key1", "initial1"], + ["key2", "initial2"], + ["key3", "initial3"], + ["key5", "initial5"], + ["key6", "initial6"], + ["key7", "initial7"], + ["key8", "initial8"], + ]; + + const initialState = { + key1: "initial1", + key2: "initial2", + key3: "initial3", + key5: "initial5", + key6: "initial6", + key7: "initial7", + key8: "initial8", + }; + + let sizeOfOneKey; + let sizeOfOneValue; + let sizeOfOneItem; + let sizeOfKeys = 0; + let sizeOfItems = 0; + + let entries = Object.entries(initialState); + for (let i = 0; i < entries.length; i++) { + let entry = entries[i]; + let sizeOfKey = entry[0].length; + let sizeOfValue = entry[1].length; + let sizeOfItem = sizeOfKey + sizeOfValue; + if (i == 0) { + sizeOfOneKey = sizeOfKey; + sizeOfOneValue = sizeOfValue; + sizeOfOneItem = sizeOfItem; + } + sizeOfKeys += sizeOfKey; + sizeOfItems += sizeOfItem; + } + + info("Size of one key is " + sizeOfOneKey); + info("Size of one value is " + sizeOfOneValue); + info("Size of one item is " + sizeOfOneItem); + info("Size of keys is " + sizeOfKeys); + info("Size of items is " + sizeOfItems); + + const prefillValues = [ + // Zero prefill (prefill disabled) + 0, + // Less than one key length prefill + sizeOfOneKey - 1, + // Greater than one key length and less than one item length prefill + sizeOfOneKey + 1, + // Precisely one item length prefill + sizeOfOneItem, + // Precisely two times one item length prefill + 2 * sizeOfOneItem, + // Precisely size of keys prefill + sizeOfKeys, + // Less than size of keys plus one value length prefill + sizeOfKeys + sizeOfOneValue - 1, + // Precisely size of keys plus one value length prefill + sizeOfKeys + sizeOfOneValue, + // Greater than size of keys plus one value length and less than size of + // keys plus two times one value length prefill + sizeOfKeys + sizeOfOneValue + 1, + // Precisely size of keys plus two times one value length prefill + sizeOfKeys + 2 * sizeOfOneValue, + // Precisely size of keys plus three times one value length prefill + sizeOfKeys + 3 * sizeOfOneValue, + // Precisely size of keys plus four times one value length prefill + sizeOfKeys + 4 * sizeOfOneValue, + // Precisely size of keys plus five times one value length prefill + sizeOfKeys + 5 * sizeOfOneValue, + // Precisely size of keys plus six times one value length prefill + sizeOfKeys + 6 * sizeOfOneValue, + // Precisely size of items prefill + sizeOfItems, + // Unlimited prefill + -1, + ]; + + for (let prefillValue of prefillValues) { + info("Setting prefill value to " + prefillValue); + + await SpecialPowers.pushPrefEnv({ + set: [["dom.storage.snapshot_prefill", prefillValue]], + }); + + const gradualPrefillValues = [ + // Zero gradual prefill + 0, + // Less than one key length gradual prefill + sizeOfOneKey - 1, + // Greater than one key length and less than one item length gradual + // prefill + sizeOfOneKey + 1, + // Precisely one item length gradual prefill + sizeOfOneItem, + // Precisely two times one item length gradual prefill + 2 * sizeOfOneItem, + // Precisely three times one item length gradual prefill + 3 * sizeOfOneItem, + // Precisely four times one item length gradual prefill + 4 * sizeOfOneItem, + // Precisely five times one item length gradual prefill + 5 * sizeOfOneItem, + // Precisely six times one item length gradual prefill + 6 * sizeOfOneItem, + // Precisely size of items prefill + sizeOfItems, + // Unlimited gradual prefill + -1, + ]; + + for (let gradualPrefillValue of gradualPrefillValues) { + info("Setting gradual prefill value to " + gradualPrefillValue); + + await SpecialPowers.pushPrefEnv({ + set: [["dom.storage.snapshot_gradual_prefill", gradualPrefillValue]], + }); + + info("Stage 1"); + + const setRemoveMutations1 = [ + ["key0", "setRemove10"], + ["key1", "setRemove11"], + ["key2", null], + ["key3", "setRemove13"], + ["key4", "setRemove14"], + ["key5", "setRemove15"], + ["key6", "setRemove16"], + ["key7", "setRemove17"], + ["key8", null], + ["key9", "setRemove19"], + ]; + + const setRemoveState1 = { + key0: "setRemove10", + key1: "setRemove11", + key3: "setRemove13", + key4: "setRemove14", + key5: "setRemove15", + key6: "setRemove16", + key7: "setRemove17", + key9: "setRemove19", + }; + + const setRemoveMutations2 = [ + ["key0", "setRemove20"], + ["key1", null], + ["key2", "setRemove22"], + ["key3", "setRemove23"], + ["key4", "setRemove24"], + ["key5", "setRemove25"], + ["key6", "setRemove26"], + ["key7", null], + ["key8", "setRemove28"], + ["key9", "setRemove29"], + ]; + + const setRemoveState2 = { + key0: "setRemove20", + key2: "setRemove22", + key3: "setRemove23", + key4: "setRemove24", + key5: "setRemove25", + key6: "setRemove26", + key8: "setRemove28", + key9: "setRemove29", + }; + + // Apply initial mutations using an explicit snapshot. The explicit + // snapshot here ensures that the parent process have received the + // changes. + await beginExplicitSnapshot(writerTab1); + await applyMutations(writerTab1, initialMutations); + await endExplicitSnapshot(writerTab1); + + // Begin explicit snapshots in all tabs except readerTab2. All these tabs + // should see the initial state regardless what other tabs are doing. + await beginExplicitSnapshot(writerTab1); + await beginExplicitSnapshot(writerTab2); + await beginExplicitSnapshot(readerTab1); + + // Apply first array of set/remove mutations in writerTab1 and end the + // explicit snapshot. This will trigger saving of values in other active + // snapshots. + await applyMutations(writerTab1, setRemoveMutations1); + await endExplicitSnapshot(writerTab1); + + // Begin an explicit snapshot in readerTab2. writerTab1 already ended its + // explicit snapshot, so readerTab2 should see mutations done by + // writerTab1. + await beginExplicitSnapshot(readerTab2); + + // Apply second array of set/remove mutations in writerTab2 and end the + // explicit snapshot. This will trigger saving of values in other active + // snapshots, but only if they haven't been saved already. + await applyMutations(writerTab2, setRemoveMutations2); + await endExplicitSnapshot(writerTab2); + + // Verify state in readerTab1, it should match the initial state. + await verifyState(readerTab1, initialState); + await endExplicitSnapshot(readerTab1); + + // Verify state in readerTab2, it should match the state after the first + // array of set/remove mutatations have been applied and "commited". + await verifyState(readerTab2, setRemoveState1); + await endExplicitSnapshot(readerTab2); + + // Verify final state, it should match the state after the second array of + // set/remove mutation have been applied and "commited". An explicit + // snapshot is used. + await beginExplicitSnapshot(readerTab1); + await verifyState(readerTab1, setRemoveState2); + await endExplicitSnapshot(readerTab1); + + info("Stage 2"); + + const setRemoveClearMutations1 = [ + ["key0", "setRemoveClear10"], + ["key1", null], + [null, null], + ]; + + const setRemoveClearState1 = {}; + + const setRemoveClearMutations2 = [ + ["key8", null], + ["key9", "setRemoveClear29"], + [null, null], + ]; + + const setRemoveClearState2 = {}; + + // This is very similar to previous stage except that in addition to + // set/remove, the clear operation is involved too. + await beginExplicitSnapshot(writerTab1); + await applyMutations(writerTab1, initialMutations); + await endExplicitSnapshot(writerTab1); + + await beginExplicitSnapshot(writerTab1); + await beginExplicitSnapshot(writerTab2); + await beginExplicitSnapshot(readerTab1); + + await applyMutations(writerTab1, setRemoveClearMutations1); + await endExplicitSnapshot(writerTab1); + + await beginExplicitSnapshot(readerTab2); + + await applyMutations(writerTab2, setRemoveClearMutations2); + await endExplicitSnapshot(writerTab2); + + await verifyState(readerTab1, initialState); + await endExplicitSnapshot(readerTab1); + + await verifyState(readerTab2, setRemoveClearState1); + await endExplicitSnapshot(readerTab2); + + await beginExplicitSnapshot(readerTab1); + await verifyState(readerTab1, setRemoveClearState2); + await endExplicitSnapshot(readerTab1); + + info("Stage 3"); + + const changeOrderMutations = [ + ["key1", null], + ["key2", null], + ["key3", null], + ["key5", null], + ["key6", null], + ["key7", null], + ["key8", null], + ["key8", "initial8"], + ["key7", "initial7"], + ["key6", "initial6"], + ["key5", "initial5"], + ["key3", "initial3"], + ["key2", "initial2"], + ["key1", "initial1"], + ]; + + // Apply initial mutations using an explicit snapshot. The explicit + // snapshot here ensures that the parent process have received the + // changes. + await beginExplicitSnapshot(writerTab1); + await applyMutations(writerTab1, initialMutations); + await endExplicitSnapshot(writerTab1); + + // Begin explicit snapshots in all tabs except writerTab2 which is not + // used in this stage. All these tabs should see the initial order + // regardless what other tabs are doing. + await beginExplicitSnapshot(readerTab1); + await beginExplicitSnapshot(writerTab1); + await beginExplicitSnapshot(readerTab2); + + // Get all keys in readerTab1 and end the explicit snapshot. No mutations + // have been applied yet. + let tab1Keys = await getKeys(readerTab1); + await endExplicitSnapshot(readerTab1); + + // Apply mutations that change the order of keys and end the explicit + // snapshot. The state is unchanged. This will trigger saving of key order + // in other active snapshots, but only if the order hasn't been saved + // already. + await applyMutations(writerTab1, changeOrderMutations); + await endExplicitSnapshot(writerTab1); + + // Get all keys in readerTab2 and end the explicit snapshot. Change order + // mutations have been applied, but the order should stay unchanged. + let tab2Keys = await getKeys(readerTab2); + await endExplicitSnapshot(readerTab2); + + // Verify the key order is the same. + is(tab2Keys.length, tab1Keys.length, "Correct keys length"); + for (let i = 0; i < tab2Keys.length; i++) { + is(tab2Keys[i], tab1Keys[i], "Correct key"); + } + + // Verify final state, it should match the initial state since applied + // mutations only changed the key order. An explicit snapshot is used. + await beginExplicitSnapshot(readerTab1); + await verifyState(readerTab1, initialState); + await endExplicitSnapshot(readerTab1); + } + } + + // - Clean up. + await cleanupTabs(knownTabs); + + clearOrigin(); +}); + +/** + * Verify that snapshots are able to work with negative usage. This can happen + * when there's an item stored in localStorage with given size and then two + * snaphots (created at the same time) mutate the item. The first one replases + * it with something bigger and the other one removes it. + */ +add_task(async function () { + if (!Services.domStorageManager.nextGenLocalStorageEnabled) { + ok(true, "Test ignored when the next gen local storage is not enabled."); + return; + } + + await SpecialPowers.pushPrefEnv({ + set: [ + // Force multiple web and webIsolated content processes so that the + // multi-e10s logic works correctly. + ["dom.ipc.processCount", 4], + ["dom.ipc.processCount.webIsolated", 2], + // Disable snapshot peak usage pre-incrementation to make the testing + // easier. + ["dom.storage.snapshot_peak_usage.initial_preincrement", 0], + ["dom.storage.snapshot_peak_usage.reduced_initial_preincrement", 0], + ["dom.storage.snapshot_peak_usage.gradual_preincrement", 0], + ["dom.storage.snapshot_peak_usage.reuced_gradual_preincrement", 0], + // Enable LocalStorage's testing API so we can explicitly create + // snapshots when needed. + ["dom.storage.testing", true], + ], + }); + + // Ensure that there is no localstorage data by forcing the origin to be + // cleared prior to the start of our test. + await clearOriginStorageEnsuringNoPreload(HELPER_PAGE_ORIGIN); + + // Open tabs. Don't configure any of them yet. + const knownTabs = new KnownTabs(); + const writerTab1 = await openTestTab( + HELPER_PAGE_URL, + "writer1", + knownTabs, + true + ); + const writerTab2 = await openTestTab( + HELPER_PAGE_URL, + "writer2", + knownTabs, + true + ); + + // Apply the initial mutation using an explicit snapshot. The explicit + // snapshot here ensures that the parent process have received the changes. + await beginExplicitSnapshot(writerTab1); + await applyMutations(writerTab1, [["key", "something"]]); + await endExplicitSnapshot(writerTab1); + + // Begin explicit snapshots in both tabs. Both tabs should see the initial + // state. + await beginExplicitSnapshot(writerTab1); + await beginExplicitSnapshot(writerTab2); + + // Apply the first mutation in writerTab1 and end the explicit snapshot. + await applyMutations(writerTab1, [["key", "somethingBigger"]]); + await endExplicitSnapshot(writerTab1); + + // Apply the second mutation in writerTab2 and end the explicit snapshot. + await applyMutations(writerTab2, [["key", null]]); + await endExplicitSnapshot(writerTab2); + + // Verify the final state, it should match the state after the second + // mutation has been applied and "commited". An explicit snapshot is used. + await beginExplicitSnapshot(writerTab1); + await verifyState(writerTab1, {}); + await endExplicitSnapshot(writerTab1); + + // Clean up. + await cleanupTabs(knownTabs); + + clearOriginStorageEnsuringNoPreload(HELPER_PAGE_ORIGIN); +}); + +/** + * Verify that snapshot usage is correctly updated after each operation. + */ +add_task(async function () { + if (!Services.domStorageManager.nextGenLocalStorageEnabled) { + ok(true, "Test ignored when the next gen local storage is not enabled."); + return; + } + + await SpecialPowers.pushPrefEnv({ + set: [ + // Force multiple web and webIsolated content processes so that the + // multi-e10s logic works correctly. + ["dom.ipc.processCount", 4], + ["dom.ipc.processCount.webIsolated", 2], + // Disable snapshot peak usage pre-incrementation to make the testing + // easier. + ["dom.storage.snapshot_peak_usage.initial_preincrement", 0], + ["dom.storage.snapshot_peak_usage.reduced_initial_preincrement", 0], + ["dom.storage.snapshot_peak_usage.gradual_preincrement", 0], + ["dom.storage.snapshot_peak_usage.reuced_gradual_preincrement", 0], + // Enable LocalStorage's testing API so we can explicitly create + // snapshots when needed. + ["dom.storage.testing", true], + ], + }); + + // Ensure that there is no localstorage data by forcing the origin to be + // cleared prior to the start of our test. + await clearOriginStorageEnsuringNoPreload(HELPER_PAGE_ORIGIN); + + // Open tabs. Don't configure any of them yet. + const knownTabs = new KnownTabs(); + const writerTab1 = await openTestTab( + HELPER_PAGE_URL, + "writer1", + knownTabs, + true + ); + const writerTab2 = await openTestTab( + HELPER_PAGE_URL, + "writer2", + knownTabs, + true + ); + + // Apply the initial mutation using an explicit snapshot. The explicit + // snapshot here ensures that the parent process have received the changes. + await beginExplicitSnapshot(writerTab1); + await verifySnapshotUsage(writerTab1, 0); + await applyMutations(writerTab1, [["key", "something"]]); + await verifySnapshotUsage(writerTab1, 12); + await endExplicitSnapshot(writerTab1); + await verifyHasSnapshot(writerTab1, false); + + // Begin an explicit snapshot in writerTab1 and apply the first mutatation + // in it. + await beginExplicitSnapshot(writerTab1); + await verifySnapshotUsage(writerTab1, 12); + await applyMutations(writerTab1, [["key", "somethingBigger"]]); + await verifySnapshotUsage(writerTab1, 18); + + // Begin an explicit snapshot in writerTab2 and apply the second mutatation + // in it. + await beginExplicitSnapshot(writerTab2); + await verifySnapshotUsage(writerTab2, 18); + await applyMutations(writerTab2, [[null, null]]); + await verifySnapshotUsage(writerTab2, 6); + + // End explicit snapshots in both tabs. + await endExplicitSnapshot(writerTab1); + await verifyHasSnapshot(writerTab1, false); + await endExplicitSnapshot(writerTab2); + await verifyHasSnapshot(writerTab2, false); + + // Verify the final state, it should match the state after the second + // mutation has been applied and "commited". An explicit snapshot is used. + await beginExplicitSnapshot(writerTab1); + await verifySnapshotUsage(writerTab1, 0); + await verifyState(writerTab1, {}); + await endExplicitSnapshot(writerTab1); + await verifyHasSnapshot(writerTab1, false); + + // Clean up. + await cleanupTabs(knownTabs); + + clearOriginStorageEnsuringNoPreload(HELPER_PAGE_ORIGIN); +}); + +/** + * Verify that datastore in the parent is correctly updated after a checkpoint. + */ +add_task(async function () { + if (!Services.domStorageManager.nextGenLocalStorageEnabled) { + ok(true, "Test ignored when the next gen local storage is not enabled."); + return; + } + + await SpecialPowers.pushPrefEnv({ + set: [ + // Force multiple web and webIsolated content processes so that the + // multi-e10s logic works correctly. + ["dom.ipc.processCount", 4], + ["dom.ipc.processCount.webIsolated", 2], + // Enable LocalStorage's testing API so we can explicitly create + // snapshots when needed. + ["dom.storage.testing", true], + ], + }); + + // Ensure that there is no localstorage data by forcing the origin to be + // cleared prior to the start of our test. + await clearOriginStorageEnsuringNoPreload(HELPER_PAGE_ORIGIN); + + // Open tabs. Don't configure any of them yet. + const knownTabs = new KnownTabs(); + const writerTab1 = await openTestTab( + HELPER_PAGE_URL, + "writer1", + knownTabs, + true + ); + + await verifyParentState({}); + + // Apply the initial mutation using an explicit snapshot. The explicit + // snapshot here ensures that the parent process have received the changes. + await beginExplicitSnapshot(writerTab1); + await verifyParentState({}); + await applyMutations(writerTab1, [["key", "something"]]); + await verifyParentState({}); + await endExplicitSnapshot(writerTab1); + + await verifyParentState({ key: "something" }); + + // Begin an explicit snapshot in writerTab1, apply the first mutation in + // writerTab1 and checkpoint the explicit snapshot. + await beginExplicitSnapshot(writerTab1); + await verifyParentState({ key: "something" }); + await applyMutations(writerTab1, [["key", "somethingBigger"]]); + await verifyParentState({ key: "something" }); + await checkpointExplicitSnapshot(writerTab1); + + await verifyParentState({ key: "somethingBigger" }); + + // Apply the second mutation in writerTab1 and checkpoint the explicit + // snapshot. + await applyMutations(writerTab1, [["key", null]]); + await verifyParentState({ key: "somethingBigger" }); + await checkpointExplicitSnapshot(writerTab1); + + await verifyParentState({}); + + // Apply the third mutation in writerTab1 and end the explicit snapshot. + await applyMutations(writerTab1, [["otherKey", "something"]]); + await verifyParentState({}); + await endExplicitSnapshot(writerTab1); + + await verifyParentState({ otherKey: "something" }); + + // Verify the final state, it should match the state after the third mutation + // has been applied and "commited". An explicit snapshot is used. + await beginExplicitSnapshot(writerTab1); + await verifyParentState({ otherKey: "something" }); + await verifyState(writerTab1, { otherKey: "something" }); + await endExplicitSnapshot(writerTab1); + + await verifyParentState({ otherKey: "something" }); + + // Clean up. + await cleanupTabs(knownTabs); + + clearOriginStorageEnsuringNoPreload(HELPER_PAGE_ORIGIN); +}); diff --git a/dom/tests/browser/browser_navigate_replace_browsingcontext.js b/dom/tests/browser/browser_navigate_replace_browsingcontext.js new file mode 100644 index 0000000000..e8e0f98658 --- /dev/null +++ b/dom/tests/browser/browser_navigate_replace_browsingcontext.js @@ -0,0 +1,23 @@ +add_task(async function parent_to_remote() { + await BrowserTestUtils.withNewTab("about:mozilla", async browser => { + let originalBC = browser.browsingContext; + + BrowserTestUtils.loadURIString(browser, "https://example.com/"); + await BrowserTestUtils.browserLoaded(browser); + let newBC = browser.browsingContext; + + isnot(originalBC.id, newBC.id, "Should have replaced the BrowsingContext"); + }); +}); + +add_task(async function remote_to_parent() { + await BrowserTestUtils.withNewTab("https://example.com/", async browser => { + let originalBC = browser.browsingContext; + + BrowserTestUtils.loadURIString(browser, "about:mozilla"); + await BrowserTestUtils.browserLoaded(browser); + let newBC = browser.browsingContext; + + isnot(originalBC.id, newBC.id, "Should have replaced the BrowsingContext"); + }); +}); diff --git a/dom/tests/browser/browser_noopener.js b/dom/tests/browser/browser_noopener.js new file mode 100644 index 0000000000..d514ff2047 --- /dev/null +++ b/dom/tests/browser/browser_noopener.js @@ -0,0 +1,182 @@ +"use strict"; + +const TESTS = [ + { id: "#test1", name: "", opener: true, newWindow: false }, + { id: "#test2", name: "", opener: false, newWindow: false }, + { id: "#test3", name: "", opener: false, newWindow: false }, + + { id: "#test4", name: "uniquename1", opener: true, newWindow: false }, + { id: "#test5", name: "uniquename2", opener: false, newWindow: false }, + { id: "#test6", name: "uniquename3", opener: false, newWindow: false }, + + { id: "#test7", name: "", opener: true, newWindow: false }, + { id: "#test8", name: "", opener: false, newWindow: false }, + { id: "#test9", name: "", opener: false, newWindow: false }, + + { id: "#test10", name: "uniquename1", opener: true, newWindow: false }, + { id: "#test11", name: "uniquename2", opener: false, newWindow: false }, + { id: "#test12", name: "uniquename3", opener: false, newWindow: false }, +]; + +const TEST_URL = + "http://mochi.test:8888/browser/dom/tests/browser/test_noopener_source.html"; +const TARGET_URL = + "http://mochi.test:8888/browser/dom/tests/browser/test_noopener_target.html"; + +const OPEN_NEWWINDOW_PREF = "browser.link.open_newwindow"; +const OPEN_NEWWINDOW = 2; +const OPEN_NEWTAB = 3; + +const NOOPENER_NEWPROC_PREF = "dom.noopener.newprocess.enabled"; + +async function doTests(usePrivate, container) { + let alwaysNewWindow = + SpecialPowers.getIntPref(OPEN_NEWWINDOW_PREF) == OPEN_NEWWINDOW; + + let window = await BrowserTestUtils.openNewBrowserWindow({ + private: usePrivate, + }); + + let tabOpenOptions = {}; + if (container) { + tabOpenOptions.userContextId = 1; + } + + for (let test of TESTS) { + const testid = `${test.id} (private=${usePrivate}, container=${container}, alwaysNewWindow=${alwaysNewWindow})`; + let originalTab = BrowserTestUtils.addTab( + window.gBrowser, + TEST_URL, + tabOpenOptions + ); + await BrowserTestUtils.browserLoaded(originalTab.linkedBrowser); + await BrowserTestUtils.switchTab(window.gBrowser, originalTab); + + let waitFor; + if (test.newWindow || alwaysNewWindow) { + waitFor = BrowserTestUtils.waitForNewWindow({ url: TARGET_URL }); + // Confirm that this window has private browsing set if we're doing a private browsing test + } else { + waitFor = BrowserTestUtils.waitForNewTab( + window.gBrowser, + TARGET_URL, + true + ); + } + + BrowserTestUtils.synthesizeMouseAtCenter( + test.id, + {}, + window.gBrowser.getBrowserForTab(originalTab) + ); + + let tab; + if (test.newWindow || alwaysNewWindow) { + let window = await waitFor; + is( + PrivateBrowsingUtils.isWindowPrivate(window), + usePrivate, + "Private status should match for " + testid + ); + tab = window.gBrowser.selectedTab; + } else { + tab = await waitFor; + } + + // Check that the name matches. + await SpecialPowers.spawn( + tab.linkedBrowser, + [test, container, testid], + async (test, container, testid) => { + Assert.equal( + content.document.nodePrincipal.originAttributes.userContextId, + container ? 1 : 0, + `User context ID should match for ${testid}` + ); + + Assert.equal( + content.window.name, + test.name, + `Name should match for ${testid}` + ); + if (test.opener) { + Assert.ok( + content.window.opener, + `Opener should have been set for ${testid}` + ); + } else { + Assert.ok( + !content.window.opener, + `Opener should not have been set for ${testid}` + ); + } + } + ); + + BrowserTestUtils.removeTab(tab); + BrowserTestUtils.removeTab(originalTab); + } + + window.close(); +} + +async function doAllTests() { + // Non-private window + await doTests(false, false); + + // Private window + await doTests(true, false); + + // Non-private window with container + await doTests(false, true); +} + +// This test takes a really long time, especially in debug builds, as it is +// constant starting and stopping processes, and opens a new window ~144 times. +requestLongerTimeout(30); + +add_task(async function prepare() { + await SpecialPowers.pushPrefEnv({ + set: [["dom.window.open.noreferrer.enabled", true]], + }); +}); + +add_task(async function newtab_sameproc() { + await SpecialPowers.pushPrefEnv({ + set: [ + [OPEN_NEWWINDOW_PREF, OPEN_NEWTAB], + [NOOPENER_NEWPROC_PREF, false], + ], + }); + await doAllTests(); +}); + +add_task(async function newtab_newproc() { + await SpecialPowers.pushPrefEnv({ + set: [ + [OPEN_NEWWINDOW_PREF, OPEN_NEWTAB], + [NOOPENER_NEWPROC_PREF, true], + ], + }); + await doAllTests(); +}); + +add_task(async function newwindow_sameproc() { + await SpecialPowers.pushPrefEnv({ + set: [ + [OPEN_NEWWINDOW_PREF, OPEN_NEWWINDOW], + [NOOPENER_NEWPROC_PREF, false], + ], + }); + await doAllTests(); +}); + +add_task(async function newwindow_newproc() { + await SpecialPowers.pushPrefEnv({ + set: [ + [OPEN_NEWWINDOW_PREF, OPEN_NEWWINDOW], + [NOOPENER_NEWPROC_PREF, true], + ], + }); + await doAllTests(); +}); diff --git a/dom/tests/browser/browser_noopener_null_uri.js b/dom/tests/browser/browser_noopener_null_uri.js new file mode 100644 index 0000000000..8bb706b2b2 --- /dev/null +++ b/dom/tests/browser/browser_noopener_null_uri.js @@ -0,0 +1,15 @@ +add_task(async function browserNoopenerNullUri() { + await BrowserTestUtils.withNewTab({ gBrowser }, async function (aBrowser) { + let numTabs = gBrowser.tabs.length; + await SpecialPowers.spawn(aBrowser, [], async () => { + ok( + !content.window.open(undefined, undefined, "noopener"), + "window.open should return null" + ); + }); + await TestUtils.waitForCondition(() => gBrowser.tabs.length == numTabs + 1); + // We successfully opened a tab in content process! + }); + // We only have to close the tab we opened earlier + await BrowserTestUtils.removeTab(gBrowser.tabs[1]); +}); diff --git a/dom/tests/browser/browser_persist_cookies.js b/dom/tests/browser/browser_persist_cookies.js new file mode 100644 index 0000000000..d64c85eced --- /dev/null +++ b/dom/tests/browser/browser_persist_cookies.js @@ -0,0 +1,128 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.org" +); +const TEST_PATH2 = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +var MockFilePicker = SpecialPowers.MockFilePicker; +MockFilePicker.init(window); + +registerCleanupFunction(async function () { + info("Running the cleanup code"); + MockFilePicker.cleanup(); + Services.obs.removeObserver(checkRequest, "http-on-modify-request"); + SpecialPowers.clearUserPref("network.cookie.sameSite.laxByDefault"); + if (gTestDir && gTestDir.exists()) { + // On Windows, sometimes nsIFile.remove() throws, probably because we're + // still writing to the directory we're trying to remove, despite + // waiting for the download to complete. Just retry a bit later... + let succeeded = false; + while (!succeeded) { + try { + gTestDir.remove(true); + succeeded = true; + } catch (ex) { + await new Promise(requestAnimationFrame); + } + } + } +}); + +let gTestDir = null; + +function checkRequest(subject) { + let httpChannel = subject.QueryInterface(Ci.nsIHttpChannel); + let spec = httpChannel.URI.spec; + // Ignore initial requests for page that sets cookies and its favicon, which may not have + // cookies. + if ( + httpChannel.URI.host == "example.org" && + !spec.endsWith("favicon.ico") && + !spec.includes("redirect.sjs") + ) { + let cookie = httpChannel.getRequestHeader("cookie"); + is( + cookie.trim(), + "normalCookie=true", + "Should have correct cookie in request for " + spec + ); + } +} + +function createTemporarySaveDirectory() { + var saveDir = Services.dirsvc.get("TmpD", Ci.nsIFile); + saveDir.append("testsavedir"); + if (!saveDir.exists()) { + info("create testsavedir!"); + saveDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + } + info("return from createTempSaveDir: " + saveDir.path); + return saveDir; +} + +add_task(async function () { + // Use nsICookieService.BEHAVIOR_REJECT_TRACKER to avoid cookie partitioning. + // In this test case, if the cookie is partitioned, there will be no cookie + // nsICookieServicebeing sent to compare. + await SpecialPowers.pushPrefEnv({ + set: [ + ["network.cookie.cookieBehavior", 4], + // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default" + ["network.cookie.sameSite.laxByDefault", false], + ], + }); + + await BrowserTestUtils.withNewTab("about:blank", async function (browser) { + Services.obs.addObserver(checkRequest, "http-on-modify-request"); + BrowserTestUtils.loadURIString( + browser, + TEST_PATH + "set-samesite-cookies-and-redirect.sjs" + ); + // Test that the original document load doesn't send same-site cookies. + await BrowserTestUtils.browserLoaded( + browser, + true, + TEST_PATH2 + "set-samesite-cookies-and-redirect.sjs" + ); + // Now check the saved page. + // Create the folder the link will be saved into. + gTestDir = createTemporarySaveDirectory(); + let destFile = gTestDir.clone(); + + MockFilePicker.displayDirectory = gTestDir; + let fileName; + MockFilePicker.showCallback = function (fp) { + info("showCallback"); + fileName = fp.defaultString; + info("fileName: " + fileName); + destFile.append(fileName); + info("path: " + destFile.path); + MockFilePicker.setFiles([destFile]); + MockFilePicker.filterIndex = 0; // kSaveAsType_Complete + info("done showCallback"); + }; + saveBrowser(browser); + let dls = await Downloads.getList(Downloads.PUBLIC); + await new Promise((resolve, reject) => { + dls.addView({ + onDownloadChanged(download) { + if (download.succeeded) { + dls.removeView(this); + dls.removeFinished(); + resolve(); + } else if (download.error) { + reject("Download failed"); + } + }, + }); + }); + }); +}); diff --git a/dom/tests/browser/browser_persist_cross_origin_iframe.js b/dom/tests/browser/browser_persist_cross_origin_iframe.js new file mode 100644 index 0000000000..94a9a74af7 --- /dev/null +++ b/dom/tests/browser/browser_persist_cross_origin_iframe.js @@ -0,0 +1,198 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.org" +); +const TEST_PATH2 = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +var MockFilePicker = SpecialPowers.MockFilePicker; +MockFilePicker.init(window); + +registerCleanupFunction(async function () { + info("Running the cleanup code"); + MockFilePicker.cleanup(); + if (gTestDir && gTestDir.exists()) { + // On Windows, sometimes nsIFile.remove() throws, probably because we're + // still writing to the directory we're trying to remove, despite + // waiting for the download to complete. Just retry a bit later... + let succeeded = false; + while (!succeeded) { + try { + gTestDir.remove(true); + succeeded = true; + } catch (ex) { + await new Promise(requestAnimationFrame); + } + } + } +}); + +let gTestDir = null; + +function createTemporarySaveDirectory() { + var saveDir = Services.dirsvc.get("TmpD", Ci.nsIFile); + saveDir.append("testsavedir"); + if (!saveDir.exists()) { + saveDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + } + return saveDir; +} + +function canonicalizeExtension(str) { + return str.replace(/\.htm$/, ".html"); +} + +function checkContents(dir, expected, str) { + let stack = [dir]; + let files = []; + while (stack.length) { + for (let file of stack.pop().directoryEntries) { + if (file.isDirectory()) { + stack.push(file); + } + + let path = canonicalizeExtension(file.getRelativePath(dir)); + files.push(path); + } + } + + SimpleTest.isDeeply( + files.sort(), + expected.sort(), + str + "Should contain downloaded files in correct place." + ); +} + +async function addFrame(browser, path, selector) { + await SpecialPowers.spawn( + browser, + [path, selector], + async function (path, selector) { + let document = content.document; + let target = document.querySelector(selector); + if (content.HTMLIFrameElement.isInstance(target)) { + document = target.contentDocument; + target = document.body; + } + let element = document.createElement("iframe"); + element.src = path; + await new Promise(resolve => { + element.onload = resolve; + target.appendChild(element); + }); + } + ); +} + +async function handleResult(expected, str) { + let dls = await Downloads.getList(Downloads.PUBLIC); + return new Promise((resolve, reject) => { + dls.addView({ + onDownloadChanged(download) { + if (download.succeeded) { + checkContents(gTestDir, expected, str); + + dls.removeView(this); + dls.removeFinished(); + resolve(); + } else if (download.error) { + reject("Download failed"); + } + }, + }); + }); +} + +add_task(async function () { + await BrowserTestUtils.withNewTab( + TEST_PATH + "image.html", + async function (browser) { + await addFrame(browser, TEST_PATH + "image.html", "body"); + await addFrame(browser, TEST_PATH2 + "image.html", "body>iframe"); + + gTestDir = createTemporarySaveDirectory(); + + MockFilePicker.displayDirectory = gTestDir; + MockFilePicker.showCallback = function (fp) { + let destFile = gTestDir.clone(); + destFile.append("first.html"); + MockFilePicker.setFiles([destFile]); + MockFilePicker.filterIndex = 0; // kSaveAsType_Complete + }; + + let expected = [ + "first.html", + "first_files", + "first_files/image.html", + "first_files/dummy.png", + "first_files/image_data", + "first_files/image_data/image.html", + "first_files/image_data/image_data", + "first_files/image_data/image_data/dummy.png", + ]; + + // This saves the top-level document contained in `browser` + saveBrowser(browser); + await handleResult(expected, "Check toplevel: "); + + // Instead of deleting previously saved files, we update our list + // of expected files for the next part of the test. To not clash + // we make sure to save to a different file name. + expected = expected.concat([ + "second.html", + "second_files", + "second_files/dummy.png", + "second_files/image.html", + "second_files/image_data", + "second_files/image_data/dummy.png", + ]); + + MockFilePicker.showCallback = function (fp) { + let destFile = gTestDir.clone(); + destFile.append("second.html"); + MockFilePicker.setFiles([destFile]); + MockFilePicker.filterIndex = 0; // kSaveAsType_Complete + }; + + // This saves the sub-document of the iframe contained in the + // top-level document, as indicated by passing a child browsing + // context as target for the save. + saveBrowser(browser, false, browser.browsingContext.children[0]); + await handleResult(expected, "Check subframe: "); + + // Instead of deleting previously saved files, we update our list + // of expected files for the next part of the test. To not clash + // we make sure to save to a different file name. + expected = expected.concat([ + "third.html", + "third_files", + "third_files/dummy.png", + ]); + + MockFilePicker.showCallback = function (fp) { + let destFile = gTestDir.clone(); + destFile.append("third.html"); + MockFilePicker.setFiles([destFile]); + MockFilePicker.filterIndex = 0; // kSaveAsType_Complete + }; + + // This saves the sub-document of the iframe contained in the + // first sub-document, as indicated by passing a child browsing + // context as target for the save. That frame is special, because + // it's cross-process. + saveBrowser( + browser, + false, + browser.browsingContext.children[0].children[0] + ); + await handleResult(expected, "Check subframe: "); + } + ); +}); diff --git a/dom/tests/browser/browser_persist_image_accept.js b/dom/tests/browser/browser_persist_image_accept.js new file mode 100644 index 0000000000..ecf3b370a6 --- /dev/null +++ b/dom/tests/browser/browser_persist_image_accept.js @@ -0,0 +1,140 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.org" +); + +var MockFilePicker = SpecialPowers.MockFilePicker; +MockFilePicker.init(window); + +registerCleanupFunction(async function () { + info("Running the cleanup code"); + MockFilePicker.cleanup(); + if (gTestDir && gTestDir.exists()) { + // On Windows, sometimes nsIFile.remove() throws, probably because we're + // still writing to the directory we're trying to remove, despite + // waiting for the download to complete. Just retry a bit later... + let succeeded = false; + while (!succeeded) { + try { + gTestDir.remove(true); + succeeded = true; + } catch (ex) { + await new Promise(requestAnimationFrame); + } + } + } +}); + +let gTestDir = null; + +function createTemporarySaveDirectory() { + var saveDir = Services.dirsvc.get("TmpD", Ci.nsIFile); + saveDir.append("testsavedir"); + if (!saveDir.exists()) { + info("create testsavedir!"); + saveDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + } + info("return from createTempSaveDir: " + saveDir.path); + return saveDir; +} + +function expectedImageAcceptHeader() { + if (Services.prefs.prefHasUserValue("image.http.accept")) { + return Services.prefs.getCharPref("image.http.accept"); + } + + return ( + (Services.prefs.getBoolPref("image.avif.enabled") ? "image/avif," : "") + + (Services.prefs.getBoolPref("image.jxl.enabled") ? "image/jxl," : "") + + (Services.prefs.getBoolPref("image.webp.enabled") ? "image/webp," : "") + + "*/*" + ); +} + +add_task(async function test_image_download() { + await BrowserTestUtils.withNewTab(TEST_PATH + "dummy.html", async browser => { + // Add the image, and wait for it to load. + await SpecialPowers.spawn(browser, [], async function () { + let loc = content.document.location.href; + let imgloc = new content.URL("dummy.png", loc); + let img = content.document.createElement("img"); + img.src = imgloc; + await new Promise(resolve => { + img.onload = resolve; + content.document.body.appendChild(img); + }); + }); + gTestDir = createTemporarySaveDirectory(); + + let destFile = gTestDir.clone(); + + MockFilePicker.displayDirectory = gTestDir; + let fileName; + MockFilePicker.showCallback = function (fp) { + info("showCallback"); + fileName = fp.defaultString; + info("fileName: " + fileName); + destFile.append(fileName); + info("path: " + destFile.path); + MockFilePicker.setFiles([destFile]); + MockFilePicker.filterIndex = 0; // just save the file + info("done showCallback"); + }; + let publicDownloads = await Downloads.getList(Downloads.PUBLIC); + let downloadFinishedPromise = new Promise(resolve => { + publicDownloads.addView({ + onDownloadChanged(download) { + info("Download changed!"); + if (download.succeeded || download.error) { + info("Download succeeded or errored"); + publicDownloads.removeView(this); + publicDownloads.removeFinished(); + resolve(download); + } + }, + }); + }); + let httpOnModifyPromise = TestUtils.topicObserved( + "http-on-modify-request", + (s, t, d) => { + let channel = s.QueryInterface(Ci.nsIChannel); + let uri = channel.URI && channel.URI.spec; + if (!uri.endsWith("dummy.png")) { + info("Ignoring request for " + uri); + return false; + } + ok(channel instanceof Ci.nsIHttpChannel, "Should be HTTP channel"); + channel.QueryInterface(Ci.nsIHttpChannel); + is( + channel.getRequestHeader("Accept"), + expectedImageAcceptHeader(), + "Header should be image header" + ); + return true; + } + ); + // open the context menu. + let popup = document.getElementById("contentAreaContextMenu"); + let popupShown = BrowserTestUtils.waitForEvent(popup, "popupshown"); + BrowserTestUtils.synthesizeMouseAtCenter( + "img", + { type: "contextmenu", button: 2 }, + browser + ); + await popupShown; + let popupHidden = BrowserTestUtils.waitForEvent(popup, "popuphidden"); + popup.activateItem(popup.querySelector("#context-saveimage")); + await popupHidden; + info("Context menu hidden, waiting for download to finish"); + let imageDownload = await downloadFinishedPromise; + ok(imageDownload.succeeded, "Image should have downloaded successfully"); + info("Waiting for http request to complete."); + // Ensure we got the http request: + await httpOnModifyPromise; + }); +}); diff --git a/dom/tests/browser/browser_persist_mixed_content_image.js b/dom/tests/browser/browser_persist_mixed_content_image.js new file mode 100644 index 0000000000..d84934376d --- /dev/null +++ b/dom/tests/browser/browser_persist_mixed_content_image.js @@ -0,0 +1,109 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.org" +); + +var MockFilePicker = SpecialPowers.MockFilePicker; +MockFilePicker.init(window); + +registerCleanupFunction(async function () { + info("Running the cleanup code"); + MockFilePicker.cleanup(); + if (gTestDir && gTestDir.exists()) { + // On Windows, sometimes nsIFile.remove() throws, probably because we're + // still writing to the directory we're trying to remove, despite + // waiting for the download to complete. Just retry a bit later... + let succeeded = false; + while (!succeeded) { + try { + gTestDir.remove(true); + succeeded = true; + } catch (ex) { + await new Promise(requestAnimationFrame); + } + } + } +}); + +let gTestDir = null; + +function createTemporarySaveDirectory() { + var saveDir = Services.dirsvc.get("TmpD", Ci.nsIFile); + saveDir.append("testsavedir"); + if (!saveDir.exists()) { + info("create testsavedir!"); + saveDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + } + info("return from createTempSaveDir: " + saveDir.path); + return saveDir; +} + +add_task(async function test_image_download() { + await BrowserTestUtils.withNewTab( + TEST_PATH + "test_mixed_content_image.html", + async browser => { + // Add the image, and wait for it to load. + await SpecialPowers.spawn(browser, [], async function () { + let loc = content.document.location.href; + let httpRoot = loc.replace("https", "http"); + let imgloc = new content.URL("dummy.png", httpRoot); + let img = content.document.createElement("img"); + img.src = imgloc; + await new Promise(resolve => { + img.onload = resolve; + content.document.body.appendChild(img); + }); + }); + gTestDir = createTemporarySaveDirectory(); + + let destFile = gTestDir.clone(); + + MockFilePicker.displayDirectory = gTestDir; + let fileName; + MockFilePicker.showCallback = function (fp) { + info("showCallback"); + fileName = fp.defaultString; + info("fileName: " + fileName); + destFile.append(fileName); + info("path: " + destFile.path); + MockFilePicker.setFiles([destFile]); + MockFilePicker.filterIndex = 0; // just save the file + info("done showCallback"); + }; + let publicDownloads = await Downloads.getList(Downloads.PUBLIC); + let downloadFinishedPromise = new Promise(resolve => { + publicDownloads.addView({ + onDownloadChanged(download) { + info("Download changed!"); + if (download.succeeded || download.error) { + info("Download succeeded or errored"); + publicDownloads.removeView(this); + publicDownloads.removeFinished(); + resolve(download); + } + }, + }); + }); + // open the context menu. + let popup = document.getElementById("contentAreaContextMenu"); + let popupShown = BrowserTestUtils.waitForEvent(popup, "popupshown"); + BrowserTestUtils.synthesizeMouseAtCenter( + "img", + { type: "contextmenu", button: 2 }, + browser + ); + await popupShown; + let popupHidden = BrowserTestUtils.waitForEvent(popup, "popuphidden"); + popup.activateItem(popup.querySelector("#context-saveimage")); + await popupHidden; + info("Context menu hidden, waiting for download to finish"); + let imageDownload = await downloadFinishedPromise; + ok(imageDownload.succeeded, "Image should have downloaded successfully"); + } + ); +}); diff --git a/dom/tests/browser/browser_pointerlock_warning.js b/dom/tests/browser/browser_pointerlock_warning.js new file mode 100644 index 0000000000..f16778cf1f --- /dev/null +++ b/dom/tests/browser/browser_pointerlock_warning.js @@ -0,0 +1,129 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +const BODY_URL = + "<body onpointerdown='this.requestPointerLock()' style='width: 100px; height: 100px;'></body>"; + +const TEST_URL = "data:text/html," + BODY_URL; + +const FRAME_TEST_URL = + 'data:text/html,<body><iframe src="http://example.org/document-builder.sjs?html=' + + encodeURI(BODY_URL) + + '"></iframe></body>'; + +function checkWarningState(aWarningElement, aExpectedState, aMsg) { + ["hidden", "ontop", "onscreen"].forEach(state => { + is( + aWarningElement.hasAttribute(state), + state == aExpectedState, + `${aMsg} - check ${state} attribute.` + ); + }); +} + +async function waitForWarningState(aWarningElement, aExpectedState) { + await BrowserTestUtils.waitForAttribute(aExpectedState, aWarningElement, ""); + checkWarningState( + aWarningElement, + aExpectedState, + `Wait for ${aExpectedState} state` + ); +} + +// Make sure the pointerlock warning is shown and exited with the escape key +add_task(async function show_pointerlock_warning_escape() { + let urls = [TEST_URL, FRAME_TEST_URL]; + for (let url of urls) { + info("Pointerlock warning test for " + url); + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); + + let warning = document.getElementById("pointerlock-warning"); + let warningShownPromise = waitForWarningState(warning, "onscreen"); + + let expectedWarningText; + + let bc = tab.linkedBrowser.browsingContext; + if (bc.children.length) { + // use the subframe if it exists + bc = bc.children[0]; + expectedWarningText = "example.org"; + } else { + expectedWarningText = "This document"; + } + expectedWarningText += + " has control of your pointer. Press Esc to take back control."; + + await BrowserTestUtils.synthesizeMouse("body", 4, 4, {}, bc); + + await warningShownPromise; + + ok(true, "Pointerlock warning shown"); + + let warningHiddenPromise = waitForWarningState(warning, "hidden"); + + await BrowserTestUtils.waitForCondition( + () => warning.innerText == expectedWarningText, + "Warning text" + ); + + EventUtils.synthesizeKey("KEY_Escape"); + await warningHiddenPromise; + + ok(true, "Pointerlock warning hidden"); + + // Pointerlock should be released after escape is pressed. + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + Assert.equal(content.document.pointerLockElement, null); + }); + + await BrowserTestUtils.removeTab(tab); + } +}); + +/* +// XXX Bug 1580961 - this part of the test is disabled. +// +// Make sure the pointerlock warning is shown, but this time escape is not pressed until after the +// notification is closed via the timeout. +add_task(async function show_pointerlock_warning_timeout() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + let warning = document.getElementById("pointerlock-warning"); + let warningShownPromise = BrowserTestUtils.waitForAttribute( + "onscreen", + warning, + "true" + ); + let warningHiddenPromise = BrowserTestUtils.waitForAttribute( + "hidden", + warning, + "true" + ); + await BrowserTestUtils.synthesizeMouse("body", 4, 4, {}, tab.linkedBrowser); + + await warningShownPromise; + ok(true, "Pointerlock warning shown"); + await warningHiddenPromise; + + // The warning closes after a few seconds, but this does not exit pointerlock mode. + await SpecialPowers.spawn(tab.linkedBrowser, [], async function() { + Assert.equal(content.document.pointerLockElement, content.document.body); + }); + + EventUtils.synthesizeKey("KEY_Escape"); + + ok(true, "Pointerlock warning hidden"); + + // Pointerlock should now be released. + await SpecialPowers.spawn(tab.linkedBrowser, [], async function() { + Assert.equal(content.document.pointerLockElement, null); + }); + + await BrowserTestUtils.removeTab(tab); +}); +*/ diff --git a/dom/tests/browser/browser_sessionStorage_navigation.js b/dom/tests/browser/browser_sessionStorage_navigation.js new file mode 100644 index 0000000000..094527dd44 --- /dev/null +++ b/dom/tests/browser/browser_sessionStorage_navigation.js @@ -0,0 +1,271 @@ +"use strict"; + +const DIRPATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + "" +); +const PATH = DIRPATH + "file_empty.html"; + +const ORIGIN1 = "https://example.com"; +const ORIGIN2 = "https://example.org"; +const URL1 = `${ORIGIN1}/${PATH}`; +const URL2 = `${ORIGIN2}/${PATH}`; +const URL1_WITH_COOP_COEP = `${ORIGIN1}/${DIRPATH}file_coop_coep.html`; + +add_task(async function () { + await BrowserTestUtils.withNewTab(URL1, async function (browser) { + const key = "key"; + const value = "value"; + + info( + `Verifying sessionStorage is preserved after navigating to a ` + + `cross-origin site and then navigating back` + ); + + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "privacy.partition.always_partition_third_party_non_cookie_storage", + false, + ], + ], + }); + + BrowserTestUtils.loadURIString(browser, URL1); + await BrowserTestUtils.browserLoaded(browser); + + await SpecialPowers.spawn( + browser, + [ORIGIN1, key, value], + async (ORIGIN, key, value) => { + is(content.window.origin, ORIGIN, `Navigate to ${ORIGIN} as expected`); + + let value1 = content.window.sessionStorage.getItem(key); + is( + value1, + null, + `SessionStorage for ${key} in ${content.window.origin} is null ` + + `since it's the first visit` + ); + + content.window.sessionStorage.setItem(key, value); + + let value2 = content.window.sessionStorage.getItem(key); + is( + value2, + value, + `SessionStorage for ${key} in ${content.window.origin} is set ` + + `correctly` + ); + } + ); + + BrowserTestUtils.loadURIString(browser, URL2); + await BrowserTestUtils.browserLoaded(browser); + + await SpecialPowers.spawn( + browser, + [ORIGIN2, key, value], + async (ORIGIN, key, value) => { + is(content.window.origin, ORIGIN, `Navigate to ${ORIGIN} as expected`); + + let value1 = content.window.sessionStorage.getItem(key); + is( + value1, + null, + `SessionStorage for ${key} in ${content.window.origin} is null ` + + `since it's the first visit` + ); + } + ); + + BrowserTestUtils.loadURIString(browser, URL1); + await BrowserTestUtils.browserLoaded(browser); + + await SpecialPowers.spawn( + browser, + [ORIGIN1, key, value], + async (ORIGIN, key, value) => { + is(content.window.origin, ORIGIN, `Navigate to ${ORIGIN} as expected`); + + let value1 = content.window.sessionStorage.getItem(key); + is( + value1, + value, + `SessionStorage for ${key} in ${content.window.origin} is preserved` + ); + } + ); + + info(`Verifying sessionStorage is preserved for ${URL1} after navigating`); + + BrowserTestUtils.loadURIString(browser, URL2); + await BrowserTestUtils.browserLoaded(browser); + + await SpecialPowers.spawn( + browser, + [ORIGIN2, ORIGIN1, URL1, key, value], + async (ORIGIN, iframeORIGIN, iframeURL, key, value) => { + is(content.window.origin, ORIGIN, `Navigate to ${ORIGIN} as expected`); + + let iframe = content.document.createElement("iframe"); + iframe.src = iframeURL; + content.document.body.appendChild(iframe); + await ContentTaskUtils.waitForEvent(iframe, "load"); + + await content.SpecialPowers.spawn( + iframe, + [iframeORIGIN, key, value], + async function (ORIGIN, key, value) { + is( + content.window.origin, + ORIGIN, + `Navigate to ${ORIGIN} as expected` + ); + + // Bug 1746646: Make mochitests work with TCP enabled (cookieBehavior = 5) + // Acquire storage access permission here so that the iframe has + // first-party access to the sessionStorage. Without this, it is + // isolated and this test will always fail + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + await SpecialPowers.addPermission( + "storageAccessAPI", + true, + content.window.location.href + ); + await SpecialPowers.wrap(content.document).requestStorageAccess(); + + let value1 = content.window.sessionStorage.getItem(key); + is( + value1, + value, + `SessionStorage for ${key} in ${content.window.origin} is ` + + `preserved` + ); + } + ); + } + ); + + info(`Verifying SSCache is loaded to the content process only once`); + + BrowserTestUtils.loadURIString(browser, URL1); + await BrowserTestUtils.browserLoaded(browser); + + await SpecialPowers.spawn( + browser, + [ORIGIN1, URL1, key, value], + async (ORIGIN, iframeURL, key, value) => { + is(content.window.origin, ORIGIN, `Navigate to ${ORIGIN} as expected`); + + let iframe = content.document.createElement("iframe"); + iframe.src = iframeURL; + content.document.body.appendChild(iframe); + await ContentTaskUtils.waitForEvent(iframe, "load"); + + await content.SpecialPowers.spawn( + iframe, + [ORIGIN, key, value], + async function (ORIGIN, key, value) { + is( + content.window.origin, + ORIGIN, + `Load an iframe to ${ORIGIN} as expected` + ); + + let value1 = content.window.sessionStorage.getItem(key); + is( + value1, + value, + `SessionStorage for ${key} in ${content.window.origin} is ` + + `preserved.` + ); + + // When we are here, it means we didn't hit the assertion for + // ensuring a SSCache can only be loaded on the content process + // once. + } + ); + } + ); + + info( + `Verifying the sessionStorage for a tab shares between ` + + `cross-origin-isolated and non cross-origin-isolated environments` + ); + const anotherKey = `anotherKey`; + const anotherValue = `anotherValue;`; + + BrowserTestUtils.loadURIString(browser, URL1_WITH_COOP_COEP); + await BrowserTestUtils.browserLoaded(browser); + + await SpecialPowers.spawn( + browser, + [ORIGIN1, key, value, anotherKey, anotherValue], + async (ORIGIN, key, value, anotherKey, anotherValue) => { + is(content.window.origin, ORIGIN, `Navigate to ${ORIGIN} as expected`); + ok( + content.window.crossOriginIsolated, + `The window is cross-origin-isolated.` + ); + + let value1 = content.window.sessionStorage.getItem(key); + is( + value1, + value, + `SessionStorage for ${key} in ${content.window.origin} was ` + + `propagated to COOP+COEP process correctly.` + ); + + let value2 = content.window.sessionStorage.getItem(anotherKey); + is( + value2, + null, + `SessionStorage for ${anotherKey} in ${content.window.origin} ` + + `hasn't been set yet.` + ); + + content.window.sessionStorage.setItem(anotherKey, anotherValue); + + let value3 = content.window.sessionStorage.getItem(anotherKey); + is( + value3, + anotherValue, + `SessionStorage for ${anotherKey} in ${content.window.origin} ` + + `was set as expected.` + ); + } + ); + + BrowserTestUtils.loadURIString(browser, URL1); + await BrowserTestUtils.browserLoaded(browser); + + await SpecialPowers.spawn( + browser, + [ORIGIN1, key, value, anotherKey, anotherValue], + async (ORIGIN, key, value, anotherKey, anotherValue) => { + is(content.window.origin, ORIGIN, `Navigate to ${ORIGIN} as expected`); + ok( + !content.window.crossOriginIsolated, + `The window is not cross-origin-isolated.` + ); + + let value1 = content.window.sessionStorage.getItem(key); + is( + value1, + value, + `SessionStorage for ${key} in ${content.window.origin} is ` + + `preserved.` + ); + + let value2 = content.window.sessionStorage.getItem(anotherKey); + is( + value2, + anotherValue, + `SessionStorage for ${anotherKey} in ${content.window.origin} was ` + + `propagated to non-COOP+COEP process correctly.` + ); + } + ); + }); +}); diff --git a/dom/tests/browser/browser_test_focus_after_modal_state.js b/dom/tests/browser/browser_test_focus_after_modal_state.js new file mode 100644 index 0000000000..2193d8fdc4 --- /dev/null +++ b/dom/tests/browser/browser_test_focus_after_modal_state.js @@ -0,0 +1,71 @@ +const TEST_URL = + "https://example.com/browser/dom/tests/browser/focus_after_prompt.html"; + +const { PromptTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromptTestUtils.sys.mjs" +); + +function awaitAndClosePrompt(browser) { + return PromptTestUtils.handleNextPrompt( + browser, + { modalType: Services.prompt.MODAL_TYPE_CONTENT, promptType: "prompt" }, + { buttonNumClick: 0 } + ); +} + +add_task(async function () { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + let browser = tab.linkedBrowser; + + // Focus on editable iframe. + await BrowserTestUtils.synthesizeMouseAtCenter("#edit", {}, browser); + await SpecialPowers.spawn(browser, [], async function () { + is( + content.document.activeElement, + content.document.getElementById("edit"), + "Focus should be on iframe element" + ); + }); + + let focusBlurPromise = SpecialPowers.spawn(browser, [], async function () { + let focusOccurred = false; + let blurOccurred = false; + + return new Promise(resolve => { + let doc = content.document.getElementById("edit").contentDocument; + doc.addEventListener("focus", function (event) { + focusOccurred = true; + if (blurOccurred) { + resolve(true); + } + }); + + doc.addEventListener("blur", function (event) { + blurOccurred = true; + if (focusOccurred) { + resolve(false); + } + }); + }); + }); + + // Click on div that triggers a prompt, and then check that focus is back on + // the editable iframe. + let dialogShown = awaitAndClosePrompt(browser); + await SpecialPowers.spawn(browser, [], async function () { + let div = content.document.getElementById("clickMeDiv"); + div.click(); + }); + await dialogShown; + let blurCameFirst = await focusBlurPromise; + await SpecialPowers.spawn(browser, [], async function () { + is( + content.document.activeElement, + content.document.getElementById("edit"), + "Focus should be back on iframe element" + ); + }); + ok(blurCameFirst, "Should receive blur and then focus event"); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/dom/tests/browser/browser_test_new_window_from_content.js b/dom/tests/browser/browser_test_new_window_from_content.js new file mode 100644 index 0000000000..e150ea3232 --- /dev/null +++ b/dom/tests/browser/browser_test_new_window_from_content.js @@ -0,0 +1,221 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* + We have three ways for content to open new windows: + 1) window.open (with the default features) + 2) window.open (with non-default features) + 3) target="_blank" in <a> tags + + We also have two prefs that modify our window opening behaviours: + + 1) browser.link.open_newwindow + + This has a numeric value that allows us to set our window-opening behaviours from + content in three ways: + 1) Open links that would normally open a new window in the current tab + 2) Open links that would normally open a new window in a new window + 3) Open links that would normally open a new window in a new tab (default) + + 2) browser.link.open_newwindow.restriction + + This has a numeric value that allows us to fine tune the browser.link.open_newwindow + pref so that it can discriminate between different techniques for opening windows. + + 0) All things that open windows should behave according to browser.link.open_newwindow. + 1) No things that open windows should behave according to browser.link.open_newwindow + (essentially rendering browser.link.open_newwindow inert). + 2) Most things that open windows should behave according to browser.link.open_newwindow, + _except_ for window.open calls with the "feature" parameter. This will open in a new + window regardless of what browser.link.open_newwindow is set at. (default) + + This file attempts to test each window opening technique against all possible settings for + each preference. +*/ + +const kContentDoc = + "https://www.example.com/browser/dom/tests/browser/test_new_window_from_content_child.html"; +const kNewWindowPrefKey = "browser.link.open_newwindow"; +const kNewWindowRestrictionPrefKey = "browser.link.open_newwindow.restriction"; +const kSameTab = "same tab"; +const kNewWin = "new window"; +const kNewTab = "new tab"; + +SpecialPowers.pushPrefEnv({ + set: [["dom.require_user_interaction_for_beforeunload", false]], +}); + +requestLongerTimeout(3); + +// The following "matrices" represent the result of content attempting to +// open a window with window.open with the default feature set. The key of +// the kWinOpenDefault object represents the value of browser.link.open_newwindow. +// The value for each key is an array that represents the result (either opening +// the link in the same tab, a new window, or a new tab), where the index of each +// result maps to the browser.link.open_newwindow.restriction pref. I've tried +// to illustrate this more clearly in the kWinOpenDefault object. +const kWinOpenDefault = { + // open_newwindow.restriction + // 0 1 2 + // open_newwindow + 1: [kSameTab, kNewWin, kSameTab], + 2: [kNewWin, kNewWin, kNewWin], + 3: [kNewTab, kNewWin, kNewTab], +}; + +const kWinOpenNonDefault = { + 1: [kSameTab, kNewWin, kNewWin], + 2: [kNewWin, kNewWin, kNewWin], + 3: [kNewTab, kNewWin, kNewWin], +}; + +const kTargetBlank = { + 1: [kSameTab, kSameTab, kSameTab], + 2: [kNewWin, kNewWin, kNewWin], + 3: [kNewTab, kNewTab, kNewTab], +}; + +// We'll be changing these preferences a lot, so we'll stash their original +// values and make sure we restore them at the end of the test. +var originalNewWindowPref = Services.prefs.getIntPref(kNewWindowPrefKey); +var originalNewWindowRestrictionPref = Services.prefs.getIntPref( + kNewWindowRestrictionPrefKey +); + +registerCleanupFunction(function () { + Services.prefs.setIntPref(kNewWindowPrefKey, originalNewWindowPref); + Services.prefs.setIntPref( + kNewWindowRestrictionPrefKey, + originalNewWindowRestrictionPref + ); +}); + +/** + * For some expectation when a link is clicked, creates and + * returns a Promise that resolves when that expectation is + * fulfilled. For example, aExpectation might be kSameTab, which + * will cause this function to return a Promise that resolves when + * the current tab attempts to browse to about:blank. + * + * This function also takes care of cleaning up once the result has + * occurred - for example, if a new window was opened, this function + * closes it before resolving. + * + * @param aBrowser the <xul:browser> with the test document + * @param aExpectation one of kSameTab, kNewWin, or kNewTab. + * @return a Promise that resolves when the expectation is fulfilled, + * and cleaned up after. + */ +function prepareForResult(aBrowser, aExpectation) { + let expectedSpec = kContentDoc.replace(/[^\/]*$/, "dummy.html"); + switch (aExpectation) { + case kSameTab: + return (async function () { + await BrowserTestUtils.browserLoaded(aBrowser); + is(aBrowser.currentURI.spec, expectedSpec, "Should be at dummy.html"); + // Now put the browser back where it came from + BrowserTestUtils.loadURIString(aBrowser, kContentDoc); + await BrowserTestUtils.browserLoaded(aBrowser); + })(); + case kNewWin: + return (async function () { + let newWin = await BrowserTestUtils.waitForNewWindow({ + url: expectedSpec, + }); + let newBrowser = newWin.gBrowser.selectedBrowser; + is(newBrowser.currentURI.spec, expectedSpec, "Should be at dummy.html"); + await BrowserTestUtils.closeWindow(newWin); + })(); + case kNewTab: + return (async function () { + let newTab = await BrowserTestUtils.waitForNewTab(gBrowser); + is( + newTab.linkedBrowser.currentURI.spec, + expectedSpec, + "Should be at dummy.html" + ); + BrowserTestUtils.removeTab(newTab); + })(); + default: + ok( + false, + "prepareForResult can't handle an expectation of " + aExpectation + ); + return Promise.resolve(); + } +} + +/** + * Ensure that clicks on a link with ID aLinkID cause us to + * perform as specified in the supplied aMatrix (kWinOpenDefault, + * for example). + * + * @param aLinkSelector a selector for the link within the testing page to click. + * @param aMatrix a testing matrix for the + * browser.link.open_newwindow and browser.link.open_newwindow.restriction + * prefs to test against. See kWinOpenDefault for an example. + */ +function testLinkWithMatrix(aLinkSelector, aMatrix) { + return BrowserTestUtils.withNewTab( + { + gBrowser, + url: kContentDoc, + }, + async function (browser) { + // This nested for-loop is unravelling the matrix const + // we set up, and gives us three things through each tick + // of the inner loop: + // 1) newWindowPref: a browser.link.open_newwindow pref to try + // 2) newWindowRestPref: a browser.link.open_newwindow.restriction pref to try + // 3) expectation: what we expect the click outcome on this link to be, + // which will either be kSameTab, kNewWin or kNewTab. + for (let newWindowPref in aMatrix) { + let expectations = aMatrix[newWindowPref]; + for (let i = 0; i < expectations.length; ++i) { + let newWindowRestPref = i; + let expectation = expectations[i]; + + Services.prefs.setIntPref( + "browser.link.open_newwindow", + newWindowPref + ); + Services.prefs.setIntPref( + "browser.link.open_newwindow.restriction", + newWindowRestPref + ); + info("Clicking on " + aLinkSelector); + info( + "Testing with browser.link.open_newwindow = " + + newWindowPref + + " and " + + "browser.link.open_newwindow.restriction = " + + newWindowRestPref + ); + info("Expecting: " + expectation); + let resultPromise = prepareForResult(browser, expectation); + BrowserTestUtils.synthesizeMouseAtCenter(aLinkSelector, {}, browser); + await resultPromise; + info("Got expectation: " + expectation); + } + } + } + ); +} + +add_task(async function test_window_open_with_defaults() { + await testLinkWithMatrix("#winOpenDefault", kWinOpenDefault); +}); + +add_task(async function test_window_open_with_non_defaults() { + await testLinkWithMatrix("#winOpenNonDefault", kWinOpenNonDefault); +}); + +add_task(async function test_window_open_dialog() { + await testLinkWithMatrix("#winOpenDialog", kWinOpenNonDefault); +}); + +add_task(async function test_target__blank() { + await testLinkWithMatrix("#targetBlank", kTargetBlank); +}); diff --git a/dom/tests/browser/browser_test_toolbars_visibility.js b/dom/tests/browser/browser_test_toolbars_visibility.js new file mode 100644 index 0000000000..f313f1549f --- /dev/null +++ b/dom/tests/browser/browser_test_toolbars_visibility.js @@ -0,0 +1,323 @@ +// Tests that toolbars have proper visibility when opening a new window +// in either content or chrome context. + +const ROOT = "http://www.example.com/browser/dom/tests/browser/"; +const CONTENT_PAGE = ROOT + "test_new_window_from_content_child.html"; +const TARGET_PAGE = ROOT + "dummy.html"; + +/** + * This function retrieves the visibility state of the toolbars of a + * window within the content context. + * + * @param aBrowser (<xul:browser>) + * The browser to query for toolbar visibility states + * @returns Promise + * A promise that resolves when the toolbar state is retrieved + * within the content context, which value is an object that holds + * the visibility state of the toolbars + */ +function getToolbarsFromBrowserContent(aBrowser) { + return SpecialPowers.spawn(aBrowser, [], async function () { + // This is still chrome context. + // Inject a script that runs on content context, and gather the result. + + let script = content.document.createElement("script"); + script.textContent = ` +let bars = [ + "toolbar", + "menubar", + "personalbar", + "statusbar", + "scrollbars", + "locationbar", +]; + +for (let bar of bars) { + let node = document.createElement("span"); + node.id = bar; + node.textContent = window[bar].visible; + document.body.appendChild(node); +} +`; + content.document.body.appendChild(script); + + let result = {}; + + let bars = [ + "toolbar", + "menubar", + "personalbar", + "statusbar", + "scrollbars", + "locationbar", + ]; + + for (let bar of bars) { + let node = content.document.getElementById(bar); + let value = node.textContent; + if (value !== "true" && value !== "false") { + throw new Error("bar visibility isn't set"); + } + result[bar] = value === "true"; + node.remove(); + } + + return result; + }); +} + +/** + * This function retrieves the visibility state of the toolbars of a + * window within the chrome context. + * + * @param win + * the chrome privileged window + * @returns object + * an object that holds the visibility state of the toolbars + */ +function getToolbarsFromWindowChrome(win) { + return { + toolbar: win.toolbar.visible, + menubar: win.menubar.visible, + personalbar: win.personalbar.visible, + statusbar: win.statusbar.visible, + scrollbars: win.scrollbars.visible, + locationbar: win.locationbar.visible, + }; +} + +/** + * Tests toolbar visibility when opening a window with default parameters. + * + * @param toolbars + * the visibility state of the toolbar elements + */ +function testDefaultToolbars(toolbars) { + ok( + toolbars.locationbar, + "locationbar should be visible on default window.open()" + ); + ok(toolbars.menubar, "menubar be visible on default window.open()"); + ok( + toolbars.personalbar, + "personalbar should be visible on default window.open()" + ); + ok( + toolbars.statusbar, + "statusbar should be visible on default window.open()" + ); + ok( + toolbars.scrollbars, + "scrollbars should be visible on default window.open()" + ); + ok(toolbars.toolbar, "toolbar should be visible on default window.open()"); +} + +/** + * Tests toolbar visibility when opening a popup window on the content context, + * and reading the value from context context. + * + * @param toolbars + * the visibility state of the toolbar elements + */ +function testNonDefaultContentToolbarsFromContent(toolbars) { + // Accessing BarProp.visible from content context should return false for + // popup, regardless of the each feature value in window.open parameter. + ok(!toolbars.locationbar, "locationbar.visible should be false for popup"); + ok(!toolbars.menubar, "menubar.visible should be false for popup"); + ok(!toolbars.personalbar, "personalbar.visible should be false for popup"); + ok(!toolbars.statusbar, "statusbar.visible should be false for popup"); + ok(!toolbars.scrollbars, "scrollbars.visible should be false for popup"); + ok(!toolbars.toolbar, "toolbar.visible should be false for popup"); +} + +/** + * Tests toolbar visibility when opening a popup window on the content context, + * and reading the value from chrome context. + * + * @param toolbars + * the visibility state of the toolbar elements + */ +function testNonDefaultContentToolbarsFromChrome(toolbars) { + // Accessing BarProp.visible from chrome context should return the + // actual visibility. + + // Locationbar should always be visible on content context + ok( + toolbars.locationbar, + "locationbar should be visible even with location=no" + ); + ok(!toolbars.menubar, "menubar shouldn't be visible when menubar=no"); + ok( + !toolbars.personalbar, + "personalbar shouldn't be visible when personalbar=no" + ); + // statusbar will report visible=true even when it's hidden because of bug#55820 + todo(!toolbars.statusbar, "statusbar shouldn't be visible when status=no"); + ok( + toolbars.scrollbars, + "scrollbars should be visible even with scrollbars=no" + ); + ok(!toolbars.toolbar, "toolbar shouldn't be visible when toolbar=no"); +} + +/** + * Tests toolbar visibility when opening a window with non default parameters + * on the chrome context. + * + * @param toolbars + * the visibility state of the toolbar elements + */ +function testNonDefaultChromeToolbars(toolbars) { + // None of the toolbars should be visible if hidden with chrome privileges + ok( + !toolbars.locationbar, + "locationbar should not be visible with location=no" + ); + ok(!toolbars.menubar, "menubar should not be visible with menubar=no"); + ok( + !toolbars.personalbar, + "personalbar should not be visible with personalbar=no" + ); + ok(!toolbars.statusbar, "statusbar should not be visible with status=no"); + ok( + toolbars.scrollbars, + "scrollbars should be visible even with scrollbars=no" + ); + ok(!toolbars.toolbar, "toolbar should not be visible with toolbar=no"); +} + +/** + * Ensure that toolbars of a window opened in the content context have the + * correct visibility. + * + * A window opened with default parameters should have all toolbars visible. + * + * A window opened with "location=no, personalbar=no, toolbar=no, scrollbars=no, + * menubar=no, status=no", should only have location visible. + */ +add_task(async function () { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: CONTENT_PAGE, + }, + async function (browser) { + // First, call the default window.open() which will open a new tab + let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#winOpenDefault", + {}, + browser + ); + let tab = await newTabPromise; + + // Check that all toolbars are visible + let toolbars = await getToolbarsFromBrowserContent( + gBrowser.selectedBrowser + ); + testDefaultToolbars(toolbars); + + // Cleanup + BrowserTestUtils.removeTab(tab); + + // Now let's open a window with toolbars hidden + let winPromise = BrowserTestUtils.waitForNewWindow({ url: TARGET_PAGE }); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#winOpenNonDefault", + {}, + browser + ); + let popupWindow = await winPromise; + + let popupBrowser = popupWindow.gBrowser.selectedBrowser; + + // Test toolbars visibility value from content. + let popupToolbars = await getToolbarsFromBrowserContent(popupBrowser); + testNonDefaultContentToolbarsFromContent(popupToolbars); + + // Test toolbars visibility value from chrome. + let chromeToolbars = getToolbarsFromWindowChrome(popupWindow); + testNonDefaultContentToolbarsFromChrome(chromeToolbars); + + // Close the new window + await BrowserTestUtils.closeWindow(popupWindow); + } + ); +}); + +/** + * Ensure that toolbars of a window opened to about:blank in the content context + * have the correct visibility. + * + * A window opened with "location=no, personalbar=no, toolbar=no, scrollbars=no, + * menubar=no, status=no", should only have location visible. + */ +add_task(async function () { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: CONTENT_PAGE, + }, + async function (browser) { + // Open a blank window with toolbars hidden + let winPromise = BrowserTestUtils.waitForNewWindow(); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#winOpenNoURLNonDefault", + {}, + browser + ); + let popupWindow = await winPromise; + + // No need to wait for this window to load, since it's loading about:blank + let popupBrowser = popupWindow.gBrowser.selectedBrowser; + let popupToolbars = await getToolbarsFromBrowserContent(popupBrowser); + testNonDefaultContentToolbarsFromContent(popupToolbars); + + let chromeToolbars = getToolbarsFromWindowChrome(popupWindow); + testNonDefaultContentToolbarsFromChrome(chromeToolbars); + + // Close the new window + await BrowserTestUtils.closeWindow(popupWindow); + } + ); +}); + +/** + * Ensure that toolbars of a window opened in the chrome context have the + * correct visibility. + * + * A window opened with default parameters should have all toolbars visible. + * + * A window opened with "location=no, personalbar=no, toolbar=no, scrollbars=no, + * menubar=no, status=no", should not have any toolbar visible. + */ +add_task(async function () { + // First open a default window from this chrome context + let defaultWindowPromise = BrowserTestUtils.waitForNewWindow({ + url: TARGET_PAGE, + }); + window.open(TARGET_PAGE, "_blank", "noopener"); + let defaultWindow = await defaultWindowPromise; + + // Check that all toolbars are visible + let toolbars = getToolbarsFromWindowChrome(defaultWindow); + testDefaultToolbars(toolbars); + + // Now lets open a window with toolbars hidden from this chrome context + let features = + "location=no, personalbar=no, toolbar=no, scrollbars=no, menubar=no, status=no, noopener"; + let popupWindowPromise = BrowserTestUtils.waitForNewWindow({ + url: TARGET_PAGE, + }); + window.open(TARGET_PAGE, "_blank", features); + let popupWindow = await popupWindowPromise; + + // Test none of the tooolbars are visible + let hiddenToolbars = getToolbarsFromWindowChrome(popupWindow); + testNonDefaultChromeToolbars(hiddenToolbars); + + // Cleanup + await BrowserTestUtils.closeWindow(defaultWindow); + await BrowserTestUtils.closeWindow(popupWindow); +}); diff --git a/dom/tests/browser/browser_unlinkable_about_page_can_load_module_scripts.js b/dom/tests/browser/browser_unlinkable_about_page_can_load_module_scripts.js new file mode 100644 index 0000000000..ca4f9a7bc3 --- /dev/null +++ b/dom/tests/browser/browser_unlinkable_about_page_can_load_module_scripts.js @@ -0,0 +1,83 @@ +/* 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 kTestPage = getRootDirectory(gTestPath) + "file_load_module_script.html"; + +const kDefaultFlags = + Ci.nsIAboutModule.ALLOW_SCRIPT | + Ci.nsIAboutModule.URI_MUST_LOAD_IN_CHILD | + Ci.nsIAboutModule.URI_CAN_LOAD_IN_PRIVILEGEDABOUT_PROCESS | + Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT; + +const kAboutPagesRegistered = Promise.all([ + BrowserTestUtils.registerAboutPage( + registerCleanupFunction, + "test-linkable-page", + kTestPage, + kDefaultFlags | Ci.nsIAboutModule.MAKE_LINKABLE + ), + BrowserTestUtils.registerAboutPage( + registerCleanupFunction, + "test-unlinkable-page", + kTestPage, + kDefaultFlags + ), +]); + +add_task(async function () { + await kAboutPagesRegistered; + + let consoleListener = { + observe() { + errorCount++; + if (shouldHaveJSError) { + ok(true, "JS error is expected and received"); + } else { + ok(false, "Unexpected JS error was received"); + } + }, + }; + Services.console.registerListener(consoleListener); + registerCleanupFunction(() => + Services.console.unregisterListener(consoleListener) + ); + + let shouldHaveJSError = true; + let errorCount = 0; + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:test-linkable-page" }, + async browser => { + await SpecialPowers.spawn(browser, [], () => { + isnot( + content.document.body.textContent, + "scriptLoaded", + "The page content shouldn't be changed on the linkable page" + ); + }); + } + ); + ok( + errorCount > 0, + "Should have an error when loading test-linkable-page, got " + errorCount + ); + + shouldHaveJSError = false; + errorCount = 0; + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:test-unlinkable-page" }, + async browser => { + await SpecialPowers.spawn(browser, [], () => { + is( + content.document.body.textContent, + "scriptLoaded", + "The page content should be changed on the unlinkable page" + ); + }); + } + ); + is(errorCount, 0, "Should have no errors when loading test-unlinkable-page"); +}); diff --git a/dom/tests/browser/browser_wakelock.js b/dom/tests/browser/browser_wakelock.js new file mode 100644 index 0000000000..5cef231d07 --- /dev/null +++ b/dom/tests/browser/browser_wakelock.js @@ -0,0 +1,40 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +add_task(async () => { + info("creating test window"); + let win = await BrowserTestUtils.openNewBrowserWindow(); + + const pm = Cc["@mozilla.org/power/powermanagerservice;1"].getService( + Ci.nsIPowerManagerService + ); + + is( + pm.getWakeLockState("screen"), + "unlocked", + "Wakelock should be unlocked state" + ); + + info("aquiring wakelock"); + const wakelock = pm.newWakeLock("screen", win); + isnot( + pm.getWakeLockState("screen"), + "unlocked", + "Wakelock shouldn't be unlocked state" + ); + + info("releasing wakelock"); + wakelock.unlock(); + is( + pm.getWakeLockState("screen"), + "unlocked", + "Wakelock should be unlocked state" + ); + + info("closing test window"); + await BrowserTestUtils.closeWindow(win); +}); diff --git a/dom/tests/browser/browser_windowProxy_transplant.js b/dom/tests/browser/browser_windowProxy_transplant.js new file mode 100644 index 0000000000..9001437a2d --- /dev/null +++ b/dom/tests/browser/browser_windowProxy_transplant.js @@ -0,0 +1,215 @@ +"use strict"; + +const DIRPATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + "" +); +const PATH = DIRPATH + "file_postMessage_parent.html"; + +const URL1 = `http://mochi.test:8888/${PATH}`; +const URL2 = `http://example.com/${PATH}`; +const URL3 = `http://example.org/${PATH}`; + +// A bunch of boilerplate which needs to be dealt with. +add_task(async function () { + // Turn on BC preservation and frameloader rebuilding to ensure that the + // BrowsingContext is preserved. + await SpecialPowers.pushPrefEnv({ + set: [["fission.preserve_browsing_contexts", true]], + }); + + // Open a window with fission force-enabled in it. + let win = await BrowserTestUtils.openNewBrowserWindow({ + fission: true, + remote: true, + }); + try { + // Get the tab & browser to perform the test in. + let tab = win.gBrowser.selectedTab; + let browser = tab.linkedBrowser; + + // Start loading the original URI, then wait until it is loaded. + BrowserTestUtils.loadURIString(browser, URL1); + await BrowserTestUtils.browserLoaded(browser, false, URL1); + + info("Chrome script has loaded initial URI."); + await SpecialPowers.spawn( + browser, + [{ URL1, URL2, URL3 }], + async ({ URL1, URL2, URL3 }) => { + let iframe = content.document.createElement("iframe"); + content.document.body.appendChild(iframe); + + info("Chrome script created iframe"); + + // Here and below, we have to store references to things in the + // iframes on the content window, because all chrome references + // to content will be turned into dead wrappers when the iframes + // are closed. + content.win0 = iframe.contentWindow; + content.bc0 = iframe.browsingContext; + + ok( + !Cu.isDeadWrapper(content.win0), + "win0 shouldn't be a dead wrapper before navigation" + ); + + // Helper for waiting for a load. + function waitLoad() { + return new Promise(resolve => { + iframe.addEventListener( + "load", + event => { + info("Got an iframe load event!"); + resolve(); + }, + { once: true } + ); + }); + } + + function askLoad(url) { + info("Chrome script asking for load of " + url); + iframe.contentWindow.postMessage( + { + action: "navigate", + location: url, + }, + "*" + ); + info("Chrome script done calling PostMessage"); + } + + // Check that BC and WindowProxy are preserved across navigations. + iframe.contentWindow.location = URL1; + await waitLoad(); + + content.win1 = iframe.contentWindow; + let chromeWin1 = iframe.contentWindow; + let chromeWin1x = Cu.waiveXrays(iframe.contentWindow); + content.win1x = Cu.waiveXrays(iframe.contentWindow); + + ok(chromeWin1 != chromeWin1x, "waiving xrays creates a new thing?"); + + content.bc1 = iframe.browsingContext; + + is( + content.bc0, + content.bc1, + "same to same-origin BrowsingContext match" + ); + is(content.win0, content.win1, "same to same-origin WindowProxy match"); + + ok( + !Cu.isDeadWrapper(content.win1), + "win1 shouldn't be a dead wrapper before navigation" + ); + ok( + !Cu.isDeadWrapper(chromeWin1), + "chromeWin1 shouldn't be a dead wrapper before navigation" + ); + + askLoad(URL2); + await waitLoad(); + + content.win2 = iframe.contentWindow; + content.bc2 = iframe.browsingContext; + + // When chrome accesses a remote window proxy in content, the result + // should be a remote outer window proxy in the chrome compartment, not an + // Xray wrapper around the content remote window proxy. The former will + // throw a security error, because @@toPrimitive can't be called cross + // process, while the latter will result in an opaque wrapper, because + // XPConnect doesn't know what to do when trying to create an Xray wrapper + // around a remote outer window proxy. See bug 1556845. + Assert.throws( + () => { + dump("content.win1 " + content.win1 + "\n"); + }, + /SecurityError: Permission denied to access property Symbol.toPrimitive on cross-origin object/, + "Should get a remote outer window proxy when accessing old window proxy" + ); + Assert.throws( + () => { + dump("content.win2 " + content.win2 + "\n"); + }, + /SecurityError: Permission denied to access property Symbol.toPrimitive on cross-origin object/, + "Should get a remote outer window proxy when accessing new window proxy" + ); + + // If we fail to transplant existing non-remote outer window proxies, then + // after we navigate the iframe existing chrome references to the window will + // become dead wrappers. Also check content.win1 for thoroughness, though + // we don't nuke content-content references. + ok( + !Cu.isDeadWrapper(content.win1), + "win1 shouldn't be a dead wrapper after navigation" + ); + ok( + !Cu.isDeadWrapper(chromeWin1), + "chromeWin1 shouldn't be a dead wrapper after navigation" + ); + ok( + Cu.isDeadWrapper(chromeWin1x), + "chromeWin1x should be a dead wrapper after navigation" + ); + ok( + Cu.isDeadWrapper(content.win1x), + "content.win1x should be a dead wrapper after navigation" + ); + + is( + content.bc1, + content.bc2, + "same to cross-origin navigation BrowsingContext match" + ); + is( + content.win1, + content.win2, + "same to cross-origin navigation WindowProxy match" + ); + + ok( + !Cu.isDeadWrapper(content.win1), + "win1 shouldn't be a dead wrapper after navigation" + ); + + askLoad(URL3); + await waitLoad(); + + content.win3 = iframe.contentWindow; + content.bc3 = iframe.browsingContext; + + is( + content.bc2, + content.bc3, + "cross to cross-origin navigation BrowsingContext match" + ); + is( + content.win2, + content.win3, + "cross to cross-origin navigation WindowProxy match" + ); + + askLoad(URL1); + await waitLoad(); + + content.win4 = iframe.contentWindow; + content.bc4 = iframe.browsingContext; + + is( + content.bc3, + content.bc4, + "cross to same-origin navigation BrowsingContext match" + ); + is( + content.win3, + content.win4, + "cross to same-origin navigation WindowProxy match" + ); + } + ); + } finally { + await BrowserTestUtils.closeWindow(win); + } +}); diff --git a/dom/tests/browser/browser_xhr_sandbox.js b/dom/tests/browser/browser_xhr_sandbox.js new file mode 100644 index 0000000000..cca69b916b --- /dev/null +++ b/dom/tests/browser/browser_xhr_sandbox.js @@ -0,0 +1,62 @@ +// This code is evaluated in a sandbox courtesy of toSource(); +var sandboxCode = + function () { + let req = new XMLHttpRequest(); + req.open("GET", "http://mochi.test:8888/browser/dom/tests/browser/", true); + req.onreadystatechange = function () { + if (req.readyState === 4) { + // If we get past the problem above, we end up with a req.status of zero + // (ie, blocked due to CORS) even though we are fetching from the same + // origin as the window itself. + let result; + if (req.status != 200) { + result = "ERROR: got request status of " + req.status; + } else if (!req.responseText.length) { + result = "ERROR: got zero byte response text"; + } else { + result = "ok"; + } + postMessage({ result }, "*"); + } + }; + req.send(null); + }.toSource() + "();"; + +add_task(async function test() { + await SpecialPowers.pushPrefEnv({ + set: [["security.allow_unsafe_parent_loads", true]], + }); + + let newWin = await BrowserTestUtils.openNewBrowserWindow(); + + let frame = newWin.document.createXULElement("iframe"); + frame.setAttribute("type", "content"); + frame.setAttribute( + "src", + "http://mochi.test:8888/browser/dom/tests/browser/browser_xhr_sandbox.js" + ); + + newWin.document.documentElement.appendChild(frame); + await BrowserTestUtils.waitForEvent(frame, "load", true); + + let contentWindow = frame.contentWindow; + let sandbox = new Cu.Sandbox(contentWindow); + + // inject some functions from the window into the sandbox. + // postMessage so the async code in the sandbox can report a result. + sandbox.importFunction( + contentWindow.postMessage.bind(contentWindow), + "postMessage" + ); + sandbox.importFunction(contentWindow.XMLHttpRequest, "XMLHttpRequest"); + Cu.evalInSandbox(sandboxCode, sandbox, "1.8"); + + let sandboxReply = await BrowserTestUtils.waitForEvent( + contentWindow, + "message", + true + ); + is(sandboxReply.data.result, "ok", "check the sandbox code was felipe"); + + await BrowserTestUtils.closeWindow(newWin); +}); diff --git a/dom/tests/browser/create_webrtc_peer_connection.html b/dom/tests/browser/create_webrtc_peer_connection.html new file mode 100644 index 0000000000..ee993d4892 --- /dev/null +++ b/dom/tests/browser/create_webrtc_peer_connection.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +<meta charset="utf-8"> +<script> +const gConnections = []; + +window.addEventListener("message", event => { + const ackTarget = window.parent ? window.parent : window; + switch (event.data) { + case "push-peer-connection": + gConnections.push(new RTCPeerConnection()); + ackTarget.postMessage("ack", "*"); + break; + case "pop-peer-connection": + gConnections.pop().close(); + ackTarget.postMessage("ack", "*"); + break; + } +}); + +window.addEventListener("DOMContentLoaded", function(ev) { + document.getElementById("msg").innerText = location.host; +}); +</script> +</head> +<body><div id="msg"></div></body> +</html> diff --git a/dom/tests/browser/dummy.html b/dom/tests/browser/dummy.html new file mode 100644 index 0000000000..6ec72c2160 --- /dev/null +++ b/dom/tests/browser/dummy.html @@ -0,0 +1,13 @@ +<!doctype html> +<html> +<head> +<title>Dummy test page</title> +<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta> +</head> +<body> +<p>Dummy test page</p> +<script> + localStorage.setItem("foo", "bar"); +</script> +</body> +</html> diff --git a/dom/tests/browser/dummy.png b/dom/tests/browser/dummy.png Binary files differnew file mode 100644 index 0000000000..a1089af09b --- /dev/null +++ b/dom/tests/browser/dummy.png diff --git a/dom/tests/browser/file_bug1685807.html b/dom/tests/browser/file_bug1685807.html new file mode 100644 index 0000000000..9e35fa1730 --- /dev/null +++ b/dom/tests/browser/file_bug1685807.html @@ -0,0 +1,12 @@ +<html> +<head> +</head> +<body> +<script> + window.onload = () => { + opener.location.href = "about:blank"; + window.close(); + }; +</script> +</body> +</html> diff --git a/dom/tests/browser/file_coop_coep.html b/dom/tests/browser/file_coop_coep.html new file mode 100644 index 0000000000..f77ef9d5f2 --- /dev/null +++ b/dom/tests/browser/file_coop_coep.html @@ -0,0 +1,6 @@ +<html> + <body> + <div id="host1"></div> + <div id="host3"></div> + </body> +</html> diff --git a/dom/tests/browser/file_coop_coep.html^headers^ b/dom/tests/browser/file_coop_coep.html^headers^ new file mode 100644 index 0000000000..63b60e490f --- /dev/null +++ b/dom/tests/browser/file_coop_coep.html^headers^ @@ -0,0 +1,2 @@ +Cross-Origin-Opener-Policy: same-origin +Cross-Origin-Embedder-Policy: require-corp diff --git a/dom/tests/browser/file_empty.html b/dom/tests/browser/file_empty.html new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/dom/tests/browser/file_empty.html diff --git a/dom/tests/browser/file_empty_cross_site_frame.html b/dom/tests/browser/file_empty_cross_site_frame.html new file mode 100644 index 0000000000..fb134786f6 --- /dev/null +++ b/dom/tests/browser/file_empty_cross_site_frame.html @@ -0,0 +1,2 @@ +<!doctype html> +<iframe src="http://example.com/browser/dom/tests/browser/file_empty.html"></iframe> diff --git a/dom/tests/browser/file_load_module_script.html b/dom/tests/browser/file_load_module_script.html new file mode 100644 index 0000000000..004c727bb4 --- /dev/null +++ b/dom/tests/browser/file_load_module_script.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<html> +<head> + <meta http-equiv="Content-Security-Policy" content="default-src chrome:; object-src 'none'"/> + <script type="module" src="chrome://mochitests/content/browser/dom/tests/browser/file_module_loaded.js"></script> +</head> +<body></body> +</html> diff --git a/dom/tests/browser/file_module_loaded.js b/dom/tests/browser/file_module_loaded.js new file mode 100644 index 0000000000..8763889e1f --- /dev/null +++ b/dom/tests/browser/file_module_loaded.js @@ -0,0 +1,6 @@ +"use strict"; + +import setBodyText from "chrome://mochitests/content/browser/dom/tests/browser/file_module_loaded2.js"; +document.addEventListener("DOMContentLoaded", () => { + setBodyText(); +}); diff --git a/dom/tests/browser/file_module_loaded2.js b/dom/tests/browser/file_module_loaded2.js new file mode 100644 index 0000000000..28b26cd16e --- /dev/null +++ b/dom/tests/browser/file_module_loaded2.js @@ -0,0 +1,3 @@ +export default function setBodyText() { + document.body.textContent = "scriptLoaded"; +} diff --git a/dom/tests/browser/file_postMessage_parent.html b/dom/tests/browser/file_postMessage_parent.html new file mode 100644 index 0000000000..f9aa63a8c7 --- /dev/null +++ b/dom/tests/browser/file_postMessage_parent.html @@ -0,0 +1,48 @@ +<!doctype html> +<script> + dump("Content running top level script " + window.location.href + "\n"); + + var winID = SpecialPowers.wrap(this).windowGlobalChild.innerWindowId; + + var observer = { + observe(subject, topic) { + var currID = SpecialPowers.wrap(subject).QueryInterface(SpecialPowers.Ci.nsISupportsPRUint64).data; + if (currID != winID) { + return; + } + // We should be able to wrap the inner window when the outer + // window has navigated out of process. + SpecialPowers.Cu.getGlobalForObject({}); + + SpecialPowers.removeObserver(observer, "inner-window-nuked"); + } + }; + SpecialPowers.addObserver(observer, "inner-window-nuked"); + + // Unfortunately, we don't currently fire the onload event on a remote iframe, + // so we can't listen for the load event directly on the iframe. Instead, we + // postMessage from the iframe when the load event would be fired. + window.addEventListener("load", function onload() { + dump("Content got load of " + window.location.href + "\n"); + if (window.parent) { + window.parent.postMessage({ + event: "load", + location: window.location.href, + }, "*"); + } + + let h1 = document.createElement("h1"); + h1.textContent = window.location.href; + document.body.appendChild(h1); + }, { once: true }); + + // In addition, we listen to the message event to trigger navigations of + // ourself when requested, as we don't fully support our embedder triggering + // us being navigated yet for Totally Not Buggy Reasons. + window.addEventListener("message", function onmessage(event) { + dump("Content got event " + window.location.href + " " + JSON.stringify(event.data) + "\n"); + if (event.data.action === "navigate") { + window.location = event.data.location; + } + }); +</script> diff --git a/dom/tests/browser/focus_after_prompt.html b/dom/tests/browser/focus_after_prompt.html new file mode 100644 index 0000000000..db60d19189 --- /dev/null +++ b/dom/tests/browser/focus_after_prompt.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Cursor should not be lost after prompt</title> + <script type="application/javascript"> + function init() { + document.getElementById("edit").contentWindow.document.designMode = "on"; + } + </script> +</head> + +<body onload="init()"> + <div id="clickMeDiv" onclick="prompt('This is a dummy prompt!');" + onmousedown="return false;">Click me!</div> + <iframe id="edit"></iframe> +</body> +</html> diff --git a/dom/tests/browser/geo_leak_test.html b/dom/tests/browser/geo_leak_test.html new file mode 100644 index 0000000000..fb3fabac40 --- /dev/null +++ b/dom/tests/browser/geo_leak_test.html @@ -0,0 +1,17 @@ +<html> +<head> +<meta http-equiv="Content-Type" content="text/html;charset=utf-8" /> +<title>Geolocation incomplete position leak test</title> +<script type="text/javascript"> + +function successCallback(position) {} +function errorCallback() {} + +function init() { + navigator.geolocation.getCurrentPosition(successCallback, errorCallback); +} +</script> +</head> +<body onload="init()"> +</body> +</html> diff --git a/dom/tests/browser/helper_localStorage.js b/dom/tests/browser/helper_localStorage.js new file mode 100644 index 0000000000..386d2d8b4e --- /dev/null +++ b/dom/tests/browser/helper_localStorage.js @@ -0,0 +1,302 @@ +/* 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/. */ + +// Simple tab wrapper abstracting our messaging mechanism; +class KnownTab { + constructor(name, tab) { + this.name = name; + this.tab = tab; + } + + cleanup() { + this.tab = null; + } +} + +// Simple data structure class to help us track opened tabs and their pids. +class KnownTabs { + constructor() { + this.byPid = new Map(); + this.byName = new Map(); + } + + cleanup() { + for (let key of this.byPid.keys()) { + this.byPid[key] = null; + } + this.byPid = null; + this.byName = null; + } +} + +/** + * Open our helper page in a tab in its own content process, asserting that it + * really is in its own process. We initially load and wait for about:blank to + * load, and only then loadURI to our actual page. This is to ensure that + * LocalStorageManager has had an opportunity to be created and populate + * mOriginsHavingData. + * + * (nsGlobalWindow will reliably create LocalStorageManager as a side-effect of + * the unconditional call to nsGlobalWindow::PreloadLocalStorage. This will + * reliably create the StorageDBChild instance, and its corresponding + * StorageDBParent will send the set of origins when it is constructed.) + */ +async function openTestTab( + helperPageUrl, + name, + knownTabs, + shouldLoadInNewProcess +) { + let realUrl = helperPageUrl + "?" + encodeURIComponent(name); + // Load and wait for about:blank. + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: "about:blank", + forceNewProcess: true, + }); + ok(!knownTabs.byName.has(name), "tab needs its own name: " + name); + + let knownTab = new KnownTab(name, tab); + knownTabs.byName.set(name, knownTab); + + // Now trigger the actual load of our page. + BrowserTestUtils.loadURIString(tab.linkedBrowser, realUrl); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + let pid = tab.linkedBrowser.frameLoader.remoteTab.osPid; + if (shouldLoadInNewProcess) { + ok( + !knownTabs.byPid.has(pid), + "tab should be loaded in new process, pid: " + pid + ); + } else { + ok( + knownTabs.byPid.has(pid), + "tab should be loaded in the same process, new pid: " + pid + ); + } + + if (knownTabs.byPid.has(pid)) { + knownTabs.byPid.get(pid).set(name, knownTab); + } else { + let pidMap = new Map(); + pidMap.set(name, knownTab); + knownTabs.byPid.set(pid, pidMap); + } + + return knownTab; +} + +/** + * Close all the tabs we opened. + */ +async function cleanupTabs(knownTabs) { + for (let knownTab of knownTabs.byName.values()) { + BrowserTestUtils.removeTab(knownTab.tab); + knownTab.cleanup(); + } + knownTabs.cleanup(); +} + +/** + * Wait for a LocalStorage flush to occur. This notification can occur as a + * result of any of: + * - The normal, hardcoded 5-second flush timer. + * - InsertDBOp seeing a preload op for an origin with outstanding changes. + * - Us generating a "domstorage-test-flush-force" observer notification. + */ +function waitForLocalStorageFlush() { + if (Services.domStorageManager.nextGenLocalStorageEnabled) { + return new Promise(resolve => executeSoon(resolve)); + } + + return new Promise(function (resolve) { + let observer = { + observe() { + SpecialPowers.removeObserver(observer, "domstorage-test-flushed"); + resolve(); + }, + }; + SpecialPowers.addObserver(observer, "domstorage-test-flushed"); + }); +} + +/** + * Trigger and wait for a flush. This is only necessary for forcing + * mOriginsHavingData to be updated. Normal operations exposed to content know + * to automatically flush when necessary for correctness. + * + * The notification we're waiting for to verify flushing is fundamentally + * ambiguous (see waitForLocalStorageFlush), so we actually trigger the flush + * twice and wait twice. In the event there was a race, there will be 3 flush + * notifications, but correctness is guaranteed after the second notification. + */ +function triggerAndWaitForLocalStorageFlush() { + if (Services.domStorageManager.nextGenLocalStorageEnabled) { + return new Promise(resolve => executeSoon(resolve)); + } + + SpecialPowers.notifyObservers(null, "domstorage-test-flush-force"); + // This first wait is ambiguous... + return waitForLocalStorageFlush().then(function () { + // So issue a second flush and wait for that. + SpecialPowers.notifyObservers(null, "domstorage-test-flush-force"); + return waitForLocalStorageFlush(); + }); +} + +/** + * Clear the origin's storage so that "OriginsHavingData" will return false for + * our origin. Note that this is only the case for AsyncClear() which is + * explicitly issued against a cache, or AsyncClearAll() which we can trigger + * by wiping all storage. However, the more targeted domain clearings that + * we can trigger via observer, AsyncClearMatchingOrigin and + * AsyncClearMatchingOriginAttributes will not clear the hashtable entry for + * the origin. + * + * So we explicitly access the cache here in the parent for the origin and issue + * an explicit clear. Clearing all storage might be a little easier but seems + * like asking for intermittent failures. + */ +function clearOriginStorageEnsuringNoPreload(origin) { + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin); + + if (Services.domStorageManager.nextGenLocalStorageEnabled) { + let request = Services.qms.clearStoragesForPrincipal( + principal, + "default", + "ls" + ); + let promise = new Promise(resolve => { + request.callback = () => { + resolve(); + }; + }); + return promise; + } + + // We want to use createStorage to force the cache to be created so we can + // issue the clear. It's possible for getStorage to return false but for the + // origin preload hash to still have our origin in it. + let storage = Services.domStorageManager.createStorage( + null, + principal, + principal, + "" + ); + storage.clear(); + + // We also need to trigger a flush os that mOriginsHavingData gets updated. + // The inherent flush race is fine here because + return triggerAndWaitForLocalStorageFlush(); +} + +async function verifyTabPreload(knownTab, expectStorageExists, origin) { + let storageExists = await SpecialPowers.spawn( + knownTab.tab.linkedBrowser, + [origin], + function (origin) { + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin); + if (Services.domStorageManager.nextGenLocalStorageEnabled) { + return Services.domStorageManager.isPreloaded(principal); + } + return !!Services.domStorageManager.getStorage( + null, + principal, + principal + ); + } + ); + is(storageExists, expectStorageExists, "Storage existence === preload"); +} + +/** + * Instruct the given tab to execute the given series of mutations. For + * simplicity, the mutations representation matches the expected events rep. + */ +async function mutateTabStorage(knownTab, mutations, sentinelValue) { + await SpecialPowers.spawn( + knownTab.tab.linkedBrowser, + [{ mutations, sentinelValue }], + function (args) { + return content.wrappedJSObject.mutateStorage(Cu.cloneInto(args, content)); + } + ); +} + +/** + * Instruct the given tab to add a "storage" event listener and record all + * received events. verifyTabStorageEvents is the corresponding method to + * check and assert the recorded events. + */ +async function recordTabStorageEvents(knownTab, sentinelValue) { + await SpecialPowers.spawn( + knownTab.tab.linkedBrowser, + [sentinelValue], + function (sentinelValue) { + return content.wrappedJSObject.listenForStorageEvents(sentinelValue); + } + ); +} + +/** + * Retrieve the current localStorage contents perceived by the tab and assert + * that they match the provided expected state. + * + * If maybeSentinel is non-null, it's assumed to be a string that identifies the + * value we should be waiting for the sentinel key to take on. This is + * necessary because we cannot make any assumptions about when state will be + * propagated to the given process. See the comments in + * page_localstorage_e10s.js for more context. In general, a sentinel value is + * required for correctness unless the process in question is the one where the + * writes were performed or verifyTabStorageEvents was used. + */ +async function verifyTabStorageState(knownTab, expectedState, maybeSentinel) { + let actualState = await SpecialPowers.spawn( + knownTab.tab.linkedBrowser, + [maybeSentinel], + function (maybeSentinel) { + return content.wrappedJSObject.getStorageState(maybeSentinel); + } + ); + + for (let [expectedKey, expectedValue] of Object.entries(expectedState)) { + ok(actualState.hasOwnProperty(expectedKey), "key present: " + expectedKey); + is(actualState[expectedKey], expectedValue, "value correct"); + } + for (let actualKey of Object.keys(actualState)) { + if (!expectedState.hasOwnProperty(actualKey)) { + ok(false, "actual state has key it shouldn't have: " + actualKey); + } + } +} + +/** + * Retrieve and clear the storage events recorded by the tab and assert that + * they match the provided expected events. For simplicity, the expected events + * representation is the same as that used by mutateTabStorage. + * + * Note that by convention for test readability we are passed a 3rd argument of + * the sentinel value, but we don't actually care what it is. + */ +async function verifyTabStorageEvents(knownTab, expectedEvents) { + let actualEvents = await SpecialPowers.spawn( + knownTab.tab.linkedBrowser, + [], + function () { + return content.wrappedJSObject.returnAndClearStorageEvents(); + } + ); + + is(actualEvents.length, expectedEvents.length, "right number of events"); + for (let i = 0; i < actualEvents.length; i++) { + let [actualKey, actualNewValue, actualOldValue] = actualEvents[i]; + let [expectedKey, expectedNewValue, expectedOldValue] = expectedEvents[i]; + is(actualKey, expectedKey, "keys match"); + is(actualNewValue, expectedNewValue, "new values match"); + is(actualOldValue, expectedOldValue, "old values match"); + } +} diff --git a/dom/tests/browser/image.html b/dom/tests/browser/image.html new file mode 100644 index 0000000000..843ebfd1b5 --- /dev/null +++ b/dom/tests/browser/image.html @@ -0,0 +1,2 @@ +<!doctype html> +<img src="dummy.png"></img> diff --git a/dom/tests/browser/load_forever.sjs b/dom/tests/browser/load_forever.sjs new file mode 100644 index 0000000000..8f9fdd18a8 --- /dev/null +++ b/dom/tests/browser/load_forever.sjs @@ -0,0 +1,15 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +let gResponses = []; + +function handleRequest(request, response) { + response.seizePower(); + response.write("HTTP/1.1 200 OK\r\n"); + response.write("Content-Type: text/plain; charset=utf-8\r\n"); + response.write("Cache-Control: no-cache, must-revalidate\r\n"); + response.write("\r\n"); + + // Keep the response alive indefinitely. + gResponses.push(response); +} diff --git a/dom/tests/browser/mimeme.sjs b/dom/tests/browser/mimeme.sjs new file mode 100644 index 0000000000..c3c7122979 --- /dev/null +++ b/dom/tests/browser/mimeme.sjs @@ -0,0 +1,32 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Small red image. +const IMG_BYTES = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" + + "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==" +); + +function handleRequest(request, response) { + let mimeType = request.queryString.match(/type=([a-z]*)/)[1]; + switch (mimeType) { + case "css": + response.setHeader("Content-Type", "text/css"); + response.write("#hi {color: red}"); + break; + case "js": + response.setHeader("Content-Type", "application/javascript"); + response.write("var foo;"); + break; + case "png": + response.setHeader("Content-Type", "image/png"); + response.write(IMG_BYTES); + break; + case "html": + response.setHeader("Content-Type", "text/html"); + response.write("<body>I am a subframe</body>"); + break; + } +} diff --git a/dom/tests/browser/page_bytecode_cache_asm_js.html b/dom/tests/browser/page_bytecode_cache_asm_js.html new file mode 100644 index 0000000000..018099cc6d --- /dev/null +++ b/dom/tests/browser/page_bytecode_cache_asm_js.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<head> + <meta charset="utf-8"> + <title>asm.js test</title> +</head> +<body> + <h1>asm.js test</h1> + <span id="result">ok</span> + <script src="page_bytecode_cache_asm_js.js"></script> +</body> diff --git a/dom/tests/browser/page_bytecode_cache_asm_js.js b/dom/tests/browser/page_bytecode_cache_asm_js.js new file mode 100644 index 0000000000..03f8a6c0b8 --- /dev/null +++ b/dom/tests/browser/page_bytecode_cache_asm_js.js @@ -0,0 +1,30 @@ +window.onerror = function (e) { + document.getElementById("result").textContent = e; +}; + +function f() { + "use asm"; + function test() { + return 10; + } + return test; +} + +var dummy = [ + "dummy text to exceed the minimal source length for bytecode cache", + "dummy text to exceed the minimal source length for bytecode cache", + "dummy text to exceed the minimal source length for bytecode cache", + "dummy text to exceed the minimal source length for bytecode cache", + "dummy text to exceed the minimal source length for bytecode cache", + "dummy text to exceed the minimal source length for bytecode cache", + "dummy text to exceed the minimal source length for bytecode cache", + "dummy text to exceed the minimal source length for bytecode cache", + "dummy text to exceed the minimal source length for bytecode cache", + "dummy text to exceed the minimal source length for bytecode cache", + "dummy text to exceed the minimal source length for bytecode cache", + "dummy text to exceed the minimal source length for bytecode cache", + "dummy text to exceed the minimal source length for bytecode cache", + "dummy text to exceed the minimal source length for bytecode cache", + "dummy text to exceed the minimal source length for bytecode cache", + "dummy text to exceed the minimal source length for bytecode cache", +]; diff --git a/dom/tests/browser/page_localStorage.js b/dom/tests/browser/page_localStorage.js new file mode 100644 index 0000000000..4fecf10bcf --- /dev/null +++ b/dom/tests/browser/page_localStorage.js @@ -0,0 +1,127 @@ +/** + * Helper page used by browser_localStorage_xxx.js. + * + * We expose methods to be invoked by SpecialPowers.spawn() calls. + * SpecialPowers.spawn() uses the message manager and is PContent-based. When + * LocalStorage was PContent-managed, ordering was inherently ensured so we + * could assume each page had already received all relevant events. Now some + * explicit type of coordination is required. + * + * This gets complicated because: + * - LocalStorage is an ugly API that gives us almost unlimited implementation + * flexibility in the face of multiple processes. It's also an API that sites + * may misuse which may encourage us to leverage that flexibility in the + * future to improve performance at the expense of propagation latency, and + * possibly involving content-observable coalescing of events. + * - The Quantum DOM effort and its event labeling and separate task queues and + * green threading and current LocalStorage implementation mean that using + * other PBackground-based APIs such as BroadcastChannel may not provide + * reliable ordering guarantees. Specifically, it's hard to guarantee that + * a BroadcastChannel postMessage() issued after a series of LocalStorage + * writes won't be received by the target window before the writes are + * perceived. At least not without constraining the implementations of both + * APIs. + * - Some of our tests explicitly want to verify LocalStorage behavior without + * having a "storage" listener, so we can't add a storage listener if the test + * didn't already want one. + * + * We use 2 approaches for coordination: + * 1. If we're already listening for events, we listen for the sentinel value to + * be written. This is efficient and appropriate in this case. + * 2. If we're not listening for events, we use setTimeout(0) to poll the + * localStorage key and value until it changes to our expected value. + * setTimeout(0) eventually clamps to setTimeout(4), so in the event we are + * experiencing delays, we have reasonable, non-CPU-consuming back-off in + * place that leaves the CPU free to time out and fail our test if something + * broke. This is ugly but makes us less brittle. + * + * Both of these involve mutateStorage writing the sentinel value at the end of + * the batch. All of our result-returning methods accordingly filter out the + * sentinel key/value pair. + **/ + +var pageName = document.location.search.substring(1); +window.addEventListener("load", () => { + document.getElementById("pageNameH").textContent = pageName; +}); + +// Key that conveys the end of a write batch. Filtered out from state and +// events. +const SENTINEL_KEY = "WRITE_BATCH_SENTINEL"; + +var storageEventsPromise = null; +function listenForStorageEvents(sentinelValue) { + const recordedEvents = []; + storageEventsPromise = new Promise(function (resolve, reject) { + window.addEventListener("storage", function thisHandler(event) { + if (event.key === SENTINEL_KEY) { + // There should be no way for this to have the wrong value, but reject + // if it is wrong. + if (event.newValue === sentinelValue) { + window.removeEventListener("storage", thisHandler); + resolve(recordedEvents); + } else { + reject(event.newValue); + } + } else { + recordedEvents.push([event.key, event.newValue, event.oldValue]); + } + }); + }); +} + +function mutateStorage({ mutations, sentinelValue }) { + mutations.forEach(function ([key, value]) { + if (key !== null) { + if (value === null) { + localStorage.removeItem(key); + } else { + localStorage.setItem(key, value); + } + } else { + localStorage.clear(); + } + }); + localStorage.setItem(SENTINEL_KEY, sentinelValue); +} + +// Returns a promise that is resolve when the sentinel key has taken on the +// sentinel value. Oddly structured to make sure promises don't let us +// accidentally side-step the timeout clamping logic. +function waitForSentinelValue(sentinelValue) { + return new Promise(function (resolve) { + function checkFunc() { + if (localStorage.getItem(SENTINEL_KEY) === sentinelValue) { + resolve(); + } else { + // I believe linters will only yell at us if we use a non-zero constant. + // Other forms of back-off were considered, including attempting to + // issue a round-trip through PBackground, but that still potentially + // runs afoul of labeling while also making us dependent on unrelated + // APIs. + setTimeout(checkFunc, 0); + } + } + checkFunc(); + }); +} + +async function getStorageState(maybeSentinel) { + if (maybeSentinel) { + await waitForSentinelValue(maybeSentinel); + } + + let numKeys = localStorage.length; + let state = {}; + for (var iKey = 0; iKey < numKeys; iKey++) { + let key = localStorage.key(iKey); + if (key !== SENTINEL_KEY) { + state[key] = localStorage.getItem(key); + } + } + return state; +} + +function returnAndClearStorageEvents() { + return storageEventsPromise; +} diff --git a/dom/tests/browser/page_localstorage.html b/dom/tests/browser/page_localstorage.html new file mode 100644 index 0000000000..aacb0ef6ac --- /dev/null +++ b/dom/tests/browser/page_localstorage.html @@ -0,0 +1,8 @@ +<!doctype html> +<html> +<head> + <meta charset="utf-8"> +<script src="page_localStorage.js"></script> +</head> +<body><h2 id="pageNameH"></h2></body> +</html> diff --git a/dom/tests/browser/page_localstorage_coop+coep.html b/dom/tests/browser/page_localstorage_coop+coep.html new file mode 100644 index 0000000000..aacb0ef6ac --- /dev/null +++ b/dom/tests/browser/page_localstorage_coop+coep.html @@ -0,0 +1,8 @@ +<!doctype html> +<html> +<head> + <meta charset="utf-8"> +<script src="page_localStorage.js"></script> +</head> +<body><h2 id="pageNameH"></h2></body> +</html> diff --git a/dom/tests/browser/page_localstorage_coop+coep.html^headers^ b/dom/tests/browser/page_localstorage_coop+coep.html^headers^ new file mode 100644 index 0000000000..63b60e490f --- /dev/null +++ b/dom/tests/browser/page_localstorage_coop+coep.html^headers^ @@ -0,0 +1,2 @@ +Cross-Origin-Opener-Policy: same-origin +Cross-Origin-Embedder-Policy: require-corp diff --git a/dom/tests/browser/page_localstorage_snapshotting.html b/dom/tests/browser/page_localstorage_snapshotting.html new file mode 100644 index 0000000000..b64f4b8cab --- /dev/null +++ b/dom/tests/browser/page_localstorage_snapshotting.html @@ -0,0 +1,68 @@ +<!doctype html> +<html> +<head> + <meta charset="utf-8"> +<script> +/** + * Helper page used by browser_localStorage_snapshotting.js. + * + * We expose methods to be invoked by ContentTask.spawn() calls. + * + **/ +var pageName = document.location.search.substring(1); +window.addEventListener( + "load", + () => { document.getElementById("pageNameH").textContent = pageName; }); + +function applyMutations(mutations) { + mutations.forEach(function([key, value]) { + if (key !== null) { + if (value === null) { + localStorage.removeItem(key); + } else { + localStorage.setItem(key, value); + } + } else { + localStorage.clear(); + } + }); +} + +function getState() { + let state = {}; + let length = localStorage.length; + for (let index = 0; index < length; index++) { + let key = localStorage.key(index); + state[key] = localStorage.getItem(key); + } + return state; +} + +function getKeys() { + return Object.keys(localStorage); +} + +function beginExplicitSnapshot() { + localStorage.beginExplicitSnapshot(); +} + +function checkpointExplicitSnapshot() { + localStorage.checkpointExplicitSnapshot(); +} + +function endExplicitSnapshot() { + localStorage.endExplicitSnapshot(); +} + +function getHasSnapshot() { + return localStorage.hasSnapshot; +} + +function getSnapshotUsage() { + return localStorage.snapshotUsage; +} + +</script> +</head> +<body><h2 id="pageNameH"></h2></body> +</html> diff --git a/dom/tests/browser/page_privatestorageevent.html b/dom/tests/browser/page_privatestorageevent.html new file mode 100644 index 0000000000..90c2184aee --- /dev/null +++ b/dom/tests/browser/page_privatestorageevent.html @@ -0,0 +1,5 @@ +<html> +<body> +la la la la +</body> +</html> diff --git a/dom/tests/browser/perfmetrics/browser.ini b/dom/tests/browser/perfmetrics/browser.ini new file mode 100644 index 0000000000..b4b1d4b63a --- /dev/null +++ b/dom/tests/browser/perfmetrics/browser.ini @@ -0,0 +1,21 @@ +[DEFAULT] +prefs = + dom.performance.children_results_ipc_timeout=2000 + +support-files = + dummy.html + ping_worker.html + ping_worker2.html + ping_worker.js + setinterval.html + settimeout.html + shared_worker.js + unresponsive.html + hello.ogg + sound.html + +[browser_test_performance_metrics.js] +skip-if = verify + +[browser_test_unresponsive.js] +skip-if = true # Bug 1498426 diff --git a/dom/tests/browser/perfmetrics/browser_test_performance_metrics.js b/dom/tests/browser/perfmetrics/browser_test_performance_metrics.js new file mode 100644 index 0000000000..65a7aaaee2 --- /dev/null +++ b/dom/tests/browser/perfmetrics/browser_test_performance_metrics.js @@ -0,0 +1,201 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* 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 ROOT_URL = "http://example.com/browser/dom/tests/browser/perfmetrics"; +const DUMMY_URL = ROOT_URL + "/dummy.html"; +const WORKER_URL = ROOT_URL + "/ping_worker.html"; +const WORKER_URL2 = ROOT_URL + "/ping_worker2.html"; +const INTERVAL_URL = ROOT_URL + "/setinterval.html"; +const TIMEOUT_URL = ROOT_URL + "/settimeout.html"; +const SOUND_URL = ROOT_URL + "/sound.html"; +const CATEGORY_TIMER = 2; + +add_task(async function test() { + waitForExplicitFinish(); + + // Load 3 pages and wait. The 3rd one has a worker + let page1 = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: "about:about", + forceNewProcess: false, + }); + + let page2 = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: "about:memory", + forceNewProcess: false, + }); + + let page3 = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: WORKER_URL, + }); + // load a 4th tab with a worker + await BrowserTestUtils.withNewTab( + { gBrowser, url: WORKER_URL2 }, + async function (browser) { + // grab events.. + let workerDuration = 0; + let workerTotal = 0; + let duration = 0; + let total = 0; + let isTopLevel = false; + let aboutMemoryFound = false; + let parentProcessEvent = false; + let subFrameIds = []; + let topLevelIds = []; + let sharedWorker = false; + let counterIds = []; + let timerCalls = 0; + let heapUsage = 0; + let mediaMemory = 0; + + function exploreResults(data, filterByWindowId) { + for (let entry of data) { + if (filterByWindowId && entry.windowId != filterByWindowId) { + continue; + } + if (!counterIds.includes(entry.pid + ":" + entry.counterId)) { + counterIds.push(entry.pid + ":" + entry.counterId); + } + sharedWorker = + entry.host.endsWith("shared_worker.js") || sharedWorker; + heapUsage += entry.memoryInfo.jsMemUsage; + mediaMemory += + entry.memoryInfo.media.audioSize + + entry.memoryInfo.media.resourcesSize; + Assert.ok( + entry.host != "" || entry.windowId != 0, + "An entry should have a host or a windowId" + ); + if ( + entry.windowId != 0 && + !entry.isToplevel && + !entry.isWorker && + !subFrameIds.includes(entry.windowId) + ) { + subFrameIds.push(entry.windowId); + } + if (entry.isTopLevel && !topLevelIds.includes(entry.windowId)) { + topLevelIds.push(entry.windowId); + } + if (entry.host == "example.com" && entry.isTopLevel) { + isTopLevel = true; + } + if (entry.host == "about:memory") { + aboutMemoryFound = true; + } + if (entry.pid == Services.appinfo.processID) { + parentProcessEvent = true; + } + if (entry.isWorker) { + workerDuration += entry.duration; + } else { + duration += entry.duration; + } + // let's look at the data we got back + for (let item of entry.items) { + Assert.ok( + item.count > 0, + "Categories with an empty count are dropped" + ); + if (entry.isWorker) { + workerTotal += item.count; + } else { + total += item.count; + } + if (item.category == CATEGORY_TIMER) { + timerCalls += item.count; + } + } + } + } + + // get all metrics via the promise + let results = await ChromeUtils.requestPerformanceMetrics(); + exploreResults(results); + + Assert.greater(workerDuration, 0, "Worker duration should be positive"); + Assert.greater(workerTotal, 0, "Worker count should be positive"); + Assert.greater(duration, 0, "Duration should be positive"); + Assert.greater(total, 0, "Should get a positive count"); + Assert.ok(parentProcessEvent, "parent process sent back some events"); + Assert.ok(isTopLevel, "example.com as a top level window"); + Assert.ok(aboutMemoryFound, "about:memory"); + Assert.greater(heapUsage, 0, "got some memory value reported"); + Assert.ok(sharedWorker, "We got some info from a shared worker"); + let numCounters = counterIds.length; + Assert.ok( + numCounters > 5, + "This test generated at least " + numCounters + " unique counters" + ); + + // checking that subframes are not orphans + for (let frameId of subFrameIds) { + Assert.ok(topLevelIds.includes(frameId), "subframe is not orphan "); + } + + // Doing a second call, we shoud get bigger values + let previousWorkerDuration = workerDuration; + let previousWorkerTotal = workerTotal; + let previousDuration = duration; + let previousTotal = total; + + results = await ChromeUtils.requestPerformanceMetrics(); + exploreResults(results); + + Assert.ok( + workerDuration > previousWorkerDuration, + "Worker duration should be positive" + ); + Assert.ok( + workerTotal > previousWorkerTotal, + "Worker count should be positive" + ); + Assert.greater(duration, previousDuration, "Duration should be positive"); + Assert.greater(total, previousTotal, "Should get a positive count"); + + // load a tab with a setInterval, we should get counters on TaskCategory::Timer + await BrowserTestUtils.withNewTab( + { gBrowser, url: INTERVAL_URL }, + async function (browser) { + let tabId = gBrowser.selectedBrowser.outerWindowID; + let previousTimerCalls = timerCalls; + results = await ChromeUtils.requestPerformanceMetrics(); + exploreResults(results, tabId); + Assert.greater(timerCalls, previousTimerCalls, "Got timer calls"); + } + ); + + // load a tab with a setTimeout, we should get counters on TaskCategory::Timer + await BrowserTestUtils.withNewTab( + { gBrowser, url: TIMEOUT_URL }, + async function (browser) { + let tabId = gBrowser.selectedBrowser.outerWindowID; + let previousTimerCalls = timerCalls; + results = await ChromeUtils.requestPerformanceMetrics(); + exploreResults(results, tabId); + Assert.greater(timerCalls, previousTimerCalls, "Got timer calls"); + } + ); + + // load a tab with a sound + await BrowserTestUtils.withNewTab( + { gBrowser, url: SOUND_URL }, + async function (browser) { + let tabId = gBrowser.selectedBrowser.outerWindowID; + results = await ChromeUtils.requestPerformanceMetrics(); + exploreResults(results, tabId); + Assert.greater(mediaMemory, 0, "Got some memory used for media"); + } + ); + } + ); + + BrowserTestUtils.removeTab(page1); + BrowserTestUtils.removeTab(page2); + BrowserTestUtils.removeTab(page3); +}); diff --git a/dom/tests/browser/perfmetrics/browser_test_unresponsive.js b/dom/tests/browser/perfmetrics/browser_test_unresponsive.js new file mode 100644 index 0000000000..770c387e7a --- /dev/null +++ b/dom/tests/browser/perfmetrics/browser_test_unresponsive.js @@ -0,0 +1,31 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* 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 ROOT_URL = "http://example.com/browser/dom/tests/browser/perfmetrics"; +const PAGE_URL = ROOT_URL + "/unresponsive.html"; + +add_task(async function test() { + waitForExplicitFinish(); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: PAGE_URL }, + async function (browser) { + let dataBack = 0; + let tabId = gBrowser.selectedBrowser.outerWindowID; + + function exploreResults(data, filterByWindowId) { + for (let entry of data) { + if (entry.windowId == tabId && entry.host != "about:blank") { + dataBack += 1; + } + } + } + let results = await ChromeUtils.requestPerformanceMetrics(); + exploreResults(results); + Assert.ok(dataBack == 0); + } + ); +}); diff --git a/dom/tests/browser/perfmetrics/dummy.html b/dom/tests/browser/perfmetrics/dummy.html new file mode 100644 index 0000000000..6ec72c2160 --- /dev/null +++ b/dom/tests/browser/perfmetrics/dummy.html @@ -0,0 +1,13 @@ +<!doctype html> +<html> +<head> +<title>Dummy test page</title> +<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta> +</head> +<body> +<p>Dummy test page</p> +<script> + localStorage.setItem("foo", "bar"); +</script> +</body> +</html> diff --git a/dom/tests/browser/perfmetrics/hello.ogg b/dom/tests/browser/perfmetrics/hello.ogg Binary files differnew file mode 100644 index 0000000000..7a80926065 --- /dev/null +++ b/dom/tests/browser/perfmetrics/hello.ogg diff --git a/dom/tests/browser/perfmetrics/ping_worker.html b/dom/tests/browser/perfmetrics/ping_worker.html new file mode 100644 index 0000000000..c576dbcb22 --- /dev/null +++ b/dom/tests/browser/perfmetrics/ping_worker.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<html lang="en" dir="ltr"> +<head> + <meta charset="utf-8"> + <script type="text/javascript"> + + var myWorker; + var shared; + + function init() { + myWorker = new Worker("ping_worker.js"); + for (let i = 0; i++; i < 10) myWorker.postMessage("ping"); + + shared = new SharedWorker("shared_worker.js"); + shared.port.start(); + shared.port.onmessage = function(e) { + console.log(e); + }; + } + + </script> +</head> +<body onload="init()"> + <h1>A page with a worker and a shared worker</h1> +</body> +</html> diff --git a/dom/tests/browser/perfmetrics/ping_worker.js b/dom/tests/browser/perfmetrics/ping_worker.js new file mode 100644 index 0000000000..0ed6bb8ba4 --- /dev/null +++ b/dom/tests/browser/perfmetrics/ping_worker.js @@ -0,0 +1,11 @@ +/* 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"; + +function messageListener(event) { + postMessage("pong"); +} + +addEventListener("message", { handleEvent: messageListener }); diff --git a/dom/tests/browser/perfmetrics/ping_worker2.html b/dom/tests/browser/perfmetrics/ping_worker2.html new file mode 100644 index 0000000000..48f6658218 --- /dev/null +++ b/dom/tests/browser/perfmetrics/ping_worker2.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<html lang="en" dir="ltr"> +<head> + <meta charset="utf-8"> + <script type="text/javascript"> + + var shared; + + function init() { + shared = new SharedWorker("shared_worker.js"); + shared.port.start(); + for (let i = 0; i < 10; i++) shared.port.postMessage(["ok"]); + } + + </script> +</head> +<body onload="init()"> + <h1>A page with a shared worker</h1> +</body> +</html> diff --git a/dom/tests/browser/perfmetrics/setinterval.html b/dom/tests/browser/perfmetrics/setinterval.html new file mode 100644 index 0000000000..4c3e7264ca --- /dev/null +++ b/dom/tests/browser/perfmetrics/setinterval.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html lang="en" dir="ltr"> +<head> + <meta charset="utf-8"> + <script type="text/javascript"> + var interval; + + function doSomething() { + console.log("We are doing something here"); + clearInterval(interval); + } + + interval = setInterval(doSomething, 1); + </script> +</head> +<body> + <h1>A page with a setInterval() call</h1> +</body> +</html> diff --git a/dom/tests/browser/perfmetrics/settimeout.html b/dom/tests/browser/perfmetrics/settimeout.html new file mode 100644 index 0000000000..01f632caf5 --- /dev/null +++ b/dom/tests/browser/perfmetrics/settimeout.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html lang="en" dir="ltr"> +<head> + <meta charset="utf-8"> + <script type="text/javascript"> + + function doSomething() { + console.log("We are doing something here"); + } + + setTimeout(doSomething, 1); + </script> +</head> +<body> + <h1>A page with a setTimeout() call</h1> +</body> +</html> diff --git a/dom/tests/browser/perfmetrics/shared_worker.js b/dom/tests/browser/perfmetrics/shared_worker.js new file mode 100644 index 0000000000..cb00bfb3eb --- /dev/null +++ b/dom/tests/browser/perfmetrics/shared_worker.js @@ -0,0 +1,7 @@ +let onconnect = function (e) { + var port = e.ports[0]; + + port.onmessage = function (e) { + port.postMessage(e.data[0]); + }; +}; diff --git a/dom/tests/browser/perfmetrics/sound.html b/dom/tests/browser/perfmetrics/sound.html new file mode 100644 index 0000000000..e365396f31 --- /dev/null +++ b/dom/tests/browser/perfmetrics/sound.html @@ -0,0 +1,14 @@ +<!doctype html> +<html> +<head> +<title>Dummy test page</title> +<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta> +</head> +<body> +<p>Page with a sound</p> +<audio controls autoplay> + <source src="hello.ogg" type="audio/ogg"> +</audio> +</script> +</body> +</html> diff --git a/dom/tests/browser/perfmetrics/unresponsive.html b/dom/tests/browser/perfmetrics/unresponsive.html new file mode 100644 index 0000000000..e139eb7f9d --- /dev/null +++ b/dom/tests/browser/perfmetrics/unresponsive.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<html lang="en" dir="ltr"> +<head> + <meta charset="utf-8"> + <script type="text/javascript"> + + function fn() { + let start = Date.now(); + while (Date.now() - start < 5000) + ; // do nothing + setTimeout(fn, 0); + } + + setTimeout(fn, 10); + + </script> +</head> +<body> + <h1>An unresponsive page</h1> +</body> +</html> diff --git a/dom/tests/browser/position.html b/dom/tests/browser/position.html new file mode 100644 index 0000000000..8fca4d6af9 --- /dev/null +++ b/dom/tests/browser/position.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<html lang="en" dir="ltr"> +<head> + <script type="text/javascript"> + var result = null; + + function handlePosition(position) { + // eslint-disable-next-line no-unsanitized/property + result.innerHTML = position.coords.latitude + " " + position.coords.longitude; + } + + function handleError(error) { + // eslint-disable-next-line no-unsanitized/property + result.innerHTML = error.message; + } + + function init() { + result = document.getElementById("result"); + + if (navigator.geolocation) + navigator.geolocation.getCurrentPosition(handlePosition, handleError); + else + result.innerHTML = "not available"; + } + + </script> +</head> +<body onload="init()"> + <p id="result">location...</p> +</body> +</html> diff --git a/dom/tests/browser/prevent_return_key.html b/dom/tests/browser/prevent_return_key.html new file mode 100644 index 0000000000..4ec846f2e0 --- /dev/null +++ b/dom/tests/browser/prevent_return_key.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Prevent return key should not submit form</title> + <script type="application/javascript"> + function init() { + let input = document.getElementById("input"); + input.addEventListener("keydown", function(aEvent) { + if (aEvent.keyCode == 13) { // return key + alert("Hello!"); + aEvent.preventDefault(); + return false; + } + return true; + }, {once: true}); + + let form = document.getElementById("form"); + form.addEventListener("submit", function() { + let result = document.getElementById("result"); + result.innerHTML = "submitted"; + }, {once: true}); + } + </script> +</head> + +<body onload="init()"> + <form id="form"> + <input type="text" id="input"> + <button type="submit" id="submitBtn">Submit</button> + </form> + <p id="result">not submitted</p> +</body> +</html> diff --git a/dom/tests/browser/set-samesite-cookies-and-redirect.sjs b/dom/tests/browser/set-samesite-cookies-and-redirect.sjs new file mode 100644 index 0000000000..940b1ee845 --- /dev/null +++ b/dom/tests/browser/set-samesite-cookies-and-redirect.sjs @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function handleRequest(request, response) { + // Set cookies and redirect for .org: + if (request.host.endsWith(".org")) { + response.setHeader("Set-Cookie", "normalCookie=true; path=/;", true); + response.setHeader( + "Set-Cookie", + "laxHeader=true; path=/; SameSite=Lax", + true + ); + response.setHeader( + "Set-Cookie", + "strictHeader=true; path=/; SameSite=Strict", + true + ); + response.write(` + <head> + <meta http-equiv='set-cookie' content='laxMeta=true; path=/; SameSite=Lax'> + <meta http-equiv='set-cookie' content='strictMeta=true; path=/; SameSite=Strict'> + </head> + <body> + <script> + document.cookie = 'laxScript=true; path=/; SameSite=Lax'; + document.cookie = 'strictScript=true; path=/; SameSite=Strict'; + location.href = location.href.replace(/\.org/, ".com"); + </script> + </body>`); + } else { + let baseURI = + "https://example.org/" + + request.path.replace(/[a-z-]*\.sjs/, "mimeme.sjs?type="); + response.write(` + <link rel="stylesheet" type="text/css" href="${baseURI}css"> + <iframe src="${baseURI}html"></iframe> + <script src="${baseURI}js"></script> + <img src="${baseURI}png"> + `); + } +} diff --git a/dom/tests/browser/test-console-api.html b/dom/tests/browser/test-console-api.html new file mode 100644 index 0000000000..e8da7c311e --- /dev/null +++ b/dom/tests/browser/test-console-api.html @@ -0,0 +1,78 @@ +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"> + <head> + <meta charset=utf8> + <title>Console API test page</title> + <script type="text/javascript"> + window.foobar585956c = function(a) { + console.trace(); + return a + "c"; + }; + + /* global foobar585956c */ + function foobar585956b(a) { + return foobar585956c(a + "b"); + } + + function foobar585956a(omg) { + return foobar585956b(omg + "a"); + } + + function foobar646025(omg) { + console.log(omg, "o", "d"); + } + + function startTimer(timer) { + console.time(timer); + } + + function stopTimer(timer) { + console.timeEnd(timer); + } + + function namelessTimer() { + console.time(); + console.timeEnd(); + } + + function test() { + var str = "Test Message."; + console.foobar(str); // if this throws, we don't execute following funcs + console.log(str); + console.info(str); + console.warn(str); + console.error(str); + console.exception(str); + console.assert(false, str); + console.count(str); + } + + function testGroups() { + console.groupCollapsed("a", "group"); + console.group("b", "group"); + console.groupEnd(); + } + + function nativeCallback() { + new Promise(function(resolve, reject) { resolve(42); }).then(console.log); + } + + function timeStamp(val) { + console.timeStamp(val); + } + </script> + </head> + <body> + <h1>Console API Test Page</h1> + <button onclick="test();">Log stuff</button> + <button id="test-trace" onclick="foobar585956a('omg');">Test trace</button> + <button id="test-location" onclick="foobar646025('omg');">Test location</button> + <button id="test-nativeCallback" onclick="nativeCallback();">Test nativeCallback</button> + <button id="test-groups" onclick="testGroups();">Test groups</button> + <button id="test-time" onclick="startTimer('foo');">Test time</button> + <button id="test-timeEnd" onclick="stopTimer('foo');">Test timeEnd</button> + <button id="test-namelessTimer" onclick="namelessTimer();">Test namelessTimer</button> + <button id="test-timeStamp" onclick="timeStamp('!!!')">Test timeStamp</button> + <button id="test-emptyTimeStamp" onclick="timeStamp();">Test emptyTimeStamp</button> + </body> +</html> diff --git a/dom/tests/browser/test_bug1004814.html b/dom/tests/browser/test_bug1004814.html new file mode 100644 index 0000000000..9f97e0487a --- /dev/null +++ b/dom/tests/browser/test_bug1004814.html @@ -0,0 +1,8 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>Console API test bug 1004814</title> + </head> + <body> + </body> +</html> diff --git a/dom/tests/browser/test_mixed_content_image.html b/dom/tests/browser/test_mixed_content_image.html new file mode 100644 index 0000000000..c8b7661f42 --- /dev/null +++ b/dom/tests/browser/test_mixed_content_image.html @@ -0,0 +1 @@ +<body></body> diff --git a/dom/tests/browser/test_new_window_from_content_child.html b/dom/tests/browser/test_new_window_from_content_child.html new file mode 100644 index 0000000000..9e3f7016d9 --- /dev/null +++ b/dom/tests/browser/test_new_window_from_content_child.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<head> + <meta charset="utf-8"> + <title>Test popup window opening behaviour</title> +</head> +<body> + <p><a id="winOpenDefault" href="#" onclick="return openWindow();">Open a new window via window.open with default features.</a></p> + <p><a id="winOpenNonDefault" href="#" onclick="return openWindow('resizable=no, location=no, personalbar=no, toolbar=no, scrollbars=no, menubar=no, status=no, directories=no, height=100, width=500');">Open a new window via window.open with non-default features.</a></p> + <p><a id="winOpenDialog" href="#" onclick="return openWindow('dialog=yes');">Open a new window via window.open with dialog=1.</a></p> + <p><a id="winOpenNoURLNonDefault" href="#" onclick="return openBlankWindow('location=no, toolbar=no, height=100, width=100');">Open a blank new window via window.open with non-default features.</a></p> + <p><a id="targetBlank" href="dummy.html" target="_blank" rel="opener">Open a new window via target="_blank".</a></p> +</body> +</html> + +<script> +function openWindow(aFeatures = "") { + window.open("dummy.html", "_blank", aFeatures); + return false; +} + +function openBlankWindow(aFeatures = "") { + window.open("", "_blank", aFeatures); + return false; +} +</script> diff --git a/dom/tests/browser/test_noopener_source.html b/dom/tests/browser/test_noopener_source.html new file mode 100644 index 0000000000..fbf28be260 --- /dev/null +++ b/dom/tests/browser/test_noopener_source.html @@ -0,0 +1,15 @@ +<a id="test1" href="test_noopener_target.html" target="_blank" rel="opener">1</a> +<a id="test2" href="test_noopener_target.html" target="_blank" rel="noopener">2</a> +<a id="test3" href="test_noopener_target.html" target="_blank" rel="noreferrer">3</a> + +<a id="test4" href="test_noopener_target.html" target="uniquename1">4</a> +<a id="test5" href="test_noopener_target.html" target="uniquename2" rel="noopener">5</a> +<a id="test6" href="test_noopener_target.html" target="uniquename3" rel="noreferrer">6</a> + +<a id="test7" onclick="window.open('test_noopener_target.html', '_blank')">7</a> +<a id="test8" onclick="window.open('test_noopener_target.html', '_blank', 'noopener')">8</a> +<a id="test9" onclick="window.open('test_noopener_target.html', '_blank', 'noreferrer')">9</a> + +<a id="test10" onclick="window.open('test_noopener_target.html', 'uniquename1')">10</a> +<a id="test11" onclick="window.open('test_noopener_target.html', 'uniquename2', 'noopener')">11</a> +<a id="test12" onclick="window.open('test_noopener_target.html', 'uniquename3', 'noreferrer')">12</a> diff --git a/dom/tests/browser/test_noopener_target.html b/dom/tests/browser/test_noopener_target.html new file mode 100644 index 0000000000..f437fc7ba5 --- /dev/null +++ b/dom/tests/browser/test_noopener_target.html @@ -0,0 +1,9 @@ + +<h2>name</h2> +<div id="window_name"></div> + +<script> + document.querySelector("#window_name").textContent = + window.name; + +</script> diff --git a/dom/tests/browser/worker_bug1004814.js b/dom/tests/browser/worker_bug1004814.js new file mode 100644 index 0000000000..4fb54da692 --- /dev/null +++ b/dom/tests/browser/worker_bug1004814.js @@ -0,0 +1,6 @@ +onmessage = function (evt) { + console.time("bug1004814"); + setTimeout(function () { + console.timeEnd("bug1004814"); + }, 200); +}; |