Source code
Revision control
Copy as Markdown
Other Tools
<!DOCTYPE HTML>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>Test for async-scrolling an OOPIF and ensuring hit-testing still works</title>
  <script src="/tests/SimpleTest/SimpleTest.js"></script>
  <script src="/tests/SimpleTest/paint_listener.js"></script>
  <script src="helper_fission_utils.js"></script>
  <script src="apz_test_utils.js"></script>
  <script src="apz_test_native_event_utils.js"></script>
  <script>
    const { AppConstants } = SpecialPowers.ChromeUtils.importESModule(
      "resource://gre/modules/AppConstants.sys.mjs"
    );
    async function getWindowProtocol() {
      return await SpecialPowers.spawnChrome([], () => {
        try {
          return Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo)
            .windowProtocol;
        } catch {
          return "";
        }
      });
    }
    function getEventPromise(eventName) {
      let eventPromise = new Promise(resolve => {
        const listener = event => {
          if (event.data === eventName) {
            window.removeEventListener("message", listener);
            resolve();
          }
        }
        window.addEventListener("message", listener);
      });
      return eventPromise;
    }
    function getClickPromise() {
      let clickPromise = new Promise(resolve => {
        window.addEventListener("message", event => {
          let data = JSON.parse(event.data);
          if ("type" in data && data.type == "clicked") {
            let coordinates = {
              offsetX: data.coords.offsetX,
              offsetY: data.coords.offsetY
            };
            resolve(coordinates);
          }
        }, { once: true });
      });
      return clickPromise;
    }
    function getScrollYPromise() {
      let scrollPromise = new Promise(resolve => {
        window.addEventListener("message", event => {
          let eventData = JSON.parse(event.data);
          if ("type" in eventData && eventData.type == "scrolled") {
            resolve(eventData.scrollY);
          }
        }, { once: true });
      });
      return scrollPromise;
    }
    async function clickOnIframe(x, y) {
      let clickPromise = getClickPromise();
      await synthesizeNativeMouseEventWithAPZ(
        { type: "click", target: document.body, offsetX: x, offsetY: y },
        () => dump("Finished synthesizing click, waiting for OOPIF message...\n")
      );
      return clickPromise;
    }
    async function makeIframeScrollable(iframe) {
      let parentScrollListenerReady = getEventPromise("scrollableReady");
      let iframeScrollMaxY = await SpecialPowers.spawn(iframe, [], async () => {
        // Ensure the oopif is scrollable, and wait for the paint so that the
        // compositor also knows it's scrollable.
        content.document.body.style.height = "200vh";
        await content.wrappedJSObject.promiseApzFlushedRepaints();
        let scrollMaxY = content.window.scrollMaxY;
        // Also register a scroll listener for when it actually gets scrolled.
        content.window.addEventListener("scroll", function () {
          dump(`OOPIF got scroll event, now at ${content.window.scrollY}\n`);
          let data = JSON.stringify({
            type: "scrolled",
            scrollY: content.window.scrollY
          });
          content.window.parent.postMessage(data, "*");
        }, { once: true });
        content.window.parent.postMessage("scrollableReady", "*");
        return scrollMaxY;
      });
      await parentScrollListenerReady;
      return iframeScrollMaxY;
    }
    async function registerIframeClickListener(iframe) {
      let readyPromise = getEventPromise("listenerReady");
      let iframeClickReady = await SpecialPowers.spawn(iframe, [], async () => {
        content.document.addEventListener("click", (e) => {
          let data = JSON.stringify({
            type: "clicked",
            coords: {
              offsetX: e.clientX,
              offsetY: e.clientY
            }
          });
          content.window.parent.postMessage(data, "*");
        });
        content.window.parent.postMessage("listenerReady", "*");
        return true;
      });
      await readyPromise;
      return iframeClickReady;
    }
    /*------------------------------------------------------------------------*/
    async function test() {
      // Skip on Wayland
      const protocol = await getWindowProtocol();
      if (AppConstants.platform === "linux" && protocol === "wayland") {
        ok(true, "Skipping this test on Linux with Wayland");
        return;
      }
      ok(SpecialPowers.getBoolPref("apz.paint_skipping.enabled"),
        "Paint-skipping is expected to be enabled for this test to be meaningful");
      // Set up iframe
      let iframe = document.getElementById("testframe");
      await setupCrossOriginIFrame(iframe, "helper_fission_plain.html");
      // Register click listener in iframe
      let iframeClickReady = await registerIframeClickListener(iframe);
      ok(iframeClickReady, "Successfully installed click listener in OOP iframe");
      // hit-test into the iframe before scrolling
      let oldClickPoint = await clickOnIframe(50, 250);
      // Ensure parent window is at 0 scroll position before scrolling
      is(window.scrollY, 0, "window is at 0 scroll position");
      // do an APZ scroll and wait for the main-thread to get the repaint request,
      // and queue up a paint-skip scroll notification back to APZ.
      let scrollend = promiseScrollend(window);
      await promiseMoveMouseAndScrollWheelOver(document.body, 10, 10);
      await scrollend;
      await promiseOnlyApzControllerFlushed();
      await promiseAllPaintsDone();
      ok(window.scrollY > 5, "window has scrolled by " + window.scrollY + " pixels");
      // hit-test into the iframe after scrolling. The coordinates here are the
      // same relative to the body as before, but get computed to be different
      // relative to the window/screen.
      let newClickPoint = await clickOnIframe(50, 250);
      is(newClickPoint.x, oldClickPoint.x, "x-coord of old and new match");
      is(newClickPoint.y, oldClickPoint.y, "y-coord of old and new match");
      // Also check that we can send scroll events to the OOPIF. Any wheel events
      // delivered to this page after this point should result in a failure.
      addEventListener("wheel", function () {
        ok(false, "Got unwanted wheel event in parent document");
      });
      // Ensure iframe is at 0 scroll position
      const iframeY = await SpecialPowers.spawn(iframe, [], () => {
        return content.window.scrollY;
      });
      is(iframeY, 0, "scrollY of iframe should be 0 initially");
      // Ensure the OOPIF is scrollable.
      let iframeScrollMaxY = await makeIframeScrollable(iframe);
      ok(iframeScrollMaxY > 0, "iframe successfully made scrollable");
      // Now scroll over the OOP-iframe (we know it must be under the 50,250 point
      // because we just checked that above). Note that listening for wheel/scroll
      // events is trickier because they will fire in the OOPIF, so we can't just
      // use promiseMoveMouseAndScrollWheelOver directly.
      let scrolledPromise = getScrollYPromise();
      await synthesizeNativeWheel(document.body, 50, 250, 0, -10);
      let iframeScrollY = await scrolledPromise;
      ok(iframeScrollY > 0, "Confirmed that oopif is scrollable");
    }
    waitUntilApzStable()
      .then(test)
      .then(subtestDone, subtestFailed);
  </script>
</head>
<body>
  <iframe style="margin-top: 200px" id="testframe"></iframe>
  <div style="height: 5000px">tall div to make the page scrollable</div>
</body>
</html>