gfx/layers/apz/test/mochitest/helper_hittest_overscroll.html
author James Teh <jteh@mozilla.com>
Wed, 01 Feb 2023 05:02:01 +0000
changeset 651150 dd0fdd1daa69783be36acd5c50544f3694eaa8f9
parent 625109 fe18343ba5e606a59ffa7eb23578de6b27f902ac
permissions -rw-r--r--
Bug 1813980: Check IsDoc before Parent in RemoteAccessibleBase::ApplyCrossDocOffset. r=morgan We call this function on every ancestor when calculating bounds. RemoteParent() currently requires a hash lookup, so it's more efficient to early return for !IsDoc() first. This is a micro-optimisation, but it might have some impact given that we call this on every ancestor, especially when hit testing, where we call Bounds() a lot. As a bit of drive-by cleanup, use RemoteParent() rather than calling Parent() and IsRemote/AsRemote(). Differential Revision: https://phabricator.services.mozilla.com/D168346

<!DOCTYPE HTML>
<html>
<head>
  <title>Test APZ hit-testing while overscrolled</title>
  <script type="application/javascript" src="apz_test_utils.js"></script>
  <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
  <script src="/tests/SimpleTest/paint_listener.js"></script>
  <meta name="viewport" content="width=device-width"/>
  <style>
    html, body {
      margin: 0;
      padding: 0;
    }
    .spacer {
      height: 5000px;
    }
    #target {
      margin-left: 100px;
      margin-top: 2px;
      height: 4px;
      width: 4px;
      background: red;
    }
    #fixedtarget {
      left: 300px;
      height: 20px;
      width: 5px;
      top: 2px;
      background: green;
      position: fixed;
    }
    #fixedtargetbutton {
      height: 10px;
      width: 5px;
      padding: 0;
      margin-top: 10;
      margin-left: 0;
      border: 0;
    }
  </style>
</head>
<body>
 <div id="target"></div>
 <div id="fixedtarget">
   <button id="fixedtargetbutton">
   </button>
 </div>
 <div id="spacer" class="spacer"></div>
</body>
<script type="application/javascript">

// Some helper functions for listening for click events in the browser chrome.

// A handle used to interact with the chrome script used to implement
// [start|stop]ListeningFOrClickEventsInChrome().
let chromeScriptHandle = null;

function startListeningForClickEventsInChrome() {
  function chromeScript() {
    /* eslint-env mozilla/chrome-script */
    let topWin = Services.wm.getMostRecentWindow("navigator:browser");
    if (!topWin) {
      topWin = Services.wm.getMostRecentWindow("navigator:geckoview");
    }
    let chromeReceivedClick = false;
    function chromeListener(e) {
      chromeReceivedClick = true;
    }
    topWin.addEventListener("click", chromeListener);
    function queryClicked() {
      sendAsyncMessage("query-clicked-response", { chromeReceivedClick });
    }
    function cleanup() {
      topWin.removeEventListener("click", chromeListener);
      removeMessageListener("query-clicked", queryClicked);
      removeMessageListener("cleanup", cleanup);
    }
    addMessageListener("query-clicked", queryClicked);
    addMessageListener("cleanup", cleanup);
  }
  chromeScriptHandle = SpecialPowers.loadChromeScript(chromeScript);
}

async function didChromeReceiveClick() {
  chromeScriptHandle.sendAsyncMessage("query-clicked", null);
  let response = await chromeScriptHandle.promiseOneMessage("query-clicked-response");
  ok(response && ("chromeReceivedClick" in response),
     "Received a well-formed response from chrome script");
  return response.chromeReceivedClick;
}

function stopListeningForClickEventsInChrome() {
  chromeScriptHandle.sendAsyncMessage("cleanup", null);
  chromeScriptHandle.destroy();
}

