<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=1173580
-->
<head>
  <title>Test for layerization</title>
  <script src="/tests/SimpleTest/SimpleTest.js"></script>
  <script src="/tests/SimpleTest/EventUtils.js"></script>
  <script src="/tests/SimpleTest/paint_listener.js"></script>
  <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
  <script type="application/javascript" src="apz_test_utils.js"></script>
  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
  <link rel="stylesheet" type="text/css" href="helper_subframe_style.css"/>
  <style>
  #container {
    display: flex;
    overflow: scroll;
    height: 500px;
  }
  .outer-frame {
    height: 500px;
    overflow: scroll;
    flex-basis: 100%;
    background: repeating-linear-gradient(#CCC, #CCC 100px, #BBB 100px, #BBB 200px);
  }
  #container-content {
    height: 200%;
  }
  </style>
</head>
<body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1173580">APZ layerization tests</a>
<p id="display"></p>
<div id="container">
  <div id="outer1" class="outer-frame">
    <div id="inner1" class="inner-frame">
      <div class="inner-content"></div>
    </div>
  </div>
  <div id="outer2" class="outer-frame">
    <div id="inner2" class="inner-frame">
      <div class="inner-content"></div>
    </div>
  </div>
  <iframe id="outer3" class="outer-frame" src="helper_iframe1.html"></iframe>
  <iframe id="outer4" class="outer-frame" src="helper_iframe2.html"></iframe>
<!-- The container-content div ensures 'container' is scrollable, so the
     optimization that layerizes the primary async-scrollable frame on page
     load layerizes it rather than its child subframes. -->
  <div id="container-content"></div>
</div>
<pre id="test">
<script type="application/javascript">

// Scroll the mouse wheel over |element|.
async function scrollWheelOver(element) {
  await promiseMoveMouseAndScrollWheelOver(element, 10, 10, /* waitForScroll = */ false);
}

const DISPLAYPORT_EXPIRY = 100;

let config = getHitTestConfig();
let activateAllScrollFrames = config.activateAllScrollFrames;

let heightMultiplier = SpecialPowers.getCharPref("apz.y_stationary_size_multiplier");
// With WebRender, the effective height multiplier can be reduced
// for alignment reasons. The reduction should be no more than a
// factor of two.
heightMultiplier /= 2;
info("effective displayport height multipler is " + heightMultiplier);

function hasNonZeroMarginDisplayPort(elementId, containingDoc = null) {
  let dp = getLastContentDisplayportFor(elementId);
  if (dp == null) {
    return false;
  }
  let elt = (containingDoc != null ? containingDoc : document).getElementById(elementId);
  info(elementId);
  info("window size " + window.innerWidth + " " + window.innerHeight);
  info("dp " + dp.x + " " + dp.y + " " + dp.width + " " + dp.height);
  info("eltsize " + elt.clientWidth + " " + elt.clientHeight);
  return dp.height >= heightMultiplier * Math.min(elt.clientHeight, window.innerHeight);
}

function hasMinimalDisplayPort(elementId, containingDoc = null) {
  let dp = getLastContentDisplayportFor(elementId);
  if (dp == null) {
    return false;
  }
  let elt = (containingDoc != null ? containingDoc : document).getElementById(elementId);
  info(elementId);
  info("dp " + dp.x + " " + dp.y + " " + dp.width + " " + dp.height);
  info("eltsize " + elt.clientWidth + " " + elt.clientHeight);
  return dp.width <= (elt.clientWidth + 2) && dp.height <= (elt.clientHeight + 2);
}

function checkDirectActivation(elementId, containingDoc = null) {
  if (activateAllScrollFrames) {
    return hasNonZeroMarginDisplayPort(elementId, containingDoc);
  }
    return isLayerized(elementId);

}

function checkAncestorActivation(elementId, containingDoc = null) {
  if (activateAllScrollFrames) {
    return hasMinimalDisplayPort(elementId, containingDoc);
  }
    return isLayerized(elementId);

}

function checkInactive(elementId, containingDoc = null) {
  if (activateAllScrollFrames) {
    return hasMinimalDisplayPort(elementId, containingDoc);
  }
    return !isLayerized(elementId);

}

