summaryrefslogtreecommitdiffstats
path: root/toolkit/content/tests/browser/browser_click_event_during_autoscrolling.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/content/tests/browser/browser_click_event_during_autoscrolling.js')
-rw-r--r--toolkit/content/tests/browser/browser_click_event_during_autoscrolling.js577
1 files changed, 577 insertions, 0 deletions
diff --git a/toolkit/content/tests/browser/browser_click_event_during_autoscrolling.js b/toolkit/content/tests/browser/browser_click_event_during_autoscrolling.js
new file mode 100644
index 0000000000..b47f72ed5b
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_click_event_during_autoscrolling.js
@@ -0,0 +1,577 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["general.autoScroll", true],
+ ["middlemouse.contentLoadURL", false],
+ ["test.events.async.enabled", false],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ "https://example.com/browser/toolkit/content/tests/browser/file_empty.html",
+ async function (browser) {
+ ok(browser.isRemoteBrowser, "This test passes only in e10s mode");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.body.innerHTML =
+ '<div style="height: 10000px;"></div>';
+ content.document.documentElement.scrollTop = 500;
+ content.document.documentElement.scrollTop; // Flush layout.
+ // Prevent to open context menu when testing the secondary button click.
+ content.window.addEventListener(
+ "contextmenu",
+ event => event.preventDefault(),
+ { capture: true }
+ );
+ });
+
+ function promiseFlushLayoutInContent() {
+ return SpecialPowers.spawn(browser, [], () => {
+ content.document.documentElement.scrollTop; // Flush layout in the remote content.
+ });
+ }
+
+ function promiseContentTick() {
+ return SpecialPowers.spawn(browser, [], async () => {
+ await new Promise(r => {
+ content.requestAnimationFrame(() => {
+ content.requestAnimationFrame(r);
+ });
+ });
+ });
+ }
+
+ let autoScroller;
+ function promiseWaitForAutoScrollerOpen() {
+ if (autoScroller?.state == "open") {
+ info("The autoscroller has already been open");
+ return Promise.resolve();
+ }
+ return BrowserTestUtils.waitForEvent(
+ window,
+ "popupshown",
+ { capture: true },
+ event => {
+ if (event.originalTarget.id != "autoscroller") {
+ return false;
+ }
+ autoScroller = event.originalTarget;
+ info('"popupshown" event is fired');
+ autoScroller.getBoundingClientRect(); // Flush layout of the autoscroller
+ return true;
+ }
+ );
+ }
+
+ function promiseWaitForAutoScrollerClosed() {
+ if (!autoScroller || autoScroller.state == "closed") {
+ info("The autoscroller has already been closed");
+ return Promise.resolve();
+ }
+ return BrowserTestUtils.waitForEvent(
+ autoScroller,
+ "popuphidden",
+ { capture: true },
+ () => {
+ info('"popuphidden" event is fired');
+ return true;
+ }
+ );
+ }
+
+ // Unfortunately, we cannot use synthesized mouse events for starting and
+ // stopping autoscrolling because they may run different path from user
+ // operation especially when there is a popup.
+
+ /**
+ * Instead of using `waitForContentEvent`, we use `addContentEventListener`
+ * for checking which events are fired because `waitForContentEvent` cannot
+ * detect redundant event since it's removed automatically at first event
+ * or timeout if the expected count is 0.
+ */
+ class ContentEventCounter {
+ constructor(aBrowser, aEventTypes) {
+ this.eventData = new Map();
+ for (let eventType of aEventTypes) {
+ const removeEventListener =
+ BrowserTestUtils.addContentEventListener(
+ aBrowser,
+ eventType,
+ () => {
+ let eventData = this.eventData.get(eventType);
+ eventData.count++;
+ },
+ { capture: true }
+ );
+ this.eventData.set(eventType, {
+ count: 0, // how many times the event fired.
+ removeEventListener, // function to remove the event listener.
+ });
+ }
+ }
+
+ getCountAndRemoveEventListener(aEventType) {
+ let eventData = this.eventData.get(aEventType);
+ if (eventData.removeEventListener) {
+ eventData.removeEventListener();
+ eventData.removeEventListener = null;
+ }
+ return eventData.count;
+ }
+
+ promiseMouseEvents(aEventTypes, aMessage) {
+ let needsToWait = [];
+ for (const eventType of aEventTypes) {
+ let eventData = this.eventData.get(eventType);
+ if (eventData.count > 0) {
+ info(`${aMessage}: Waiting "${eventType}" event in content...`);
+ needsToWait.push(
+ // Let's use `waitForCondition` here. "timeout" is not worthwhile
+ // to debug this test. We want clearer failure log.
+ TestUtils.waitForCondition(
+ () => eventData.count > 0,
+ `${aMessage}: "${eventType}" should be fired, but timed-out`
+ )
+ );
+ break;
+ }
+ }
+ return Promise.all(needsToWait);
+ }
+ }
+
+ await SpecialPowers.pushPrefEnv({ set: [["middlemouse.paste", true]] });
+ await (async function testMouseEventsAtStartingAutoScrolling() {
+ info(
+ "Waiting autoscroller popup for testing mouse events at starting autoscrolling"
+ );
+ await promiseFlushLayoutInContent();
+ let eventsInContent = new ContentEventCounter(browser, [
+ "click",
+ "auxclick",
+ "mousedown",
+ "mouseup",
+ "paste",
+ ]);
+ // Ensure that the event listeners added in the content with accessing
+ // the remote content.
+ await promiseFlushLayoutInContent();
+ await EventUtils.promiseNativeMouseEvent({
+ type: "mousemove",
+ target: browser,
+ atCenter: true,
+ });
+ const waitForOpenAutoScroll = promiseWaitForAutoScrollerOpen();
+ await EventUtils.promiseNativeMouseEvent({
+ type: "mousedown",
+ target: browser,
+ atCenter: true,
+ button: 1, // middle button
+ });
+ await waitForOpenAutoScroll;
+ // In the wild, native "mouseup" event occurs after the popup is open.
+ await EventUtils.promiseNativeMouseEvent({
+ type: "mouseup",
+ target: browser,
+ atCenter: true,
+ button: 1, // middle button
+ });
+ await promiseFlushLayoutInContent();
+ await promiseContentTick();
+ await eventsInContent.promiseMouseEvents(
+ ["mouseup"],
+ "At starting autoscrolling"
+ );
+ for (let eventType of ["click", "auxclick", "paste"]) {
+ is(
+ eventsInContent.getCountAndRemoveEventListener(eventType),
+ 0,
+ `"${eventType}" event shouldn't be fired in the content when a middle click starts autoscrolling`
+ );
+ }
+ for (let eventType of ["mousedown", "mouseup"]) {
+ is(
+ eventsInContent.getCountAndRemoveEventListener(eventType),
+ 1,
+ `"${eventType}" event should be fired in the content when a middle click starts autoscrolling`
+ );
+ }
+ info("Waiting autoscroller close for preparing the following tests");
+ let waitForAutoScrollEnd = promiseWaitForAutoScrollerClosed();
+ EventUtils.synthesizeKey("KEY_Escape");
+ await waitForAutoScrollEnd;
+ })();
+
+ if (
+ // Bug 1693240: We don't support setting modifiers while posting a mouse event on Windows.
+ !navigator.platform.includes("Win") &&
+ // Bug 1693237: We don't support setting modifiers on Android.
+ !navigator.appVersion.includes("Android") &&
+ // In Headless mode, modifiers are not supported by this kind of APIs.
+ !Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo).isHeadless
+ ) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["general.autoscroll.prevent_to_start.shiftKey", true],
+ ["general.autoscroll.prevent_to_start.altKey", true],
+ ["general.autoscroll.prevent_to_start.ctrlKey", true],
+ ["general.autoscroll.prevent_to_start.metaKey", true],
+ ],
+ });
+ for (const modifier of ["Shift", "Control", "Alt", "Meta"]) {
+ if (modifier == "Meta" && !navigator.platform.includes("Mac")) {
+ continue; // Delete this after fixing bug 1232918.
+ }
+ await (async function modifiersPreventToStartAutoScrolling() {
+ info(
+ `Waiting to check not to open autoscroller popup with middle button click with ${modifier}`
+ );
+ await promiseFlushLayoutInContent();
+ let eventsInContent = new ContentEventCounter(browser, [
+ "click",
+ "auxclick",
+ "mousedown",
+ "mouseup",
+ "paste",
+ ]);
+ // Ensure that the event listeners added in the content with accessing
+ // the remote content.
+ await promiseFlushLayoutInContent();
+ await EventUtils.promiseNativeMouseEvent({
+ type: "mousemove",
+ target: browser,
+ atCenter: true,
+ });
+ info(
+ `Waiting to MozAutoScrollNoStart event for the middle button click with ${modifier}`
+ );
+ await EventUtils.promiseNativeMouseEvent({
+ type: "mousedown",
+ target: browser,
+ atCenter: true,
+ button: 1, // middle button
+ modifiers: {
+ altKey: modifier == "Alt",
+ ctrlKey: modifier == "Control",
+ metaKey: modifier == "Meta",
+ shiftKey: modifier == "Shift",
+ },
+ });
+ try {
+ await TestUtils.waitForCondition(
+ () => autoScroller?.state == "open",
+ `Waiting to check not to open autoscroller popup with ${modifier}`,
+ 100,
+ 10
+ );
+ ok(
+ false,
+ `The autoscroller popup shouldn't be opened by middle click with ${modifier}`
+ );
+ } catch (ex) {
+ ok(
+ true,
+ `The autoscroller popup was not open as expected after middle click with ${modifier}`
+ );
+ }
+ // In the wild, native "mouseup" event occurs after the popup is open.
+ await EventUtils.promiseNativeMouseEvent({
+ type: "mouseup",
+ target: browser,
+ atCenter: true,
+ button: 1, // middle button
+ });
+ await promiseFlushLayoutInContent();
+ await promiseContentTick();
+ await eventsInContent.promiseMouseEvents(
+ ["paste"],
+ `At middle clicking with ${modifier}`
+ );
+ for (let eventType of [
+ "mousedown",
+ "mouseup",
+ "click",
+ "auxclick",
+ "paste",
+ ]) {
+ is(
+ eventsInContent.getCountAndRemoveEventListener(eventType),
+ 1,
+ `"${eventType}" event should be fired in the content when a middle click with ${modifier}`
+ );
+ }
+ info(
+ "Waiting autoscroller close for preparing the following tests"
+ );
+ })();
+ }
+ }
+
+ async function doTestMouseEventsAtStoppingAutoScrolling({
+ aButton = 0,
+ aClickOutsideAutoScroller = false,
+ aDescription = "Unspecified",
+ }) {
+ info(
+ `Starting autoscrolling for testing to stop autoscrolling with ${aDescription}`
+ );
+ await promiseFlushLayoutInContent();
+ await EventUtils.promiseNativeMouseEvent({
+ type: "mousemove",
+ target: browser,
+ atCenter: true,
+ });
+ const waitForOpenAutoScroll = promiseWaitForAutoScrollerOpen();
+ await EventUtils.promiseNativeMouseEvent({
+ type: "mousedown",
+ target: browser,
+ atCenter: true,
+ button: 1, // middle button
+ });
+ // In the wild, native "mouseup" event occurs after the popup is open.
+ await waitForOpenAutoScroll;
+ await EventUtils.promiseNativeMouseEvent({
+ type: "mouseup",
+ target: browser,
+ atCenter: true,
+ button: 1, // middle button
+ });
+ await promiseFlushLayoutInContent();
+ // Just to be sure, wait for a tick for wait APZ stable.
+ await TestUtils.waitForTick();
+
+ let eventsInContent = new ContentEventCounter(browser, [
+ "click",
+ "auxclick",
+ "mousedown",
+ "mouseup",
+ "paste",
+ "contextmenu",
+ ]);
+ // Ensure that the event listeners added in the content with accessing
+ // the remote content.
+ await promiseFlushLayoutInContent();
+
+ aDescription = `Stop autoscrolling with ${aDescription}`;
+ info(
+ `${aDescription}: Synthesizing primary mouse button event on the autoscroller`
+ );
+ const autoScrollerRect = autoScroller.getOuterScreenRect();
+ info(
+ `${aDescription}: autoScroller: { left: ${autoScrollerRect.left}, top: ${autoScrollerRect.top}, width: ${autoScrollerRect.width}, height: ${autoScrollerRect.height} }`
+ );
+ const waitForCloseAutoScroller = promiseWaitForAutoScrollerClosed();
+ if (aClickOutsideAutoScroller) {
+ info(
+ `${aDescription}: Synthesizing mousemove move cursor outside the autoscroller...`
+ );
+ await EventUtils.promiseNativeMouseEvent({
+ type: "mousemove",
+ target: autoScroller,
+ offsetX: -10,
+ offsetY: -10,
+ elementOnWidget: browser, // use widget for the parent window of the autoscroller
+ });
+ info(
+ `${aDescription}: Synthesizing mousedown to stop autoscrolling...`
+ );
+ await EventUtils.promiseNativeMouseEvent({
+ type: "mousedown",
+ target: autoScroller,
+ offsetX: -10,
+ offsetY: -10,
+ button: aButton,
+ elementOnWidget: browser, // use widget for the parent window of the autoscroller
+ });
+ } else {
+ info(
+ `${aDescription}: Synthesizing mousemove move cursor onto the autoscroller...`
+ );
+ await EventUtils.promiseNativeMouseEvent({
+ type: "mousemove",
+ target: autoScroller,
+ atCenter: true,
+ elementOnWidget: browser, // use widget for the parent window of the autoscroller
+ });
+ info(
+ `${aDescription}: Synthesizing mousedown to stop autoscrolling...`
+ );
+ await EventUtils.promiseNativeMouseEvent({
+ type: "mousedown",
+ target: autoScroller,
+ atCenter: true,
+ button: aButton,
+ elementOnWidget: browser, // use widget for the parent window of the autoscroller
+ });
+ }
+ // In the wild, native "mouseup" event occurs after the popup is closed.
+ await waitForCloseAutoScroller;
+ info(
+ `${aDescription}: Synthesizing mouseup event for preceding mousedown which is for stopping autoscrolling`
+ );
+ await EventUtils.promiseNativeMouseEvent({
+ type: "mouseup",
+ target: browser,
+ atCenter: true,
+ button: aButton,
+ });
+ await promiseFlushLayoutInContent();
+ await promiseContentTick();
+ await eventsInContent.promiseMouseEvents(
+ aButton != 2 ? ["mouseup"] : ["mouseup", "contextmenu"],
+ aDescription
+ );
+ is(
+ autoScroller.state,
+ "closed",
+ `${aDescription}: The autoscroller should've been closed`
+ );
+ // - On macOS, when clicking outside autoscroller, nsChildView
+ // intentionally blocks both "mousedown" and "mouseup" events in the
+ // case of the primary button click, and only "mousedown" for the
+ // middle button when the "mousedown". I'm not sure how it should work
+ // on macOS for conforming to the platform manner. Note that autoscroll
+ // isn't available on the other browsers on macOS. So, there is no
+ // reference, but for consistency between platforms, it may be better
+ // to ignore the platform manner.
+ // - On Windows, when clicking outside autoscroller, nsWindow
+ // intentionally blocks only "mousedown" events for the primary button
+ // and the middle button. But this behavior is different from Chrome
+ // so that we need to fix this in the future.
+ // - On Linux, when clicking outside autoscroller, nsWindow
+ // intentionally blocks only "mousedown" events for any buttons. But
+ // on Linux, autoscroll isn't available by the default settings. So,
+ // not so urgent, but should be fixed in the future for consistency
+ // between platforms and compatibility with Chrome on Windows.
+ const rollingUpPopupConsumeMouseDown =
+ aClickOutsideAutoScroller &&
+ (aButton != 2 || navigator.platform.includes("Linux"));
+ const rollingUpPopupConsumeMouseUp =
+ aClickOutsideAutoScroller &&
+ aButton == 0 &&
+ navigator.platform.includes("Mac");
+ const checkFuncForClick =
+ aClickOutsideAutoScroller &&
+ aButton == 2 &&
+ !navigator.platform.includes("Linux")
+ ? todo_is
+ : is;
+ for (let eventType of ["click", "auxclick"]) {
+ checkFuncForClick(
+ eventsInContent.getCountAndRemoveEventListener(eventType),
+ 0,
+ `${aDescription}: "${eventType}" event shouldn't be fired in the remote content`
+ );
+ }
+ is(
+ eventsInContent.getCountAndRemoveEventListener("paste"),
+ 0,
+ `${aDescription}: "paste" event shouldn't be fired in the remote content`
+ );
+ const checkFuncForMouseDown = rollingUpPopupConsumeMouseDown
+ ? todo_is
+ : is;
+ checkFuncForMouseDown(
+ eventsInContent.getCountAndRemoveEventListener("mousedown"),
+ 1,
+ `${aDescription}: "mousedown" event should be fired in the remote content`
+ );
+ const checkFuncForMouseUp = rollingUpPopupConsumeMouseUp ? todo_is : is;
+ checkFuncForMouseUp(
+ eventsInContent.getCountAndRemoveEventListener("mouseup"),
+ 1,
+ `${aDescription}: "mouseup" event should be fired in the remote content`
+ );
+ const checkFuncForContextMenu =
+ aButton == 2 &&
+ aClickOutsideAutoScroller &&
+ navigator.platform.includes("Linux")
+ ? todo_is
+ : is;
+ checkFuncForContextMenu(
+ eventsInContent.getCountAndRemoveEventListener("contextmenu"),
+ aButton == 2 ? 1 : 0,
+ `${aDescription}: "contextmenu" event should${
+ aButton != 2 ? " not" : ""
+ } be fired in the remote content`
+ );
+
+ const promiseClickEvent = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "click",
+ {
+ capture: true,
+ }
+ );
+ await promiseFlushLayoutInContent();
+ info(`${aDescription}: Waiting for click event in the remote content`);
+ EventUtils.synthesizeNativeMouseEvent({
+ type: "click",
+ target: browser,
+ atCenter: true,
+ });
+ await promiseClickEvent;
+ ok(
+ true,
+ `${aDescription}: click event is fired in the remote content after stopping autoscrolling`
+ );
+ }
+
+ // Clicking the primary button to stop autoscrolling.
+ await doTestMouseEventsAtStoppingAutoScrolling({
+ aButton: 0,
+ aClickOutsideAutoScroller: false,
+ aDescription: "a primary button click on autoscroller",
+ });
+ await doTestMouseEventsAtStoppingAutoScrolling({
+ aButton: 0,
+ aClickOutsideAutoScroller: true,
+ aDescription: "a primary button click outside autoscroller",
+ });
+
+ // Clicking the secondary button to stop autoscrolling.
+ await doTestMouseEventsAtStoppingAutoScrolling({
+ aButton: 2,
+ aClickOutsideAutoScroller: false,
+ aDescription: "a secondary button click on autoscroller",
+ });
+ await doTestMouseEventsAtStoppingAutoScrolling({
+ aButton: 2,
+ aClickOutsideAutoScroller: true,
+ aDescription: "a secondary button click outside autoscroller",
+ });
+
+ // Clicking the middle button to stop autoscrolling.
+ await SpecialPowers.pushPrefEnv({ set: [["middlemouse.paste", true]] });
+ await doTestMouseEventsAtStoppingAutoScrolling({
+ aButton: 1,
+ aClickOutsideAutoScroller: false,
+ aDescription:
+ "a middle button click on autoscroller (middle click paste enabled)",
+ });
+ await doTestMouseEventsAtStoppingAutoScrolling({
+ aButton: 1,
+ aClickOutsideAutoScroller: true,
+ aDescription:
+ "a middle button click outside autoscroller (middle click paste enabled)",
+ });
+ await SpecialPowers.pushPrefEnv({ set: [["middlemouse.paste", false]] });
+ await doTestMouseEventsAtStoppingAutoScrolling({
+ aButton: 1,
+ aClickOutsideAutoScroller: false,
+ aDescription:
+ "a middle button click on autoscroller (middle click paste disabled)",
+ });
+ await doTestMouseEventsAtStoppingAutoScrolling({
+ aButton: 1,
+ aClickOutsideAutoScroller: true,
+ aDescription:
+ "a middle button click outside autoscroller (middle click paste disabled)",
+ });
+ }
+ );
+});