+
+
+
diff --git a/layout/base/tests/border_radius_hit_testing_iframe.html b/layout/base/tests/border_radius_hit_testing_iframe.html
new file mode 100644
index 0000000000..a0f7ba1b92
--- /dev/null
+++ b/layout/base/tests/border_radius_hit_testing_iframe.html
@@ -0,0 +1,27 @@
+
+border-radius hit testing
+
+
+
+
diff --git a/layout/base/tests/browser.toml b/layout/base/tests/browser.toml
new file mode 100644
index 0000000000..a5b279145f
--- /dev/null
+++ b/layout/base/tests/browser.toml
@@ -0,0 +1,61 @@
+[DEFAULT]
+prefs = [
+ "layout.css.properties-and-values.enabled=true",
+]
+
+["browser_bug617076.js"]
+
+["browser_bug1701027-1.js"]
+support-files = ["helper_bug1701027-1.html"]
+
+["browser_bug1701027-2.js"]
+support-files = ["helper_bug1701027-2.html"]
+
+["browser_bug1757410.js"]
+run-if = [
+ "os == 'mac' && debug",
+ "os == 'win' && processor == 'x86_64' && debug"
+]
+
+["browser_bug1787079.js"]
+run-if = ["os == 'win' && processor == 'x86_64' && debug"]
+
+["browser_bug1791083.js"]
+skip-if = ["!sessionHistoryInParent"]
+
+["browser_css_registered_property.js"]
+
+["browser_disableDialogs_onbeforeunload.js"]
+
+["browser_onbeforeunload_only_after_interaction.js"]
+
+["browser_onbeforeunload_only_after_interaction_in_frame.js"]
+
+["browser_scroll_into_view_in_out_of_process_iframe.js"]
+support-files = [
+ "test_scroll_into_view_in_oopif.html",
+ "scroll_into_view_in_child.html"
+]
+
+["browser_select_popup_position_in_out_of_process_iframe.js"]
+skip-if = [
+ "verify && (os == 'mac')", # bug 1627874
+ "apple_silicon", # Disabled due to bleedover with other tests when run in regular suites; passes in "failures" jobs
+ "os == 'linux' && socketprocess_networking && fission && !debug",
+] # high frequency intermittent
+support-files = [
+ "!/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js",
+ "!/browser/base/content/test/forms/head.js"
+]
+
+["browser_stylesheet_change_events.js"]
+support-files = [
+ "file_stylesheet_change_events.html",
+ "stylesheet_change_events.css"
+]
+
+["browser_visual_viewport_iframe.js"]
+support-files = [
+ "test_visual_viewport_in_oopif.html",
+ "visual_viewport_in_child.html"
+]
diff --git a/layout/base/tests/browser_bug1701027-1.js b/layout/base/tests/browser_bug1701027-1.js
new file mode 100644
index 0000000000..7f35bd4f00
--- /dev/null
+++ b/layout/base/tests/browser_bug1701027-1.js
@@ -0,0 +1,136 @@
+/* This test is based on
+ https://searchfox.org/mozilla-central/rev/e082df56bbfeaff0f388e7da9da401ff414df18f/gfx/layers/apz/test/mochitest/browser_test_select_zoom.js
+*/
+
+// In order for this test to test the original bug we need:
+// 1) At least e10s enabled so that apz is enabled so we can create an
+// nsDisplayAsyncZoom item
+// (the insertion of this item without marking the required frame modified
+// is what causes the bug in the retained display list merging)
+// 2) a root content document, again so that we can create a nsDisplayAsyncZoom
+// item
+// 3) the root content document cannot have a display port to start
+// (if it has a display port then it gets a nsDisplayAsyncZoom, but we need
+// that to be created after the anonymous content we insert into the
+// document)
+// Point 3) requires the root content document to be in the parent process,
+// since if it is in a content process it will get a displayport for being at
+// the root of a process.
+// Creating an in-process root content document I think is not possible in
+// mochitest-plain. mochitest-chrome does not have e10s enabled. So this has to
+// be a mochitest-browser-chrome test.
+
+// Outline of this test:
+// Open a new tab with a pretty simple content file, that is not scrollable
+// Use the anonymous content api to insert into that content doc
+// Send a mouse click over the content doc
+// The click hits fixed pos content.
+// This sets a displayport on the root scroll frame of the content doc.
+// (This is because we call GetAsyncScrollableAncestorFrame in
+// PrepareForSetTargetAPZCNotification
+// https://searchfox.org/mozilla-central/rev/e082df56bbfeaff0f388e7da9da401ff414df18f/gfx/layers/apz/util/APZCCallbackHelper.cpp#624
+// which passes the SCROLLABLE_FIXEDPOS_FINDS_ROOT flag
+// https://searchfox.org/mozilla-central/rev/e082df56bbfeaff0f388e7da9da401ff414df18f/layout/base/nsLayoutUtils.cpp#2884
+// so starting from fixed pos content means we always find the root scroll
+// frame, whereas if we started from non-fixed content we'd walk pass the root
+// scroll frame becase it isn't scrollable.)
+// Then we have to be careful not to do anything that causes a full display
+// list rebuild.
+// And finally we change the color of the fixed element which covers the whole
+// viewport which causes us to do a partial display list update including the
+// anonymous content, which hits the assert we are aiming to test.
+
+add_task(async function () {
+ function getChromeURL(filename) {
+ let chromeURL = getRootDirectory(gTestPath) + filename;
+ return chromeURL;
+ }
+
+ // We need this otherwise there is a burst animation on the new tab when it
+ // loads and that somehow scrolls a scroll frame, which makes it active,
+ // which makes the scrolled frame an AGR, which means we have multiple AGRs
+ // (the display port makes the root scroll frame active and an AGR) so we hit
+ // this
+ // https://searchfox.org/mozilla-central/rev/e082df56bbfeaff0f388e7da9da401ff414df18f/layout/painting/RetainedDisplayListBuilder.cpp#1179
+ // and are forced to do a full display list rebuild and that prevents us from
+ // testing the original bug.
+ await SpecialPowers.pushPrefEnv({
+ set: [["ui.prefersReducedMotion", 1]],
+ });
+
+ const pageUrl = getChromeURL("helper_bug1701027-1.html");
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
+
+ const [theX, theY] = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ async () => {
+ content.document.body.offsetWidth;
+
+ await new Promise(r => content.window.requestAnimationFrame(r));
+
+ const rect = content.document
+ .getElementById("fd")
+ .getBoundingClientRect();
+ const x = content.window.mozInnerScreenX + rect.left + rect.width / 2;
+ const y = content.window.mozInnerScreenY + rect.top + rect.height / 2;
+
+ let doc = SpecialPowers.wrap(content.document);
+ var bq = doc.createElement("blockquote");
+ bq.textContent = "This blockquote text.";
+ var div = doc.createElement("div");
+ div.textContent = " This div text.";
+ bq.appendChild(div);
+ var ac = doc.insertAnonymousContent(bq);
+ content.document.body.offsetWidth;
+
+ await new Promise(r => content.window.requestAnimationFrame(r));
+ await new Promise(r => content.window.requestAnimationFrame(r));
+
+ return [x, y];
+ }
+ );
+
+ // We intentionally turn off a11y_checks, because the following click
+ // is targeting test content that's not meant to be interactive and
+ // is not expected to be accessible:
+ AccessibilityUtils.setEnv({
+ mustHaveAccessibleRule: false,
+ });
+ EventUtils.synthesizeNativeMouseEvent({
+ type: "click",
+ target: window.document.documentElement,
+ screenX: theX,
+ screenY: theY,
+ });
+
+ await new Promise(resolve => setTimeout(resolve, 0));
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ await new Promise(r => content.window.requestAnimationFrame(r));
+ await new Promise(r => content.window.requestAnimationFrame(r));
+ });
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ content.document.getElementById("fd").style.backgroundColor = "blue";
+ });
+
+ await new Promise(resolve => setTimeout(resolve, 0));
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ await new Promise(r => content.window.requestAnimationFrame(r));
+ await new Promise(r => content.window.requestAnimationFrame(r));
+ });
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ content.document.getElementById("fd").style.backgroundColor = "red";
+ });
+
+ await new Promise(resolve => setTimeout(resolve, 0));
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ await new Promise(r => content.window.requestAnimationFrame(r));
+ await new Promise(r => content.window.requestAnimationFrame(r));
+ });
+
+ BrowserTestUtils.removeTab(tab);
+
+ ok(true, "didn't crash");
+});
diff --git a/layout/base/tests/browser_bug1701027-2.js b/layout/base/tests/browser_bug1701027-2.js
new file mode 100644
index 0000000000..00e55ca562
--- /dev/null
+++ b/layout/base/tests/browser_bug1701027-2.js
@@ -0,0 +1,126 @@
+/* This test is based on
+ https://searchfox.org/mozilla-central/rev/e082df56bbfeaff0f388e7da9da401ff414df18f/gfx/layers/apz/test/mochitest/browser_test_select_zoom.js
+*/
+
+// In order for this test to test the original bug we need:
+// 1) At least e10s enabled so that apz is enabled so we can create an
+// nsDisplayAsyncZoom item
+// (the insertion of this item without marking the required frame modified
+// is what causes the bug in the retained display list merging)
+// 2) a root content document, again so that we can create a nsDisplayAsyncZoom
+// item
+// 3) the root content document cannot have a display port to start
+// (if it has a display port then it gets a nsDisplayAsyncZoom, but we need
+// that to be created after the anonymous content we insert into the
+// document)
+// Point 3) requires the root content document to be in the parent process,
+// since if it is in a content process it will get a displayport for being at
+// the root of a process.
+// Creating an in-process root content document I think is not possible in
+// mochitest-plain. mochitest-chrome does not have e10s enabled. So this has to
+// be a mochitest-browser-chrome test.
+
+// Outline of this test:
+// Open a new tab with a pretty simple content file, that is not scrollable
+// Use the anonymous content api to insert into that content doc
+// Set a displayport on the root scroll frame of the content doc directly.
+// Then we have to be careful not to do anything that causes a full display
+// list rebuild.
+// And finally we change the color of the fixed element which covers the whole
+// viewport which causes us to do a partial display list update including the
+// anonymous content, which hits the assert we are aiming to test.
+
+add_task(async function () {
+ function getChromeURL(filename) {
+ let chromeURL = getRootDirectory(gTestPath) + filename;
+ return chromeURL;
+ }
+
+ // We need this otherwise there is a burst animation on the new tab when it
+ // loads and that somehow scrolls a scroll frame, which makes it active,
+ // which makes the scrolled frame an AGR, which means we have multiple AGRs
+ // (the display port makes the root scroll frame active and an AGR) so we hit
+ // this
+ // https://searchfox.org/mozilla-central/rev/e082df56bbfeaff0f388e7da9da401ff414df18f/layout/painting/RetainedDisplayListBuilder.cpp#1179
+ // and are forced to do a full display list rebuild and that prevents us from
+ // testing the original bug.
+ await SpecialPowers.pushPrefEnv({
+ set: [["ui.prefersReducedMotion", 1]],
+ });
+
+ const pageUrl = getChromeURL("helper_bug1701027-2.html");
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
+
+ const [theX, theY] = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ async () => {
+ content.document.body.offsetWidth;
+
+ await new Promise(r => content.window.requestAnimationFrame(r));
+
+ const rect = content.document
+ .getElementById("fd")
+ .getBoundingClientRect();
+ const x = content.window.mozInnerScreenX + rect.left + rect.width / 2;
+ const y = content.window.mozInnerScreenY + rect.top + rect.height / 2;
+
+ let doc = SpecialPowers.wrap(content.document);
+ var bq = doc.createElement("blockquote");
+ bq.textContent = "This blockquote text.";
+ var div = doc.createElement("div");
+ div.textContent = " This div text.";
+ bq.appendChild(div);
+ var ac = doc.insertAnonymousContent(bq);
+ content.document.body.offsetWidth;
+
+ await new Promise(r => content.window.requestAnimationFrame(r));
+ await new Promise(r => content.window.requestAnimationFrame(r));
+
+ content.window.windowUtils.setDisplayPortMarginsForElement(
+ 0,
+ 0,
+ 0,
+ 0,
+ doc.documentElement,
+ 1
+ );
+ content.window.windowUtils.setDisplayPortBaseForElement(
+ 0,
+ 0,
+ 100,
+ 100,
+ doc.documentElement
+ );
+
+ await new Promise(r => content.window.requestAnimationFrame(r));
+ await new Promise(r => content.window.requestAnimationFrame(r));
+
+ return [x, y];
+ }
+ );
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ content.document.getElementById("fd").style.backgroundColor = "blue";
+ });
+
+ await new Promise(resolve => setTimeout(resolve, 0));
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ await new Promise(r => content.window.requestAnimationFrame(r));
+ await new Promise(r => content.window.requestAnimationFrame(r));
+ });
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ content.document.getElementById("fd").style.backgroundColor = "red";
+ });
+
+ await new Promise(resolve => setTimeout(resolve, 0));
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ await new Promise(r => content.window.requestAnimationFrame(r));
+ await new Promise(r => content.window.requestAnimationFrame(r));
+ });
+
+ BrowserTestUtils.removeTab(tab);
+
+ ok(true, "didn't crash");
+});
diff --git a/layout/base/tests/browser_bug1757410.js b/layout/base/tests/browser_bug1757410.js
new file mode 100644
index 0000000000..59c740e5a8
--- /dev/null
+++ b/layout/base/tests/browser_bug1757410.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const PAGECONTENT =
+ "" +
+ "" +
+ "" +
+ "";
+
+const pageUrl = "data:text/html," + encodeURIComponent(PAGECONTENT);
+
+add_task(async function test() {
+ if (window.devicePixelRatio == 1) {
+ ok(
+ true,
+ "Skip this test since this test is supposed to run on HiDPI mode, " +
+ "the devixePixelRato on this machine is " +
+ window.devicePixelRatio
+ );
+ return;
+ }
+
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
+
+ // Scroll the content a bit.
+ const originalScrollPosition = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ async () => {
+ content.document.scrollingElement.scrollTop = 100;
+ return content.document.scrollingElement.scrollTop;
+ }
+ );
+
+ // Disabling HiDPI mode and check the scroll position.
+ SpecialPowers.DOMWindowUtils.setHiDPIMode(false);
+ // Make sure we restore even if this test failed.
+ registerCleanupFunction(() => {
+ SpecialPowers.DOMWindowUtils.restoreHiDPIMode();
+ });
+
+ const scrollPosition = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ async () => {
+ return content.document.scrollingElement.scrollTop;
+ }
+ );
+ is(
+ originalScrollPosition,
+ scrollPosition,
+ "The scroll position should be kept"
+ );
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/layout/base/tests/browser_bug1787079.js b/layout/base/tests/browser_bug1787079.js
new file mode 100644
index 0000000000..2161b1f3dc
--- /dev/null
+++ b/layout/base/tests/browser_bug1787079.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const PAGECONTENT =
+ "" +
+ "" +
+ "" +
+ "";
+
+const pageUrl = "data:text/html," + encodeURIComponent(PAGECONTENT);
+
+add_task(async function test() {
+ SpecialPowers.DOMWindowUtils.setHiDPIMode(true);
+ registerCleanupFunction(() => {
+ SpecialPowers.DOMWindowUtils.restoreHiDPIMode();
+ });
+
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
+
+ // Enter fullscreen.
+ let fullscreenChangePromise = BrowserTestUtils.waitForContentEvent(
+ tab.linkedBrowser,
+ "fullscreenchange"
+ );
+ await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ content.document.documentElement.requestFullscreen();
+ });
+ await fullscreenChangePromise;
+
+ let [originalInnerWidth, originalInnerHeight] = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ () => {
+ return [content.window.innerWidth, content.window.innerHeight];
+ }
+ );
+
+ // Then change the DPI.
+ let originalPixelRatio = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ () => {
+ return content.window.devicePixelRatio;
+ }
+ );
+ let dpiChangedPromise = TestUtils.waitForCondition(async () => {
+ let pixelRatio = await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ return content.window.devicePixelRatio;
+ });
+ return pixelRatio != originalPixelRatio;
+ }, "Make sure the DPI changed");
+ SpecialPowers.DOMWindowUtils.setHiDPIMode(false);
+ await dpiChangedPromise;
+
+ let [innerWidth, innerHeight] = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ () => {
+ return [content.window.innerWidth, content.window.innerHeight];
+ }
+ );
+
+ Assert.less(
+ originalInnerWidth,
+ innerWidth,
+ "window.innerWidth on a lower DPI should be greater than the original"
+ );
+ Assert.less(
+ originalInnerHeight,
+ innerHeight,
+ "window.innerHeight on a lower DPI should be greater than the original"
+ );
+
+ fullscreenChangePromise = BrowserTestUtils.waitForContentEvent(
+ tab.linkedBrowser,
+ "fullscreenchange"
+ );
+ await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ content.document.exitFullscreen();
+ });
+ await fullscreenChangePromise;
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/layout/base/tests/browser_bug1791083.js b/layout/base/tests/browser_bug1791083.js
new file mode 100644
index 0000000000..e68a723c4e
--- /dev/null
+++ b/layout/base/tests/browser_bug1791083.js
@@ -0,0 +1,81 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const URL =
+ "data:text/html," +
+ "" +
+ "" +
+ "Click Me" +
+ "";
+
+function isAnchorHovered(win) {
+ return SpecialPowers.spawn(
+ win.gBrowser.selectedBrowser,
+ [],
+ async function () {
+ const a = content.document.querySelector("a");
+ return a.matches(":hover");
+ }
+ );
+}
+
+add_task(async function test() {
+ let newWin = await BrowserTestUtils.openNewBrowserWindow();
+
+ // This bug is only reproducible if the cursor is out of the viewport, so
+ // we resize the window to ensure the cursor is out of the viewport.
+
+ // SynthesizeMouse isn't sufficient because it only synthesizes
+ // mouse events without actually moving the cursor permanently to a
+ // new location.
+ newWin.resizeTo(50, 50);
+
+ BrowserTestUtils.startLoadingURIString(newWin.gBrowser.selectedBrowser, URL);
+ await BrowserTestUtils.browserLoaded(newWin.gBrowser.selectedBrowser);
+
+ await SpecialPowers.spawn(
+ newWin.gBrowser.selectedBrowser,
+ [],
+ async function () {
+ const a = content.document.querySelector("a");
+ await EventUtils.synthesizeMouseAtCenter(
+ a,
+ { type: "mousemove" },
+ content
+ );
+ }
+ );
+
+ // We've hovered the anchor element.
+ let anchorHovered = await isAnchorHovered(newWin);
+ ok(anchorHovered, "Anchor should be hovered");
+
+ let locationChange = BrowserTestUtils.waitForLocationChange(newWin.gBrowser);
+
+ // Click the anchor to navigate away
+ await SpecialPowers.spawn(
+ newWin.gBrowser.selectedBrowser,
+ [],
+ async function () {
+ const a = content.document.querySelector("a");
+ await EventUtils.synthesizeMouseAtCenter(
+ a,
+ { type: "mousedown" },
+ content
+ );
+ await EventUtils.synthesizeMouseAtCenter(a, { type: "mouseup" }, content);
+ }
+ );
+ await locationChange;
+
+ // Navigate back to the previous page which has the anchor
+ locationChange = BrowserTestUtils.waitForLocationChange(newWin.gBrowser);
+ newWin.gBrowser.selectedBrowser.goBack();
+ await locationChange;
+
+ // Hover state should be cleared upon page caching.
+ anchorHovered = await isAnchorHovered(newWin);
+ ok(!anchorHovered, "Anchor should not be hovered");
+
+ BrowserTestUtils.closeWindow(newWin);
+});
diff --git a/layout/base/tests/browser_bug617076.js b/layout/base/tests/browser_bug617076.js
new file mode 100644
index 0000000000..c76cbd41d3
--- /dev/null
+++ b/layout/base/tests/browser_bug617076.js
@@ -0,0 +1,71 @@
+/**
+ * 1. load about:addons in a new tab and select that tab
+ * 2. insert a button with tooltiptext
+ * 3. create a new blank tab and select that tab
+ * 4. select the about:addons tab and hover the inserted button
+ * 5. remove the about:addons tab
+ * 6. remove the blank tab
+ *
+ * the test succeeds if it doesn't trigger any assertions
+ */
+
+add_task(async function test() {
+ // Open the test tab
+ let testTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:addons"
+ );
+
+ // insert button into test page content
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ let doc = content.document;
+ let e = doc.createXULElement("button");
+ e.setAttribute("label", "hello");
+ e.setAttribute("tooltiptext", "world");
+ e.setAttribute("id", "test-button");
+ doc.documentElement.insertBefore(e, doc.documentElement.firstChild);
+ });
+
+ // open a second tab and select it
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank",
+ true
+ );
+ gBrowser.selectedTab = tab2;
+
+ // Select the testTab then perform mouse events on inserted button
+ gBrowser.selectedTab = testTab;
+ let browser = gBrowser.selectedBrowser;
+ EventUtils.disableNonTestMouseEvents(true);
+ try {
+ await BrowserTestUtils.synthesizeMouse(
+ "#test-button",
+ 1,
+ 1,
+ { type: "mouseover" },
+ browser
+ );
+ await BrowserTestUtils.synthesizeMouse(
+ "#test-button",
+ 2,
+ 6,
+ { type: "mousemove" },
+ browser
+ );
+ await BrowserTestUtils.synthesizeMouse(
+ "#test-button",
+ 2,
+ 4,
+ { type: "mousemove" },
+ browser
+ );
+ } finally {
+ EventUtils.disableNonTestMouseEvents(false);
+ }
+
+ // cleanup
+ BrowserTestUtils.removeTab(testTab);
+ BrowserTestUtils.removeTab(tab2);
+ ok(true, "pass if no assertions");
+});
diff --git a/layout/base/tests/browser_css_registered_property.js b/layout/base/tests/browser_css_registered_property.js
new file mode 100644
index 0000000000..3a8999d87c
--- /dev/null
+++ b/layout/base/tests/browser_css_registered_property.js
@@ -0,0 +1,93 @@
+"use strict";
+
+add_task(async function test() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "data:text/html," },
+ async function (browser) {
+ await SpecialPowers.spawn(browser, [], testRegisterProperty);
+ }
+ );
+});
+
+// This function runs entirely in the content process. It doesn't have access
+// any free variables in this file.
+async function testRegisterProperty() {
+ let doc = content.document;
+ doc.styleSheetChangeEventsEnabled = true;
+
+ const EVENT_NAME = "csscustompropertyregistered";
+
+ const unexpectedContentEvent = event =>
+ ok(false, "Received a " + event.type + " event on content");
+ doc.addEventListener(EVENT_NAME, unexpectedContentEvent);
+ doc.defaultView.addEventListener(EVENT_NAME, unexpectedContentEvent);
+ doc.addEventListener(EVENT_NAME, unexpectedContentEvent);
+ doc.defaultView.addEventListener(EVENT_NAME, unexpectedContentEvent);
+
+ function waitForCssCustomPropertyRegistered() {
+ return ContentTaskUtils.waitForEvent(
+ docShell.chromeEventHandler,
+ EVENT_NAME,
+ true
+ );
+ }
+
+ function checkCssCustomPropertyRegisteredEvent(
+ event,
+ expectedPropertyDefinition
+ ) {
+ is(event.type, EVENT_NAME, "event.type has expected value");
+ is(event.target, doc, "event targets correct document");
+ Assert.deepEqual(event.propertyDefinition, expectedPropertyDefinition);
+ }
+
+ let onCustomPropertyRegistered, evt;
+
+ info("Register property and wait for event");
+ onCustomPropertyRegistered = waitForCssCustomPropertyRegistered();
+ content.CSS.registerProperty({ name: "--a", syntax: "*", inherits: false });
+ evt = await onCustomPropertyRegistered;
+ ok(true, `Received ${EVENT_NAME} event after registering --a`);
+ checkCssCustomPropertyRegisteredEvent(evt, {
+ name: "--a",
+ syntax: "*",
+ inherits: false,
+ initialValue: null,
+ fromJS: true,
+ });
+
+ info("Register another property and wait for a new event");
+ onCustomPropertyRegistered = waitForCssCustomPropertyRegistered();
+ content.CSS.registerProperty({
+ name: "--b",
+ syntax: "",
+ inherits: true,
+ initialValue: "tomato",
+ });
+ evt = await onCustomPropertyRegistered;
+ ok(true, `Received ${EVENT_NAME} event after registering --b`);
+ checkCssCustomPropertyRegisteredEvent(evt, {
+ name: "--b",
+ syntax: "",
+ inherits: true,
+ initialValue: "tomato",
+ fromJS: true,
+ });
+
+ info("Register existing property and assert that we don't get an event");
+ onCustomPropertyRegistered = waitForCssCustomPropertyRegistered();
+ const timeout = new Promise(resolve =>
+ content.setTimeout(() => resolve("TIMEOUT"), 500)
+ );
+ try {
+ content.CSS.registerProperty({ name: "--b", syntax: "*", inherits: false });
+ } catch (e) {
+ ok(true, "CSS.registerProperty threw");
+ }
+ const res = await Promise.race([onCustomPropertyRegistered, timeout]);
+ is(
+ res,
+ "TIMEOUT",
+ `Did not receive ${EVENT_NAME} event when registration failed`
+ );
+}
diff --git a/layout/base/tests/browser_disableDialogs_onbeforeunload.js b/layout/base/tests/browser_disableDialogs_onbeforeunload.js
new file mode 100644
index 0000000000..f060d1db51
--- /dev/null
+++ b/layout/base/tests/browser_disableDialogs_onbeforeunload.js
@@ -0,0 +1,64 @@
+const { PromptTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromptTestUtils.sys.mjs"
+);
+
+function pageScript() {
+ window.addEventListener(
+ "beforeunload",
+ function (event) {
+ var str = "Some text that causes the beforeunload dialog to be shown";
+ event.returnValue = str;
+ return str;
+ },
+ true
+ );
+}
+
+SpecialPowers.pushPrefEnv({
+ set: [["dom.require_user_interaction_for_beforeunload", false]],
+});
+
+const PAGE_URL =
+ "data:text/html," +
+ encodeURIComponent("");
+
+add_task(async function enableDialogs() {
+ // The onbeforeunload dialog should appear.
+ let dialogPromise = PromptTestUtils.waitForPrompt(null, {
+ modalType: Services.prompt.MODAL_TYPE_CONTENT,
+ promptType: "confirmEx",
+ });
+
+ let openPagePromise = openPage(true);
+ let dialog = await dialogPromise;
+ Assert.ok(true, "Showed the beforeunload dialog.");
+
+ await PromptTestUtils.handlePrompt(dialog, { buttonNumClick: 0 });
+ await openPagePromise;
+});
+
+add_task(async function disableDialogs() {
+ // The onbeforeunload dialog should NOT appear.
+ await openPage(false);
+ info("If we time out here, then the dialog was shown...");
+});
+
+async function openPage(enableDialogs) {
+ // Open about:blank in a new tab.
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async function (browser) {
+ // Load the page.
+ BrowserTestUtils.startLoadingURIString(browser, PAGE_URL);
+ await BrowserTestUtils.browserLoaded(browser);
+ // Load the content script in the frame.
+ let methodName = enableDialogs ? "enableDialogs" : "disableDialogs";
+ await SpecialPowers.spawn(browser, [methodName], async function (name) {
+ content.windowUtils[name]();
+ });
+ // And then navigate away.
+ BrowserTestUtils.startLoadingURIString(browser, "http://example.com/");
+ await BrowserTestUtils.browserLoaded(browser);
+ }
+ );
+}
diff --git a/layout/base/tests/browser_onbeforeunload_only_after_interaction.js b/layout/base/tests/browser_onbeforeunload_only_after_interaction.js
new file mode 100644
index 0000000000..b0a577335b
--- /dev/null
+++ b/layout/base/tests/browser_onbeforeunload_only_after_interaction.js
@@ -0,0 +1,75 @@
+const { PromptTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromptTestUtils.sys.mjs"
+);
+
+function pageScript() {
+ window.addEventListener(
+ "beforeunload",
+ function (event) {
+ var str = "Some text that causes the beforeunload dialog to be shown";
+ event.returnValue = str;
+ return str;
+ },
+ true
+ );
+}
+
+SpecialPowers.pushPrefEnv({
+ set: [["dom.require_user_interaction_for_beforeunload", true]],
+});
+
+const PAGE_URL =
+ "data:text/html," +
+ encodeURIComponent("");
+
+add_task(async function doClick() {
+ // The onbeforeunload dialog should appear.
+ let dialogPromise = PromptTestUtils.waitForPrompt(null, {
+ modalType: Services.prompt.MODAL_TYPE_CONTENT,
+ promptType: "confirmEx",
+ });
+
+ let openPagePromise = openPage(true);
+ let dialog = await dialogPromise;
+ Assert.ok(true, "Showed the beforeunload dialog.");
+
+ await PromptTestUtils.handlePrompt(dialog, { buttonNumClick: 0 });
+ await openPagePromise;
+});
+
+add_task(async function noClick() {
+ // The onbeforeunload dialog should NOT appear.
+ await openPage(false);
+ info("If we time out here, then the dialog was shown...");
+});
+
+async function openPage(shouldClick) {
+ // Open about:blank in a new tab.
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async function (browser) {
+ // Load the page.
+ BrowserTestUtils.startLoadingURIString(browser, PAGE_URL);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ if (shouldClick) {
+ await BrowserTestUtils.synthesizeMouse("body", 2, 2, {}, browser);
+ }
+ let hasInteractedWith = await SpecialPowers.spawn(
+ browser,
+ [""],
+ function () {
+ return content.document.userHasInteracted;
+ }
+ );
+ is(
+ shouldClick,
+ hasInteractedWith,
+ "Click should update document interactivity state"
+ );
+ // And then navigate away.
+ BrowserTestUtils.startLoadingURIString(browser, "http://example.com/");
+ await BrowserTestUtils.browserLoaded(browser);
+ }
+ );
+}
diff --git a/layout/base/tests/browser_onbeforeunload_only_after_interaction_in_frame.js b/layout/base/tests/browser_onbeforeunload_only_after_interaction_in_frame.js
new file mode 100644
index 0000000000..bf9ab67254
--- /dev/null
+++ b/layout/base/tests/browser_onbeforeunload_only_after_interaction_in_frame.js
@@ -0,0 +1,96 @@
+const { PromptTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromptTestUtils.sys.mjs"
+);
+
+function pageScript() {
+ window.addEventListener(
+ "beforeunload",
+ function (event) {
+ var str = "Some text that causes the beforeunload dialog to be shown";
+ event.returnValue = str;
+ return str;
+ },
+ true
+ );
+}
+
+SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.require_user_interaction_for_beforeunload", true],
+ ["security.allow_eval_with_system_principal", true],
+ ],
+});
+
+const FRAME_URL =
+ "data:text/html," + encodeURIComponent("Just a frame");
+
+const PAGE_URL =
+ "data:text/html," +
+ encodeURIComponent(
+ ""
+ );
+
+add_task(async function doClick() {
+ // The onbeforeunload dialog should appear.
+ let dialogPromise = PromptTestUtils.waitForPrompt(null, {
+ modalType: Services.prompt.MODAL_TYPE_CONTENT,
+ promptType: "confirmEx",
+ });
+
+ let openPagePromise = openPage(true);
+ let dialog = await dialogPromise;
+ Assert.ok(true, "Showed the beforeunload dialog.");
+
+ await PromptTestUtils.handlePrompt(dialog, { buttonNumClick: 0 });
+ await openPagePromise;
+});
+
+add_task(async function noClick() {
+ // The onbeforeunload dialog should NOT appear.
+ await openPage(false);
+ info("If we time out here, then the dialog was shown...");
+});
+
+async function openPage(shouldClick) {
+ // Open about:blank in a new tab.
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async function (browser) {
+ // Load the page.
+ BrowserTestUtils.startLoadingURIString(browser, PAGE_URL);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ let frameBC = browser.browsingContext.children[0];
+ if (shouldClick) {
+ await BrowserTestUtils.synthesizeMouse("body", 2, 2, {}, frameBC);
+ }
+ let hasInteractedWith = await SpecialPowers.spawn(
+ frameBC,
+ [],
+ function () {
+ return [
+ content.document.userHasInteracted,
+ content.document.userHasInteracted,
+ ];
+ }
+ );
+ is(
+ shouldClick,
+ hasInteractedWith[0],
+ "Click should update parent interactivity state"
+ );
+ is(
+ shouldClick,
+ hasInteractedWith[1],
+ "Click should update frame interactivity state"
+ );
+ // And then navigate away.
+ BrowserTestUtils.startLoadingURIString(browser, "http://example.com/");
+ await BrowserTestUtils.browserLoaded(browser);
+ }
+ );
+}
diff --git a/layout/base/tests/browser_scroll_into_view_in_out_of_process_iframe.js b/layout/base/tests/browser_scroll_into_view_in_out_of_process_iframe.js
new file mode 100644
index 0000000000..07369feb4d
--- /dev/null
+++ b/layout/base/tests/browser_scroll_into_view_in_out_of_process_iframe.js
@@ -0,0 +1,51 @@
+"use strict";
+
+add_task(async () => {
+ function httpURL(filename, host = "https://example.com/") {
+ let root = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ host
+ );
+ return root + filename;
+ }
+
+ const fissionWindow = await BrowserTestUtils.openNewBrowserWindow({
+ fission: true,
+ });
+ const url = httpURL(
+ "test_scroll_into_view_in_oopif.html",
+ "http://mochi.test:8888/"
+ );
+ const crossOriginIframeUrl = httpURL("scroll_into_view_in_child.html");
+
+ try {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser: fissionWindow.gBrowser, url },
+ async browser => {
+ await SpecialPowers.spawn(
+ browser,
+ [crossOriginIframeUrl],
+ async iframeUrl => {
+ const iframe = content.document.getElementById("iframe");
+ iframe.setAttribute("src", iframeUrl);
+
+ // Wait for a scroll event since scrollIntoView for cross origin documents is
+ // asyncronously processed.
+ const scroller = content.document.getElementById("scroller");
+ await new Promise(resolve => {
+ scroller.addEventListener("scroll", resolve, { once: true });
+ });
+
+ Assert.greater(
+ scroller.scrollTop,
+ 0,
+ "scrollIntoView works in a cross origin iframe"
+ );
+ }
+ );
+ }
+ );
+ } finally {
+ await BrowserTestUtils.closeWindow(fissionWindow);
+ }
+});
diff --git a/layout/base/tests/browser_select_popup_position_in_out_of_process_iframe.js b/layout/base/tests/browser_select_popup_position_in_out_of_process_iframe.js
new file mode 100644
index 0000000000..2472a658db
--- /dev/null
+++ b/layout/base/tests/browser_select_popup_position_in_out_of_process_iframe.js
@@ -0,0 +1,125 @@
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js",
+ this
+);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/base/content/test/forms/head.js",
+ this
+);
+
+const PAGECONTENT_TRANSLATED =
+ "" +
+ "
" +
+ "
";
+
+function openSelectPopup(x, y, win) {
+ const popupShownPromise = BrowserTestUtils.waitForSelectPopupShown(win);
+ EventUtils.synthesizeNativeMouseEvent({
+ type: "click",
+ target: win.document.documentElement,
+ screenX: x,
+ screenY: y,
+ });
+ return popupShownPromise;
+}
+
+add_task(async function () {
+ const pageUrl = "data:text/html," + escape(PAGECONTENT_TRANSLATED);
+
+ const newWin = await BrowserTestUtils.openNewBrowserWindow({ fission: true });
+
+ const browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ newWin.gBrowser.selectedBrowser,
+ true /* includeSubFrames */
+ );
+ BrowserTestUtils.startLoadingURIString(
+ newWin.gBrowser.selectedBrowser,
+ pageUrl
+ );
+ await browserLoadedPromise;
+
+ newWin.gBrowser.selectedBrowser.focus();
+
+ const tab = newWin.gBrowser.selectedTab;
+
+ // We need to explicitly call Element.focus() since dataURL is treated as
+ // cross-origin, thus autofocus doesn't work there.
+ const iframeBC = await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ return content.document.querySelector("iframe").browsingContext;
+ });
+
+ const [iframeBorderLeft, iframeBorderTop, iframeX, iframeY] =
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ await SpecialPowers.contentTransformsReceived(content);
+ const iframe = content.document.querySelector("iframe");
+ const rect = iframe.getBoundingClientRect();
+ const x = content.window.mozInnerScreenX + rect.left;
+ const y = content.window.mozInnerScreenY + rect.top;
+ const cs = content.window.getComputedStyle(iframe);
+ return [parseInt(cs.borderLeftWidth), parseInt(cs.borderTopWidth), x, y];
+ });
+
+ const selectRect = await SpecialPowers.spawn(iframeBC, [], async function () {
+ await SpecialPowers.contentTransformsReceived(content);
+ const input = content.document.getElementById("select");
+ const focusPromise = new Promise(resolve => {
+ input.addEventListener("focus", resolve, { once: true });
+ });
+ input.focus();
+ await focusPromise;
+ return input.getBoundingClientRect();
+ });
+
+ // Open the select popup.
+ const selectPopup = await openSelectPopup(
+ iframeX + selectRect.x + selectRect.width / 2,
+ iframeY + selectRect.y + selectRect.height / 2,
+ newWin
+ );
+
+ // Check the coordinates of 'selectPopup'.
+ let popupRect = selectPopup.getBoundingClientRect();
+ is(
+ popupRect.x,
+ iframeX +
+ iframeBorderLeft +
+ selectRect.x -
+ newWin.mozInnerScreenX +
+ parseFloat(getComputedStyle(selectPopup).marginLeft),
+ "x position of the popup"
+ );
+
+ let expectedYPosition =
+ iframeY +
+ selectRect.y +
+ iframeBorderTop -
+ newWin.mozInnerScreenY +
+ parseFloat(getComputedStyle(selectPopup).marginTop);
+
+ // On platforms other than macOS the popup menu is positioned below the
+ // option element. On macOS the top is aligned to the selected item (so the
+ // first label).
+ if (navigator.platform.includes("Mac")) {
+ const offsetToSelectedItem =
+ selectPopup.querySelector("menuitem[selected]").getBoundingClientRect()
+ .top - popupRect.top;
+ expectedYPosition -= offsetToSelectedItem;
+ } else {
+ expectedYPosition += selectRect.height;
+ }
+
+ isfuzzy(
+ popupRect.y,
+ expectedYPosition,
+ window.devicePixelRatio,
+ "y position of the popup"
+ );
+
+ await hideSelectPopup("enter", newWin);
+
+ await BrowserTestUtils.closeWindow(newWin);
+});
diff --git a/layout/base/tests/browser_stylesheet_change_events.js b/layout/base/tests/browser_stylesheet_change_events.js
new file mode 100644
index 0000000000..c867197050
--- /dev/null
+++ b/layout/base/tests/browser_stylesheet_change_events.js
@@ -0,0 +1,227 @@
+const gTestRoot = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "http://127.0.0.1:8888/"
+);
+
+add_task(async function test() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: gTestRoot + "file_stylesheet_change_events.html" },
+ async function (browser) {
+ await SpecialPowers.spawn(
+ browser,
+ [gTestRoot],
+ testApplicableStateChangeEvent
+ );
+ }
+ );
+});
+
+// This function runs entirely in the content process. It doesn't have access
+// any free variables in this file.
+async function testApplicableStateChangeEvent(testRoot) {
+ // We've seen the original stylesheet in the document.
+ // Now add a stylesheet on the fly and make sure we see it.
+ let doc = content.document;
+ doc.styleSheetChangeEventsEnabled = true;
+
+ const unexpectedContentEvent = event =>
+ ok(false, "Received a " + event.type + " event on content");
+ doc.addEventListener(
+ "StyleSheetApplicableStateChanged",
+ unexpectedContentEvent
+ );
+ doc.defaultView.addEventListener(
+ "StyleSheetApplicableStateChanged",
+ unexpectedContentEvent
+ );
+ doc.addEventListener("StyleSheetRemoved", unexpectedContentEvent);
+ doc.defaultView.addEventListener("StyleSheetRemoved", unexpectedContentEvent);
+
+ function shouldIgnoreEvent(e) {
+ // accessiblecaret.css might be reported, interfering with the test
+ // assertions, so let's ignore it
+ return (
+ e.stylesheet?.href === "resource://content-accessible/accessiblecaret.css"
+ );
+ }
+
+ function waitForStyleApplicableStateChanged() {
+ return ContentTaskUtils.waitForEvent(
+ docShell.chromeEventHandler,
+ "StyleSheetApplicableStateChanged",
+ true,
+ e => !shouldIgnoreEvent(e)
+ );
+ }
+
+ function waitForStyleSheetRemovedEvent() {
+ return ContentTaskUtils.waitForEvent(
+ docShell.chromeEventHandler,
+ "StyleSheetRemoved",
+ true,
+ e => !shouldIgnoreEvent(e)
+ );
+ }
+
+ function checkApplicableStateChangeEvent(event, { applicable, stylesheet }) {
+ is(
+ event.type,
+ "StyleSheetApplicableStateChanged",
+ "event.type has expected value"
+ );
+ is(event.target, doc, "event targets correct document");
+ is(event.stylesheet, stylesheet, "event.stylesheet has the expected value");
+ is(event.applicable, applicable, "event.applicable has the expected value");
+ }
+
+ function checkStyleSheetRemovedEvent(event, { stylesheet }) {
+ is(event.type, "StyleSheetRemoved", "event.type has expected value");
+ is(event.target, doc, "event targets correct document");
+ is(event.stylesheet, stylesheet, "event.stylesheet has the expected value");
+ }
+
+ // Updating the text content will actually create a new StyleSheet instance,
+ // and so we should get one event for the new instance, and another one for
+ // the removal of the "previous"one.
+ function waitForTextContentChange() {
+ return Promise.all([
+ waitForStyleSheetRemovedEvent(),
+ waitForStyleApplicableStateChanged(),
+ ]);
+ }
+
+ let stateChanged, evt;
+
+ {
+ const gStyleSheet = "stylesheet_change_events.css";
+
+ info("Add and wait for applicable state change event");
+ let linkEl = doc.createElement("link");
+ linkEl.setAttribute("rel", "stylesheet");
+ linkEl.setAttribute("type", "text/css");
+ linkEl.setAttribute("href", testRoot + gStyleSheet);
+
+ stateChanged = waitForStyleApplicableStateChanged();
+ doc.body.appendChild(linkEl);
+ evt = await stateChanged;
+
+ ok(true, "received dynamic style sheet applicable state change event");
+ checkApplicableStateChangeEvent(evt, {
+ stylesheet: linkEl.sheet,
+ applicable: true,
+ });
+
+ stateChanged = waitForStyleApplicableStateChanged();
+ linkEl.sheet.disabled = true;
+ evt = await stateChanged;
+
+ ok(true, "received dynamic style sheet applicable state change event");
+ checkApplicableStateChangeEvent(evt, {
+ stylesheet: linkEl.sheet,
+ applicable: false,
+ });
+
+ info("Remove stylesheet and wait for removed event");
+ const removedStylesheet = linkEl.sheet;
+ const onStyleSheetRemoved = waitForStyleSheetRemovedEvent();
+ doc.body.removeChild(linkEl);
+ const removedStyleSheetEvt = await onStyleSheetRemoved;
+
+ ok(true, "received removed sheet event");
+ checkStyleSheetRemovedEvent(removedStyleSheetEvt, {
+ stylesheet: removedStylesheet,
+ });
+ }
+
+ {
+ info("Add `;
+ evt = await stateChanged;
+
+ ok(true, "received dynamic style sheet applicable state change event");
+ const shadowStyleEl = shadowRoot.querySelector("style");
+ checkApplicableStateChangeEvent(evt, {
+ stylesheet: shadowStyleEl.sheet,
+ applicable: true,
+ });
+
+ info("Updating
+
+
+
+ Mozilla Bug 1078327
+