async function test() {
  await SpecialPowers.pushPrefEnv({
    "set": [
       // Causes the test to intermittently fail on ASAN opt linux.
       ["mousewheel.system_scroll_override.enabled", false],
    ]
  });

  let outer3Doc = document.getElementById("outer3").contentDocument;
  let outer4Doc = document.getElementById("outer4").contentDocument;

  // Initially, everything should be inactive.
  ok(checkInactive("outer1"), "initially 'outer1' should not be active");
  ok(checkInactive("inner1"), "initially 'inner1' should not be active");
  ok(checkInactive("outer2"), "initially 'outer2' should not be active");
  ok(checkInactive("inner2"), "initially 'inner2' should not be active");
  ok(checkInactive("outer3"), "initially 'outer3' should not be active");
  ok(checkInactive("inner3", outer3Doc),
    "initially 'inner3' should not be active");
  ok(checkInactive("outer4"), "initially 'outer4' should not be active");
  ok(checkInactive("inner4", outer4Doc),
    "initially 'inner4' should not be active");

  // Scrolling over outer1 should activate outer1 directly, but not inner1.
  await scrollWheelOver(document.getElementById("outer1"));
  await promiseAllPaintsDone();
  await promiseOnlyApzControllerFlushed();
  ok(checkDirectActivation("outer1"),
    "scrolling 'outer1' should activate it directly");
  ok(checkInactive("inner1"),
    "scrolling 'outer1' should not cause 'inner1' to get activated");

  // Scrolling over inner2 should activate inner2 directly, but outer2 only ancestrally.
  await scrollWheelOver(document.getElementById("inner2"));
  await promiseAllPaintsDone();
  await promiseOnlyApzControllerFlushed();
  ok(checkDirectActivation("inner2"),
    "scrolling 'inner2' should cause it to be directly activated");
  ok(checkAncestorActivation("outer2"),
    "scrolling 'inner2' should cause 'outer2' to be activated as an ancestor");

  // The second half of the test repeats the same checks as the first half,
  // but with an iframe as the outer scrollable frame.

  // Scrolling over outer3 should activate outer3 directly, but not inner3.
  await scrollWheelOver(outer3Doc.documentElement);
  await promiseAllPaintsDone();
  await promiseOnlyApzControllerFlushed();
  ok(checkDirectActivation("outer3"), "scrolling 'outer3' should cause it to be directly activated");
  ok(checkInactive("inner3", outer3Doc),
    "scrolling 'outer3' should not cause 'inner3' to be activated");

  // Scrolling over inner4 should activate inner4 directly, but outer4 only ancestrally.
  await scrollWheelOver(outer4Doc.getElementById("inner4"));
  await promiseAllPaintsDone();
  await promiseOnlyApzControllerFlushed();
  ok(checkDirectActivation("inner4", outer4Doc),
    "scrolling 'inner4' should cause it to be directly activated");
  ok(checkAncestorActivation("outer4"),
    "scrolling 'inner4' should cause 'outer4' to be activated");

  // Now we enable displayport expiry, and verify that things are still
  // activated as they were before.
  await SpecialPowers.pushPrefEnv({"set": [["apz.displayport_expiry_ms", DISPLAYPORT_EXPIRY]]});
  ok(checkDirectActivation("outer1"), "outer1 still has non zero display port after enabling expiry");
  ok(checkInactive("inner1"), "inner1 is still has zero margin display port after enabling expiry");
  ok(checkAncestorActivation("outer2"), "outer2 still has zero margin display port after enabling expiry");
  ok(checkDirectActivation("inner2"), "inner2 still has non zero display port after enabling expiry");
  ok(checkDirectActivation("outer3"), "outer3 still has non zero display port after enabling expiry");
  ok(checkInactive("inner3", outer3Doc),
    "inner3 still has zero margin display port after enabling expiry");
  ok(checkDirectActivation("inner4", outer4Doc),
    "inner4 still has non zero display port after enabling expiry");
  ok(checkAncestorActivation("outer4"), "outer4 still has zero margin display port after enabling expiry");

  // Now we trigger a scroll on some of the things still layerized, so that
  // the displayport expiry gets triggered.

  // Expire displayport with scrolling on outer1
  await scrollWheelOver(document.getElementById("outer1"));
  await promiseAllPaintsDone();
  await promiseOnlyApzControllerFlushed();
  await SpecialPowers.promiseTimeout(DISPLAYPORT_EXPIRY);
  await promiseAllPaintsDone();
  ok(checkInactive("outer1"), "outer1 is inactive after displayport expiry");
  ok(checkInactive("inner1"), "inner1 is inactive after displayport expiry");

  // Expire displayport with scrolling on inner2
  await scrollWheelOver(document.getElementById("inner2"));
  await promiseAllPaintsDone();
  await promiseOnlyApzControllerFlushed();
  // Once the expiry elapses, it will trigger expiry on outer2, so we check
  // both, one at a time.
  await SpecialPowers.promiseTimeout(DISPLAYPORT_EXPIRY);
  await promiseAllPaintsDone();
  ok(checkInactive("inner2"), "inner2 is inactive after displayport expiry");
  await SpecialPowers.promiseTimeout(DISPLAYPORT_EXPIRY);
  await promiseAllPaintsDone();
  ok(checkInactive("outer2"), "outer2 is inactive with inner2");

  // We need to wrap the next bit in a loop and keep retrying until it
  // succeeds. Let me explain why this is the best option at this time. Below
  // we scroll over inner3, this triggers a 100 ms timer to expire it's display
  // port. Then when it expires it schedules a paint and triggers another
  // 100 ms timer on it's parent, outer3, to expire. The paint needs to happen
  // before the timer fires because the paint is what updates
  // mIsParentToActiveScrollFrames on outer3, and mIsParentToActiveScrollFrames
  // being true blocks a display port from expiring. It was true because it
  // contained inner3, but no longer. In real life the timer is 15000 ms so a
  // paint will happen, but here in a test the timer is 100 ms so that paint
  // can not happen in time. We could add some more complication to this code
  // just for this test, or we could just loop here.
  let itWorked = false;
  while (!itWorked) {
    // Scroll on inner3. inner3 isn't layerized, and this will cause it to
    // get layerized, but it will also trigger displayport expiration for inner3
    // which will eventually trigger displayport expiration on inner3 and outer3.
    // Note that the displayport expiration might actually happen before the wheel
    // input is processed in the compositor (see bug 1246480 comment 3), and so
    // we make sure not to wait for a scroll event here, since it may never fire.
    // However, if we do get a scroll event while waiting for the expiry, we need
    // to restart the expiry timer because the displayport expiry got reset. There's
    // no good way that I can think of to deterministically avoid doing this.
    let inner3 = outer3Doc.getElementById("inner3");
    await scrollWheelOver(inner3);
    await promiseAllPaintsDone();
    await promiseOnlyApzControllerFlushed();
    let timerPromise = new Promise(resolve => {
      var timeoutTarget = function() {
        inner3.removeEventListener("scroll", timeoutResetter);
        resolve();
      };
      var timerId = setTimeout(timeoutTarget, DISPLAYPORT_EXPIRY);
      var timeoutResetter = function() {
        ok(true, "Got a scroll event; resetting timer...");
        clearTimeout(timerId);
        setTimeout(timeoutTarget, DISPLAYPORT_EXPIRY);
        // by not updating timerId we ensure that this listener resets the timeout
        // at most once.
      };
      inner3.addEventListener("scroll", timeoutResetter);
    });
    await timerPromise; // wait for the setTimeout to elapse

    await promiseAllPaintsDone();
    ok(checkInactive("inner3", outer3Doc),
      "inner3 is inactive after expiry");
    await SpecialPowers.promiseTimeout(DISPLAYPORT_EXPIRY);
    await promiseAllPaintsDone();
    if (checkInactive("outer3")) {
      ok(true, "outer3 is inactive after inner3 triggered expiry");
      itWorked = true;
    }
  }

  // Scroll outer4 and wait for the expiry. It should NOT get expired because
  // inner4 is still layerized
  await scrollWheelOver(outer4Doc.documentElement);
  await promiseAllPaintsDone();
  await promiseOnlyApzControllerFlushed();
  // Wait for the expiry to elapse
  await SpecialPowers.promiseTimeout(DISPLAYPORT_EXPIRY);
  await promiseAllPaintsDone();
  ok(checkDirectActivation("inner4", outer4Doc),
    "inner4 still is directly activated because it never expired");
  ok(checkDirectActivation("outer4"),
    "outer4 still still is directly activated because inner4 is still layerized");
}

if (isApzEnabled()) {
  SimpleTest.waitForExplicitFinish();
  SimpleTest.requestFlakyTimeout("we are testing code that measures an actual timeout");
  SimpleTest.expectAssertions(0, 8); // we get a bunch of "ASSERTION: Bounds computation mismatch" sometimes (bug 1232856)

  // Disable smooth scrolling, because it results in long-running scroll
  // animations that can result in a 'scroll' event triggered by an earlier
  // wheel event as corresponding to a later wheel event.
  // Also enable APZ test logging, since we use that data to determine whether
  // a scroll frame was layerized.
  pushPrefs([["general.smoothScroll", false],
             ["apz.displayport_expiry_ms", 0],
             ["apz.test.logging_enabled", true]])
  .then(waitUntilApzStable)
  .then(test)
  .then(SimpleTest.finish, SimpleTest.finishWithFailure);
}

</script>
</pre>
</body>
</html>