summaryrefslogtreecommitdiffstats
path: root/dom/tests/browser
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /dom/tests/browser
parentInitial commit. (diff)
downloadfirefox-upstream/124.0.1.tar.xz
firefox-upstream/124.0.1.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--dom/tests/browser/beforeunload_test_page.html92
-rw-r--r--dom/tests/browser/browser.toml177
-rw-r--r--dom/tests/browser/browser_ConsoleAPI_originAttributes.js108
-rw-r--r--dom/tests/browser/browser_ConsoleStorageAPITests.js108
-rw-r--r--dom/tests/browser/browser_ConsoleStoragePBTest_perwindowpb.js96
-rw-r--r--dom/tests/browser/browser_alert_from_about_blank.js42
-rw-r--r--dom/tests/browser/browser_autofocus_background.js59
-rw-r--r--dom/tests/browser/browser_autofocus_preference.js16
-rw-r--r--dom/tests/browser/browser_beforeunload_between_chrome_content.js152
-rw-r--r--dom/tests/browser/browser_bug1004814.js47
-rw-r--r--dom/tests/browser/browser_bug1008941_dismissGeolocationHanger.js45
-rw-r--r--dom/tests/browser/browser_bug1236512.js119
-rw-r--r--dom/tests/browser/browser_bug1238427.js40
-rw-r--r--dom/tests/browser/browser_bug1316330.js52
-rw-r--r--dom/tests/browser/browser_bug1563629.js79
-rw-r--r--dom/tests/browser/browser_bug1685807.js78
-rw-r--r--dom/tests/browser/browser_bug1709346.js48
-rw-r--r--dom/tests/browser/browser_bug396843.js342
-rw-r--r--dom/tests/browser/browser_bytecode_cache_asm_js.js31
-rw-r--r--dom/tests/browser/browser_cancel_keydown_keypress_event.js41
-rw-r--r--dom/tests/browser/browser_data_document_crossOriginIsolated.js68
-rw-r--r--dom/tests/browser/browser_focus_steal_from_chrome.js214
-rw-r--r--dom/tests/browser/browser_focus_steal_from_chrome_during_mousedown.js82
-rw-r--r--dom/tests/browser/browser_form_associated_custom_elements_validity.js111
-rw-r--r--dom/tests/browser/browser_frame_elements.html15
-rw-r--r--dom/tests/browser/browser_frame_elements.js74
-rw-r--r--dom/tests/browser/browser_hasActivePeerConnections.js134
-rw-r--r--dom/tests/browser/browser_hasbeforeunload.js875
-rw-r--r--dom/tests/browser/browser_keypressTelemetry.js69
-rw-r--r--dom/tests/browser/browser_localStorage_e10s.js284
-rw-r--r--dom/tests/browser/browser_localStorage_fis.js529
-rw-r--r--dom/tests/browser/browser_localStorage_privatestorageevent.js87
-rw-r--r--dom/tests/browser/browser_localStorage_snapshotting.js774
-rw-r--r--dom/tests/browser/browser_navigate_replace_browsingcontext.js23
-rw-r--r--dom/tests/browser/browser_noopener.js176
-rw-r--r--dom/tests/browser/browser_noopener_null_uri.js15
-rw-r--r--dom/tests/browser/browser_persist_cookies.js128
-rw-r--r--dom/tests/browser/browser_persist_cross_origin_iframe.js198
-rw-r--r--dom/tests/browser/browser_persist_image_accept.js139
-rw-r--r--dom/tests/browser/browser_persist_mixed_content_image.js109
-rw-r--r--dom/tests/browser/browser_pointerlock_warning.js129
-rw-r--r--dom/tests/browser/browser_sessionStorage_navigation.js271
-rw-r--r--dom/tests/browser/browser_test_focus_after_modal_state.js71
-rw-r--r--dom/tests/browser/browser_test_new_window_from_content.js221
-rw-r--r--dom/tests/browser/browser_test_toolbars_visibility.js323
-rw-r--r--dom/tests/browser/browser_unlinkable_about_page_can_load_module_scripts.js84
-rw-r--r--dom/tests/browser/browser_wakelock.js40
-rw-r--r--dom/tests/browser/browser_windowProxy_transplant.js219
-rw-r--r--dom/tests/browser/browser_xhr_sandbox.js62
-rw-r--r--dom/tests/browser/create_webrtc_peer_connection.html28
-rw-r--r--dom/tests/browser/dummy.html13
-rw-r--r--dom/tests/browser/dummy.pngbin0 -> 703 bytes
-rw-r--r--dom/tests/browser/file_bug1685807.html12
-rw-r--r--dom/tests/browser/file_coop_coep.html6
-rw-r--r--dom/tests/browser/file_coop_coep.html^headers^2
-rw-r--r--dom/tests/browser/file_empty.html0
-rw-r--r--dom/tests/browser/file_empty_cross_site_frame.html2
-rw-r--r--dom/tests/browser/file_load_module_script.html8
-rw-r--r--dom/tests/browser/file_module_loaded.mjs9
-rw-r--r--dom/tests/browser/file_module_loaded2.mjs3
-rw-r--r--dom/tests/browser/file_postMessage_parent.html48
-rw-r--r--dom/tests/browser/focus_after_prompt.html18
-rw-r--r--dom/tests/browser/geo_leak_test.html17
-rw-r--r--dom/tests/browser/helper_localStorage.js302
-rw-r--r--dom/tests/browser/image.html2
-rw-r--r--dom/tests/browser/load_forever.sjs15
-rw-r--r--dom/tests/browser/mimeme.sjs32
-rw-r--r--dom/tests/browser/page_bytecode_cache_asm_js.html10
-rw-r--r--dom/tests/browser/page_bytecode_cache_asm_js.js30
-rw-r--r--dom/tests/browser/page_localStorage.js127
-rw-r--r--dom/tests/browser/page_localstorage.html8
-rw-r--r--dom/tests/browser/page_localstorage_coop+coep.html8
-rw-r--r--dom/tests/browser/page_localstorage_coop+coep.html^headers^2
-rw-r--r--dom/tests/browser/page_localstorage_snapshotting.html68
-rw-r--r--dom/tests/browser/page_privatestorageevent.html5
-rw-r--r--dom/tests/browser/position.html29
-rw-r--r--dom/tests/browser/prevent_return_key.html34
-rw-r--r--dom/tests/browser/set-samesite-cookies-and-redirect.sjs43
-rw-r--r--dom/tests/browser/test-console-api.html78
-rw-r--r--dom/tests/browser/test_bug1004814.html8
-rw-r--r--dom/tests/browser/test_mixed_content_image.html1
-rw-r--r--dom/tests/browser/test_new_window_from_content_child.html25
-rw-r--r--dom/tests/browser/test_noopener_source.html15
-rw-r--r--dom/tests/browser/test_noopener_target.html9
-rw-r--r--dom/tests/browser/worker_bug1004814.js6
85 files changed, 8319 insertions, 0 deletions
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.toml b/dom/tests/browser/browser.toml
new file mode 100644
index 0000000000..dfb7c2c569
--- /dev/null
+++ b/dom/tests/browser/browser.toml
@@ -0,0 +1,177 @@
+[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_ConsoleAPI_originAttributes.js"]
+
+["browser_ConsoleStorageAPITests.js"]
+
+["browser_ConsoleStoragePBTest_perwindowpb.js"]
+
+["browser_alert_from_about_blank.js"]
+
+["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"]
+
+["browser_bug1709346.js"]
+support-files = [
+ "load_forever.sjs",
+ "file_empty_cross_site_frame.html",
+]
+
+["browser_bytecode_cache_asm_js.js"]
+support-files = [
+ "page_bytecode_cache_asm_js.html",
+ "page_bytecode_cache_asm_js.js",
+]
+
+["browser_cancel_keydown_keypress_event.js"]
+support-files = ["prevent_return_key.html"]
+
+["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_hasActivePeerConnections.js"]
+support-files = ["create_webrtc_peer_connection.html"]
+skip-if = [
+ "os == 'linux' && bits == 64", # Bug 1742012
+ "os == 'win' && debug", # Bug 1742012
+]
+
+["browser_hasbeforeunload.js"]
+https_first_disabled = true
+support-files = ["beforeunload_test_page.html"]
+
+["browser_keypressTelemetry.js"]
+skip-if = ["true"]
+
+["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",
+]
+
+["browser_localStorage_privatestorageevent.js"]
+
+["browser_localStorage_snapshotting.js"]
+
+["browser_navigate_replace_browsingcontext.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_persist_cookies.js"]
+support-files = [
+ "set-samesite-cookies-and-redirect.sjs",
+ "mimeme.sjs",
+]
+
+["browser_persist_cross_origin_iframe.js"]
+support-files = ["image.html"]
+
+["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 = [
+ "os == '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"]
+
+["browser_unlinkable_about_page_can_load_module_scripts.js"]
+support-files = [
+ "file_load_module_script.html",
+ "file_module_loaded.mjs",
+ "file_module_loaded2.mjs",
+]
+
+["browser_wakelock.js"]
+
+["browser_windowProxy_transplant.js"]
+support-files = ["file_postMessage_parent.html"]
+
+["browser_xhr_sandbox.js"]
diff --git a/dom/tests/browser/browser_ConsoleAPI_originAttributes.js b/dom/tests/browser/browser_ConsoleAPI_originAttributes.js
new file mode 100644
index 0000000000..a9cc4989db
--- /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.createAboutBlankDocumentViewer(principal, principal);
+
+ info("fake webextension docShell created");
+
+ registerCleanupFunction(function () {
+ policy.active = false;
+ if (chromeWebNav) {
+ chromeWebNav.close();
+ chromeWebNav = null;
+ }
+ ConsoleObserver.uninit();
+ });
+
+ let window = docShell.docViewer.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..38f85ef5b1
--- /dev/null
+++ b/dom/tests/browser/browser_ConsoleStoragePBTest_perwindowpb.js
@@ -0,0 +1,96 @@
+/* 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.startLoadingURIString(
+ 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_alert_from_about_blank.js b/dom/tests/browser/browser_alert_from_about_blank.js
new file mode 100644
index 0000000000..aa12dd1f7e
--- /dev/null
+++ b/dom/tests/browser/browser_alert_from_about_blank.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_check_alert_from_blank() {
+ await BrowserTestUtils.withNewTab(
+ "https://example.com/blank",
+ async browser => {
+ await SpecialPowers.spawn(browser, [], () => {
+ let button = content.document.createElement("button");
+ button.addEventListener("click", () => {
+ let newWin = content.open(
+ "about:blank",
+ "",
+ "popup,width=600,height=600"
+ );
+ newWin.alert("Alert from the popup.");
+ });
+ content.document.body.append(button);
+ });
+ let newWinPromise = BrowserTestUtils.waitForNewWindow();
+ await BrowserTestUtils.synthesizeMouseAtCenter("button", {}, browser);
+ let otherWin = await newWinPromise;
+ Assert.equal(
+ otherWin.gBrowser.currentURI?.spec,
+ "about:blank",
+ "Should have opened about:blank"
+ );
+ Assert.equal(
+ otherWin.menubar.visible,
+ false,
+ "Should be a popup window."
+ );
+ let contentDialogManager = gBrowser
+ .getTabDialogBox(gBrowser.selectedBrowser)
+ .getContentDialogManager();
+ todo_is(contentDialogManager._dialogs.length, 1, "Should have 1 dialog.");
+ await BrowserTestUtils.closeWindow(otherWin);
+ }
+ );
+});
diff --git a/dom/tests/browser/browser_autofocus_background.js b/dom/tests/browser/browser_autofocus_background.js
new file mode 100644
index 0000000000..7c0696cfd2
--- /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.startLoadingURIString(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..8fdb08ff98
--- /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.startLoadingURIString(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..ae211c59fe
--- /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.startLoadingURIString(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.startLoadingURIString(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..789709a8d7
--- /dev/null
+++ b/dom/tests/browser/browser_bug1004814.js
@@ -0,0 +1,47 @@
+/* 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);
+ });
+ });
+
+ Assert.greater(
+ 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..ae339e1d8d
--- /dev/null
+++ b/dom/tests/browser/browser_bug1008941_dismissGeolocationHanger.js
@@ -0,0 +1,45 @@
+/* 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;
+
+ // We intentionally turn off this a11y check, because the following click
+ // is sent on the <toolbar> to dismiss the Geolocation hanger using an
+ // alternative way of the popup dismissal, while the other way like `Esc` key
+ // is available for assistive technology users, thus this test can be ignored.
+ AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false });
+ // click outside the Geolocation hanger to dismiss it
+ window.document.getElementById("nav-bar").click();
+ AccessibilityUtils.resetEnv();
+ 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..66d58ab132
--- /dev/null
+++ b/dom/tests/browser/browser_bug1236512.js
@@ -0,0 +1,119 @@
+/**
+ * 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.startLoadingURIString(
+ 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..afbf5970d0
--- /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.startLoadingURIString(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..e51f9c705b
--- /dev/null
+++ b/dom/tests/browser/browser_bug396843.js
@@ -0,0 +1,342 @@
+/** 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;
+ }
+ Assert.strictEqual(
+ doc.wrappedJSObject.nodePrincipal,
+ undefined,
+ "Must not have document principal for " + documentID
+ );
+ Assert.strictEqual(
+ doc.wrappedJSObject.baseURIObject,
+ undefined,
+ "Must not have document base URI for " + documentID
+ );
+ Assert.strictEqual(
+ doc.wrappedJSObject.documentURIObject,
+ undefined,
+ "Must not have document URI for " + documentID
+ );
+
+ for (var i = 0; i < allNodes.length; ++i) {
+ Assert.strictEqual(
+ allNodes[i].wrappedJSObject.nodePrincipal,
+ undefined,
+ "Unexpected principal appears for " +
+ allNodes[i].nodeName +
+ " in " +
+ documentID
+ );
+ Assert.strictEqual(
+ 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..6edfc922e6
--- /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.startLoadingURIString(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..12ee374d82
--- /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.startLoadingURIString(bg, test.uri);
+ await BrowserTestUtils.browserLoaded(bg);
+ BrowserTestUtils.startLoadingURIString(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.startLoadingURIString(bg, "about:blank");
+ await BrowserTestUtils.browserLoaded(bg);
+ BrowserTestUtils.startLoadingURIString(fg, "about:blank");
+ await BrowserTestUtils.browserLoaded(fg);
+
+ // Load the URIs.
+ BrowserTestUtils.startLoadingURIString(bg, test.uri);
+ await BrowserTestUtils.browserLoaded(bg);
+ BrowserTestUtils.startLoadingURIString(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..2930e0facd
--- /dev/null
+++ b/dom/tests/browser/browser_focus_steal_from_chrome_during_mousedown.js
@@ -0,0 +1,82 @@
+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
+ );
+
+ // We intentionally turn off this a11y check, because the following click
+ // is sent on an arbitrary web content that is not expected to be tested
+ // by itself with the browser mochitests, therefore this rule check shall
+ // be ignored by a11y-checks suite.
+ AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false });
+ // 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 });
+ AccessibilityUtils.resetEnv();
+
+ 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..de86a1ea7a
--- /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.startLoadingURIString(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.startLoadingURIString(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..e2ae309201
--- /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();
+ Assert.equal(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();
+ Assert.equal(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..bc53591f84
--- /dev/null
+++ b/dom/tests/browser/browser_localStorage_snapshotting.js
@@ -0,0 +1,774 @@
+const HELPER_PAGE_URL =
+ "https://example.com/browser/dom/tests/browser/page_localstorage_snapshotting.html";
+const HELPER_PAGE_ORIGIN = "https://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..05f96e96bf
--- /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.startLoadingURIString(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.startLoadingURIString(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..c3ade6ee99
--- /dev/null
+++ b/dom/tests/browser/browser_noopener.js
@@ -0,0 +1,176 @@
+"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 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..282ad22060
--- /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.startLoadingURIString(
+ 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..b4648a51ec
--- /dev/null
+++ b/dom/tests/browser/browser_persist_image_accept.js
@@ -0,0 +1,139 @@
+/* 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," : "") +
+ "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..8598969dc8
--- /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.startLoadingURIString(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.startLoadingURIString(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.startLoadingURIString(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.startLoadingURIString(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.startLoadingURIString(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.startLoadingURIString(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.startLoadingURIString(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..5496f4a43a
--- /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.startLoadingURIString(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..50bf9f34ab
--- /dev/null
+++ b/dom/tests/browser/browser_unlinkable_about_page_can_load_module_scripts.js
@@ -0,0 +1,84 @@
+/* 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"
+ );
+ });
+ }
+ );
+ Assert.greater(
+ 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..6b9e316968
--- /dev/null
+++ b/dom/tests/browser/browser_windowProxy_transplant.js
@@ -0,0 +1,219 @@
+"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.startLoadingURIString(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);
+
+ Assert.notEqual(
+ 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
new file mode 100644
index 0000000000..a1089af09b
--- /dev/null
+++ b/dom/tests/browser/dummy.png
Binary files differ
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..a3f0d0991a
--- /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.mjs"></script>
+</head>
+<body></body>
+</html>
diff --git a/dom/tests/browser/file_module_loaded.mjs b/dom/tests/browser/file_module_loaded.mjs
new file mode 100644
index 0000000000..9d5946cc1f
--- /dev/null
+++ b/dom/tests/browser/file_module_loaded.mjs
@@ -0,0 +1,9 @@
+// Left in this test to ensure it does not cause any issues.
+// eslint-disable-next-line strict
+"use strict";
+
+import setBodyText from "chrome://mochitests/content/browser/dom/tests/browser/file_module_loaded2.mjs";
+
+document.addEventListener("DOMContentLoaded", () => {
+ setBodyText();
+});
diff --git a/dom/tests/browser/file_module_loaded2.mjs b/dom/tests/browser/file_module_loaded2.mjs
new file mode 100644
index 0000000000..28b26cd16e
--- /dev/null
+++ b/dom/tests/browser/file_module_loaded2.mjs
@@ -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..2d131df1ab
--- /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.startLoadingURIString(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/position.html b/dom/tests/browser/position.html
new file mode 100644
index 0000000000..d5b665d8e5
--- /dev/null
+++ b/dom/tests/browser/position.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html lang="en" dir="ltr">
+<head>
+ <script type="text/javascript">
+ var result = null;
+
+ function handlePosition(position) {
+ result.innerHTML = position.coords.latitude + " " + position.coords.longitude;
+ }
+
+ function handleError(error) {
+ 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);
+};