async function test() {
  var config = getHitTestConfig();
  var utils = config.utils;

  // Overscroll the root scroll frame at the top, creating a gutter.
  // Note that the size of the gutter will only be 8px, because
  // setAsyncScrollOffset() applies the overscroll as a single delta,
  // and current APZ logic that transforms a delta into an overscroll
  // amount limits each delta to at most 8px.
  utils.setAsyncScrollOffset(document.documentElement, 0, -200);

  // Check that the event hits the root scroll frame in APZ.
  // This is important because additional pan-gesture events in the gutter
  // should continue to be handled and cause further overscroll (or
  // relieving overscroll, depending on their direction).
  let hitResult = hitTest({x: 100, y: 4});
  let rootViewId = utils.getViewId(document.documentElement);
  checkHitResult(hitResult,
                 APZHitResultFlags.VISIBLE,
                 rootViewId,
                 utils.getLayersId(),
                 "APZ hit-test in the root gutter");

  // Now, perform a click in the gutter and check that APZ prevents
  // the event from reaching Gecko.
  // To be sure that no event was dispatched to Gecko, install listeners
  // on both the browser chrome window and the content window.
  // This makes sure we catch the case where the overscroll transform causes
  // the event to incorrectly target the browser chrome.

  //// Util function to perform mouse events in the gutter. Used to ensure these
  //// events are not dispatched to the content.
  async function clickInGutter(xOffset, yOffset) {
    startListeningForClickEventsInChrome();
    let contentReceivedClick = false;
    let contentListener = function(e) {
      info("event clientX = " + e.clientX);
      info("event clientY = " + e.clientY);
      info("event target id: " + e.target.id);
      contentReceivedClick = true;
    };
    document.addEventListener("click", contentListener);
    await synthesizeNativeMouseEventWithAPZ({
      type: "click",
      target: window,
      offsetX: xOffset,
      offsetY: yOffset,
    });
    // Wait 10 frames for the event to maybe arrive, and if it
    // hasn't, assume it won't.
    for (let i = 0; i < 10; i++) {
      await promiseFrame();
    }
    info("Finished waiting around for click event");
    let chromeReceivedClick = await didChromeReceiveClick();
    ok(!chromeReceivedClick,
       "Gecko received click event in browser chrome when it shouldn't have");
    ok(!contentReceivedClick,
       "Gecko received click event targeting web content when it shouldn't have");
    stopListeningForClickEventsInChrome();
    document.removeEventListener("click", contentListener);
  }

  // Perform the test
  await clickInGutter(100, 4);

  // Finally, while still overscrolled, perform a click not in the gutter.
  // This event should successfully go through to the web content, and
  // be untransformed by the overscroll transform (such that it hits the
  // content that is visually under the cursor).
  let clickPromise = new Promise(resolve => {
    document.addEventListener("click", function(e) {
      info("event clientX = " + e.clientX);
      info("event clientY = " + e.clientY);
      is(e.target, target, "Click while overscrolled hit intended target");
      resolve();
    }, { once: true });
  });
  await synthesizeNativeMouseEventWithAPZ({
    type: "click",
    target: window,
    offsetX: 102,
    offsetY: 12,
  });
  await clickPromise;

  // Test that mouse events targetting the fixed content are dispatched
  // to the content.
  async function clickFixed(yOffset, expectedTarget) {
    let clickFixedPromise = new Promise(resolve => {
      document.addEventListener("click", function(e) {
        info("event clientX = " + e.clientX);
        info("event clientY = " + e.clientY);
        info("event target id: " + e.target.id);
        is(e.target, expectedTarget, "Click while overscrolled hit intended target");
        resolve();
      }, { once: true });
    });
    await synthesizeNativeMouseEventWithAPZ({
      type: "click",
      target: window,
      offsetX: 302,
      offsetY: yOffset,
    });
    await clickFixedPromise;
  }

  // This hit is technically in the gutter created by the overscroll, but we
  // should still dispatch to gecko due to the fixed element extending into
  // the gutter.
  await clickFixed(4, fixedtarget, false);
  // Perform various mouse events to ensure the fixed element has not moved
  await clickFixed(10, fixedtarget, false);
  await clickFixed(14, fixedtargetbutton, false);
  await clickFixed(18, fixedtargetbutton, false);

  let clickFixedPromise = new Promise(resolve => {
    document.addEventListener("click", function(e) {
      info("event clientX = " + e.clientX);
      info("event clientY = " + e.clientY);
      info("event target id: " + e.target.id);
      // TODO(dlrobertson): What exists directly below the fixed element?
      // Should there be a gutter below the fixed element? Or should events
      // directed below the fixed element be dispatched normally. In the
      // transform of the mouse event, the hit will not have any side bits
      // set and we  will transform the hit result. As a result, 25 will be
      // transformed to ~17, and the event will be dispatched to the fixed
      // element.
      todo(false,
           "Click while overscrolled hit intended target below fixed content");
      resolve();
    }, { once: true });
  });
  await synthesizeNativeMouseEventWithAPZ({
    type: "click",
    target: window,
    offsetX: 302,
    offsetY: 25,
  });
  await clickFixedPromise;

  // Click above the fixed element, but in the gutter.
  await clickInGutter(302, 1);
  // Click left of the the fixed element, but in the gutter.
  await clickInGutter(298, 4);
}

waitUntilApzStable()
.then(test)
.then(subtestDone, subtestFailed);

</script>
</html>