<!doctype html>
<head>
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
<meta charset=utf-8>
<title>Tests restyles caused by animations</title>
<script>
const ok = opener.ok.bind(opener);
const is = opener.is.bind(opener);
const todo = opener.todo.bind(opener);
const todo_is = opener.todo_is.bind(opener);
const info = opener.info.bind(opener);
const original_finish = opener.SimpleTest.finish;
const SimpleTest = opener.SimpleTest;
const add_task = opener.add_task;
SimpleTest.finish = function finish() {
  self.close();
  original_finish();
}
</script>
<script src="/tests/SimpleTest/EventUtils.js"></script>
<script src="/tests/SimpleTest/paint_listener.js"></script>
<script src="../testcommon.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
<style>
@keyframes background-position {
  0% {
    background-position: -25px center;
  }

  40%,
  100% {
    background-position: 36px center;
  }
}
@keyframes opacity {
  from { opacity: 1; }
  to { opacity: 0; }
}
@keyframes opacity-without-end-value {
  from { opacity: 0; }
}
@keyframes on-main-thread {
  from { z-index: 0; }
  to { z-index: 999; }
}
@keyframes rotate {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}
@keyframes move-in {
  from { transform: translate(120%, 120%); }
  to { transform: translate(0%, 0%); }
}
@keyframes background-color {
  from { background-color: rgb(255, 0, 0,); }
  to { background-color: rgb(0, 255, 0,); }
}
div {
  /* Element needs geometry to be eligible for layerization */
  width: 100px;
  height: 100px;
  background-color: white;
}
progress:not(.stop)::-moz-progress-bar {
  animation: on-main-thread 100s;
}
body {
  /*
   * set overflow:hidden to avoid accidentally unthrottling animations to update
   * the overflow region.
   */
  overflow: hidden;
}
</style>
</head>
<body>
<script>
'use strict';

// Returns observed animation restyle markers when |funcToMakeRestyleHappen|
// is called.
// NOTE: This function is synchronous version of the above observeStyling().
// Unlike the above observeStyling, this function takes a callback function,
// |funcToMakeRestyleHappen|, which may be expected to trigger a synchronous
// restyles, and returns any restyle markers produced by calling that function.
function observeAnimSyncStyling(funcToMakeRestyleHappen) {
  const docShell = getDocShellForObservingRestylesForWindow(window);

  funcToMakeRestyleHappen();

  const markers = docShell.popProfileTimelineMarkers();
  docShell.recordProfileTimelineMarkers = false;
  return Array.prototype.filter.call(markers, (marker, index) => {
    return marker.name == 'Styles' && marker.isAnimationOnly;
  });
}

function ensureElementRemoval(aElement) {
  return new Promise(resolve => {
    aElement.remove();
    waitForAllPaintsFlushed(resolve);
  });
}

function waitForWheelEvent(aTarget) {
  return new Promise(resolve => {
    // Get the scrollable target element position in this window coordinate
    // system to send a wheel event to the element.
    const targetRect = aTarget.getBoundingClientRect();
    const centerX = targetRect.left + targetRect.width / 2;
    const centerY = targetRect.top + targetRect.height / 2;

    sendWheelAndPaintNoFlush(aTarget, centerX, centerY,
                             { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
                               deltaY: targetRect.height },
                             resolve);
  });
}

const omtaEnabled = isOMTAEnabled();

function add_task_if_omta_enabled(test) {
  if (!omtaEnabled) {
    info(test.name + " is skipped because OMTA is disabled");
    return;
  }
  add_task(test);
}

// We need to wait for all paints before running tests to avoid contaminations
// from styling of this document itself.
waitForAllPaints(() => {
  add_task(async function () {
    // Start vsync rate measurement in after a RAF callback.
    await waitForNextFrame();

    const timeAtStart = document.timeline.currentTime;
    await waitForAnimationFrames(5);
    const vsyncRate = (document.timeline.currentTime - timeAtStart) / 5;

    // In this test we basically observe restyling counts in 5 frames, if it
    // takes over 200ms during the 5 frames, this test will fail. So
    // "200ms / 5 = 40ms" is a threshold whether the test works as expected or
    // not. We'd take 5ms additional tolerance here.
    // Note that the 200ms is a period we unthrottle throttled animations that
    // at least one of the animating styles produces change hints causing
    // overflow, the value is defined in
    // KeyframeEffect::OverflowRegionRefreshInterval.
    if (vsyncRate > 40 - 5) {
      ok(true, `the vsync rate ${vsyncRate} on this machine is too slow to run this test`);
      SimpleTest.finish();
    }
  });

  add_task(async function restyling_for_main_thread_animations() {
    const div = addDiv(null, { style: 'animation: on-main-thread 100s' });
    const animation = div.getAnimations()[0];

    await waitForAnimationReadyToRestyle(animation);

    ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);

    const markers = await observeStyling(5);
    is(markers.length, 5,
       'CSS animations running on the main-thread should update style ' +
       'on the main thread');
    await ensureElementRemoval(div);
  });

  add_task(async function restyling_for_main_thread_animations_progress_bar_pseudo() {
    const progress = document.createElement("progress");
    document.body.appendChild(progress);

    await waitForNextFrame();
    await waitForNextFrame();

    let markers = await observeStyling(5);
    is(markers.length, 5,
       'CSS animations running on the main-thread should update style ' +
       'on the main thread on ::-moz-progress-bar');
    progress.classList.add("stop");
    await waitForNextFrame();
    await waitForNextFrame();

    markers = await observeStyling(5);
    is(markers.length, 0, 'Animation is correctly removed');
    await ensureElementRemoval(progress);
  });

  add_task_if_omta_enabled(async function no_restyling_for_compositor_animations() {
    const div = addDiv(null, { style: 'animation: opacity 100s' });
    const animation = div.getAnimations()[0];

    await waitForAnimationReadyToRestyle(animation);
    ok(SpecialPowers.wrap(animation).isRunningOnCompositor);

    const markers = await observeStyling(5);
    is(markers.length, 0,
       'CSS animations running on the compositor should not update style ' +
       'on the main thread');
    await ensureElementRemoval(div);
  });

  add_task_if_omta_enabled(async function no_restyling_for_compositor_transitions() {
    const div = addDiv(null, { style: 'transition: opacity 100s; opacity: 0' });
    getComputedStyle(div).opacity;
    div.style.opacity = 1;

    const animation = div.getAnimations()[0];

    await waitForAnimationReadyToRestyle(animation);
    ok(SpecialPowers.wrap(animation).isRunningOnCompositor);

    const markers = await observeStyling(5);
    is(markers.length, 0,
       'CSS transitions running on the compositor should not update style ' +
       'on the main thread');
    await ensureElementRemoval(div);
  });

  add_task_if_omta_enabled(async function no_restyling_when_animation_duration_is_changed() {
    const div = addDiv(null, { style: 'animation: opacity 100s' });
    const animation = div.getAnimations()[0];

    await waitForAnimationReadyToRestyle(animation);
    ok(SpecialPowers.wrap(animation).isRunningOnCompositor);

    div.animationDuration = '200s';

    const markers = await observeStyling(5);
    is(markers.length, 0,
       'Animations running on the compositor should not update style ' +
       'on the main thread');
    await ensureElementRemoval(div);
  });

  add_task_if_omta_enabled(async function only_one_restyling_after_finish_is_called() {
    const div = addDiv(null, { style: 'animation: opacity 100s' });
    const animation = div.getAnimations()[0];

    await waitForAnimationReadyToRestyle(animation);
    ok(SpecialPowers.wrap(animation).isRunningOnCompositor);

    animation.finish();

    let markers = await observeStyling(1);
    is(markers.length, 1,
       'Animations running on the compositor should only update style once ' +
       'after finish() is called');

    markers = await observeStyling(1);
    todo_is(markers.length, 0,
            'Bug 1415457: Animations running on the compositor should only ' +
            'update style once after finish() is called');

    markers = await observeStyling(5);
    is(markers.length, 0,
       'Finished animations should never update style after one ' +
       'restyle happened for finish()');

    await ensureElementRemoval(div);
  });

  add_task(async function no_restyling_mouse_movement_on_finished_transition() {
    const div = addDiv(null, { style: 'transition: opacity 1ms; opacity: 0' });
    getComputedStyle(div).opacity;
    div.style.opacity = 1;

    const animation = div.getAnimations()[0];
    const initialRect = div.getBoundingClientRect();

    await animation.finished;
    let markers = await observeStyling(1);
    is(markers.length, 1,
       'Finished transitions should restyle once after Animation.finished ' +
       'was fulfilled');

    let mouseX = initialRect.left + initialRect.width / 2;
    let mouseY = initialRect.top + initialRect.height / 2;
    markers = await observeStyling(5, () => {
      // We can't use synthesizeMouse here since synthesizeMouse causes
      // layout flush.
      synthesizeMouseAtPoint(mouseX++, mouseY++,
                             { type: 'mousemove' }, window);
    });

    is(markers.length, 0,
       'Finished transitions should never cause restyles when mouse is moved ' +
       'on the transitions');
    await ensureElementRemoval(div);
  });

  add_task(async function no_restyling_mouse_movement_on_finished_animation() {
    const div = addDiv(null, { style: 'animation: opacity 1ms' });
    const animation = div.getAnimations()[0];

    const initialRect = div.getBoundingClientRect();

    await animation.finished;
    let markers = await observeStyling(1);
    is(markers.length, 1,
       'Finished animations should restyle once after Animation.finished ' +
       'was fulfilled');

    let mouseX = initialRect.left + initialRect.width / 2;
    let mouseY = initialRect.top + initialRect.height / 2;
    markers = await observeStyling(5, () => {
      // We can't use synthesizeMouse here since synthesizeMouse causes
      // layout flush.
      synthesizeMouseAtPoint(mouseX++, mouseY++,
                             { type: 'mousemove' }, window);
    });

    is(markers.length, 0,
       'Finished animations should never cause restyles when mouse is moved ' +
       'on the animations');
    await ensureElementRemoval(div);
  });

  add_task_if_omta_enabled(async function no_restyling_compositor_animations_out_of_view_element() {
    const div = addDiv(null,
      { style: 'animation: opacity 100s; transform: translateY(-400px);' });
    const animation = div.getAnimations()[0];

    await waitForAnimationReadyToRestyle(animation);
    ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);

    const markers = await observeStyling(5);

    is(markers.length, 0,
       'Animations running on the compositor in an out-of-view element ' +
       'should never cause restyles');
    await ensureElementRemoval(div);
  });

  add_task(async function no_restyling_main_thread_animations_out_of_view_element() {
    const div = addDiv(null,
      { style: 'animation: on-main-thread 100s; transform: translateY(-400px);' });
    const animation = div.getAnimations()[0];

    await waitForAnimationReadyToRestyle(animation);
    const markers = await observeStyling(5);

    is(markers.length, 0,
       'Animations running on the main-thread in an out-of-view element ' +
       'should never cause restyles');
    await ensureElementRemoval(div);
  });

  add_task_if_omta_enabled(async function no_restyling_compositor_animations_in_scrolled_out_element() {
    const parentElement = addDiv(null,
      { style: 'overflow-y: scroll; height: 20px;' });
    const div = addDiv(null,
      { style: 'animation: opacity 100s; position: relative; top: 100px;' });
    parentElement.appendChild(div);
    const animation = div.getAnimations()[0];

    await waitForAnimationReadyToRestyle(animation);

    const markers = await observeStyling(5);

    is(markers.length, 0,
       'Animations running on the compositor for elements ' +
       'which are scrolled out should never cause restyles');

    await ensureElementRemoval(parentElement);
  });

  add_task(
    async function no_restyling_missing_keyframe_opacity_animations_on_scrolled_out_element() {
      const parentElement = addDiv(null,
        { style: 'overflow-y: scroll; height: 20px;' });
      const div = addDiv(null,
        { style: 'animation: opacity-without-end-value 100s; ' +
                 'position: relative; top: 100px;' });
      parentElement.appendChild(div);
      const animation = div.getAnimations()[0];
      await waitForAnimationReadyToRestyle(animation);

      const markers = await observeStyling(5);

      is(markers.length, 0,
         'Opacity animations on scrolled out elements should never cause ' +
         'restyles even if the animation has missing keyframes');

      await ensureElementRemoval(parentElement);
    }
  );

  add_task(
    async function restyling_transform_animations_in_scrolled_out_element() {
      // Make sure we start from the state right after requestAnimationFrame.
      await waitForFrame();

      const parentElement = addDiv(null,
        { style: 'overflow-y: scroll; height: 20px;' });
      const div = addDiv(null,
        { style: 'animation: rotate 100s infinite; position: relative; top: 100px;' });
      parentElement.appendChild(div);
      const animation = div.getAnimations()[0];
      let timeAtStart = document.timeline.currentTime;

      ok(!animation.isRunningOnCompositor,
         'The transform animation is not running on the compositor');

      let markers;
      let now;
      let elapsed;
      while (true) {
        now = document.timeline.currentTime;
        elapsed = (now - timeAtStart);
        markers = await observeStyling(1);
        if (markers.length) {
          break;
        }
      }
      // If the current time has elapsed over 200ms since the animation was
      // created, it means that the animation should have already
      // unthrottled in this tick, let's see what we observe in this tick's
      // restyling process.
      // We use toPrecision here and below so 199.99999999999977 will turn into 200.
      ok(elapsed.toPrecision(10) >= 200,
           'Transform animation running on the element which is scrolled out ' +
           'should be throttled until 200ms is elapsed. now: ' +
            now + ' start time: ' + timeAtStart + ' elapsed:' + elapsed);

      timeAtStart = document.timeline.currentTime;
      markers = await observeStyling(1);
      now = document.timeline.currentTime;
      elapsed = (now - timeAtStart);

      let expectedMarkersLengthValid;
      // On the fence of 200 ms, we probably have 1 marker; but if we hit a bad rounding
      // we might still have 0. But if it's > 200, we should have 1; and less we should have 0.
      if (elapsed.toPrecision(10) == 200)
        expectedMarkersLengthValid = markers.length < 2;
      else if (elapsed.toPrecision(10) > 200)
        expectedMarkersLengthValid = markers.length == 1;
      else
        expectedMarkersLengthValid = !markers.length;
      ok(expectedMarkersLengthValid,
         'Transform animation running on the element which is scrolled out ' +
         'should be unthrottled after around 200ms have elapsed. now: ' +
         now + ' start time: ' + timeAtStart + ' elapsed: ' + elapsed);

      await ensureElementRemoval(parentElement);
    }
  );

  add_task(
    async function restyling_out_of_view_transform_animations_in_another_element() {
      // Make sure we start from the state right after requestAnimationFrame.
      await waitForFrame();

      const parentElement = addDiv(null,
        { style: 'overflow: hidden;' });
      const div = addDiv(null,
        { style: 'animation: move-in 100s infinite;' });
      parentElement.appendChild(div);
      const animation = div.getAnimations()[0];
      let timeAtStart = document.timeline.currentTime;

      ok(!animation.isRunningOnCompositor,
         'The transform animation on out of view element ' +
         'is not running on the compositor');

      // Structure copied from restyling_transform_animations_in_scrolled_out_element
      let markers;
      let now;
      let elapsed;
      while (true) {
        now = document.timeline.currentTime;
        elapsed = (now - timeAtStart);
        markers = await observeStyling(1);
        if (markers.length) {
          break;
        }
      }

      ok(elapsed.toPrecision(10) >= 200,
         'Transform animation running on out of view element ' +
         'should be throttled until 200ms is elapsed. now: ' +
            now + ' start time: ' + timeAtStart + ' elapsed:' + elapsed);

      timeAtStart = document.timeline.currentTime;
      markers = await observeStyling(1);
      now = document.timeline.currentTime;
      elapsed = (now - timeAtStart);

      let expectedMarkersLengthValid;
      // On the fence of 200 ms, we probably have 1 marker; but if we hit a bad rounding
      // we might still have 0. But if it's > 200, we should have 1; and less we should have 0.
      if (elapsed.toPrecision(10) == 200)
        expectedMarkersLengthValid = markers.length < 2;
      else if (elapsed.toPrecision(10) > 200)
        expectedMarkersLengthValid = markers.length == 1;
      else
        expectedMarkersLengthValid = !markers.length;
      ok(expectedMarkersLengthValid,
         'Transform animation running on out of view element ' +
         'should be unthrottled after around 200ms have elapsed. now: ' +
         now + ' start time: ' + timeAtStart +  ' elapsed: ' + elapsed);

      await ensureElementRemoval(parentElement);
    }
  );

  add_task(async function finite_transform_animations_in_out_of_view_element() {
    const parentElement = addDiv(null, { style: 'overflow: hidden;' });
    const div = addDiv(null);
    const animation =
      div.animate({ transform: [ 'translateX(120%)', 'translateX(100%)' ] },
                                // This animation will move a bit but
                                // will remain out-of-view.
                  100 * MS_PER_SEC);
    parentElement.appendChild(div);

    await waitForAnimationReadyToRestyle(animation);
    ok(!SpecialPowers.wrap(animation).isRunningOnCompositor, "Should not be running in compositor");

    const markers = await observeStyling(20);
    is(markers.length, 20,
       'Finite transform animation in out-of-view element should never be ' +
       'throttled');

    await ensureElementRemoval(parentElement);
  });

  add_task(async function restyling_main_thread_animations_in_scrolled_out_element() {
    const parentElement = addDiv(null,
      { style: 'overflow-y: scroll; height: 20px;' });
    const div = addDiv(null,
      { style: 'animation: on-main-thread 100s; position: relative; top: 20px;' });
    parentElement.appendChild(div);
    const animation = div.getAnimations()[0];

    await waitForAnimationReadyToRestyle(animation);
    let markers = await observeStyling(5);

    is(markers.length, 0,
       'Animations running on the main-thread for elements ' +
       'which are scrolled out should never cause restyles');

    await waitForWheelEvent(parentElement);

    // Make sure we are ready to restyle before counting restyles.
    await waitForFrame();

    markers = await observeStyling(5);
    is(markers.length, 5,
       'Animations running on the main-thread which were in scrolled out ' +
       'elements should update restyling soon after the element moved in ' +
       'view by scrolling');

    await ensureElementRemoval(parentElement);
  });

  add_task(async function restyling_main_thread_animations_in_nested_scrolled_out_element() {
    const grandParent = addDiv(null,
      { style: 'overflow-y: scroll; height: 20px;' });
    const parentElement = addDiv(null,
      { style: 'overflow-y: scroll; height: 100px;' });
    const div = addDiv(null,
      { style: 'animation: on-main-thread 100s; ' +
               'position: relative; ' +
               'top: 20px;' }); // This element is in-view in the parent, but
                                // out of view in the grandparent.
    grandParent.appendChild(parentElement);
    parentElement.appendChild(div);
    const animation = div.getAnimations()[0];

    await waitForAnimationReadyToRestyle(animation);
    let markers = await observeStyling(5);

    is(markers.length, 0,
       'Animations running on the main-thread which are in nested elements ' +
       'which are scrolled out should never cause restyles');

    await waitForWheelEvent(grandParent);

    await waitForFrame();

    markers = await observeStyling(5);
    is(markers.length, 5,
       'Animations running on the main-thread which were in nested scrolled ' +
       'out elements should update restyle soon after the element moved ' +
       'in view by scrolling');

    await ensureElementRemoval(grandParent);
  });

  add_task_if_omta_enabled(async function no_restyling_compositor_animations_in_visibility_hidden_element() {
    const div = addDiv(null,
     { style: 'animation: opacity 100s; visibility: hidden' });
    const animation = div.getAnimations()[0];

    await waitForAnimationReadyToRestyle(animation);
    ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);

    const markers = await observeStyling(5);

    is(markers.length, 0,
       'Animations running on the compositor in visibility hidden element ' +
       'should never cause restyles');
    await ensureElementRemoval(div);
  });

  add_task(async function restyling_main_thread_animations_move_out_of_view_by_scrolling() {
    const parentElement = addDiv(null,
      { style: 'overflow-y: scroll; height: 200px;' });
    const div = addDiv(null,
      { style: 'animation: on-main-thread 100s;' });
    const pad = addDiv(null,
      { style: 'height: 400px;' });
    parentElement.appendChild(div);
    parentElement.appendChild(pad);
    const animation = div.getAnimations()[0];

    await waitForAnimationReadyToRestyle(animation);

    await waitForWheelEvent(parentElement);

    await waitForFrame();

    const markers = await observeStyling(5);

    // FIXME: We should reduce a redundant restyle here.
    ok(markers.length >= 0,
       'Animations running on the main-thread which are in scrolled out ' +
       'elements should throttle restyling');

    await ensureElementRemoval(parentElement);
  });

  add_task(async function restyling_main_thread_animations_moved_in_view_by_resizing() {
    const parentElement = addDiv(null,
      { style: 'overflow-y: scroll; height: 20px;' });
    const div = addDiv(null,
      { style: 'animation: on-main-thread 100s; position: relative; top: 100px;' });
    parentElement.appendChild(div);
    const animation = div.getAnimations()[0];

    await waitForAnimationReadyToRestyle(animation);

    let markers = await observeStyling(5);
    is(markers.length, 0,
       'Animations running on the main-thread which is in scrolled out ' +
       'elements should not update restyling');

    parentElement.style.height = '100px';
    markers = await observeStyling(1);

    is(markers.length, 1,
       'Animations running on the main-thread which was in scrolled out ' +
       'elements should update restyling soon after the element moved in ' +
       'view by resizing');

    await ensureElementRemoval(parentElement);
  });

  add_task(
    async function restyling_animations_on_visibility_changed_element_having_child() {
      const div = addDiv(null,
       { style: 'animation: on-main-thread 100s;' });
      const childElement = addDiv(null);
      div.appendChild(childElement);

      const animation = div.getAnimations()[0];

      await waitForAnimationReadyToRestyle(animation);

      // We don't check the animation causes restyles here since we already
      // check it in the first test case.

      div.style.visibility = 'hidden';
      await waitForNextFrame();

      const markers = await observeStyling(5);
      todo_is(markers.length, 0,
              'Animations running on visibility hidden element which ' +
              'has a child whose visiblity is inherited from the element and ' +
              'the element was initially visible');

      await ensureElementRemoval(div);
    }
  );

  add_task(
    async function restyling_animations_on_visibility_hidden_element_which_gets_visible() {
      const div = addDiv(null,
       { style: 'animation: on-main-thread 100s; visibility: hidden' });
      const animation = div.getAnimations()[0];


      await waitForAnimationReadyToRestyle(animation);
      let markers = await observeStyling(5);

      is(markers.length, 0,
         'Animations running on visibility hidden element should never ' +
         'cause restyles');

      div.style.visibility = 'visible';
      await waitForNextFrame();

       markers = await observeStyling(5);
      is(markers.length, 5,
         'Animations running that was on visibility hidden element which ' +
         'gets visible should not throttle restyling any more');

      await ensureElementRemoval(div);
    }
  );

  add_task(async function restyling_animations_in_visibility_changed_parent() {
    const parentDiv = addDiv(null, { style: 'visibility: hidden' });
    const div = addDiv(null, { style: 'animation: on-main-thread 100s;' });
    parentDiv.appendChild(div);

    const animation = div.getAnimations()[0];

    await waitForAnimationReadyToRestyle(animation);
    let markers = await observeStyling(5);

    is(markers.length, 0,
       'Animations running in visibility hidden parent should never cause ' +
       'restyles');

    parentDiv.style.visibility = 'visible';
    await waitForNextFrame();

    markers = await observeStyling(5);
    is(markers.length, 5,
       'Animations that was in visibility hidden parent should not ' +
       'throttle restyling any more');

    parentDiv.style.visibility = 'hidden';
    await waitForNextFrame();

    markers = await observeStyling(5);
    is(markers.length, 0,
       'Animations that the parent element became visible should throttle ' +
       'restyling again');

    await ensureElementRemoval(parentDiv);
  });

  add_task(
    async function restyling_animations_on_visibility_hidden_element_with_visibility_changed_children() {
      const div = addDiv(null,
       { style: 'animation: on-main-thread 100s; visibility: hidden' });
      const animation = div.getAnimations()[0];

      await waitForAnimationReadyToRestyle(animation);
      let markers = await observeStyling(5);

      is(markers.length, 0,
         'Animations on visibility hidden element having no visible children ' +
         'should never cause restyles');

      const childElement = addDiv(null, { style: 'visibility: visible' });
      div.appendChild(childElement);
      await waitForNextFrame();

      markers = await observeStyling(5);
      is(markers.length, 5,
         'Animations running on visibility hidden element but the element has ' +
         'a visible child should not throttle restyling');

      childElement.style.visibility = 'hidden';
      await waitForNextFrame();

      markers = await observeStyling(5);
      todo_is(markers.length, 0,
              'Animations running on visibility hidden element that a child ' +
              'has become invisible should throttle restyling');

      childElement.style.visibility = 'visible';
      await waitForNextFrame();

      markers = await observeStyling(5);
      is(markers.length, 5,
         'Animations running on visibility hidden element should not throttle ' +
         'restyling after the invisible element changed to visible');

      childElement.remove();
      await waitForNextFrame();

      markers = await observeStyling(5);
      todo_is(markers.length, 0,
              'Animations running on visibility hidden element should throttle ' +
              'restyling again after all visible descendants were removed');

      await ensureElementRemoval(div);
    }
  );

  add_task(
    async function restyling_animations_on_visiblity_hidden_element_having_oof_child() {
      const div = addDiv(null,
        { style: 'animation: on-main-thread 100s; position: absolute' });
      const childElement = addDiv(null,
        { style: 'float: left; visibility: hidden' });
      div.appendChild(childElement);

      const animation = div.getAnimations()[0];

      await waitForAnimationReadyToRestyle(animation);

      // We don't check the animation causes restyles here since we already
      // check it in the first test case.

      div.style.visibility = 'hidden';
      await waitForNextFrame();

      const markers = await observeStyling(5);
      is(markers.length, 0,
         'Animations running on visibility hidden element which has an ' +
         'out-of-flow child should throttle restyling');

      await ensureElementRemoval(div);
    }
  );

  add_task(
    async function restyling_animations_on_visibility_hidden_element_having_grandchild() {
      // element tree:
      //
      //        root(visibility:hidden)
      //       /                       \
      //    childA                   childB
      //    /     \                 /      \
      //  AA       AB             BA        BB

      const div = addDiv(null,
       { style: 'animation: on-main-thread 100s; visibility: hidden' });

      const childA = addDiv(null);
      div.appendChild(childA);
      const childB = addDiv(null);
      div.appendChild(childB);

      const grandchildAA = addDiv(null);
      childA.appendChild(grandchildAA);
      const grandchildAB = addDiv(null);
      childA.appendChild(grandchildAB);

      const grandchildBA = addDiv(null);
      childB.appendChild(grandchildBA);
      const grandchildBB = addDiv(null);
      childB.appendChild(grandchildBB);

      const animation = div.getAnimations()[0];

      await waitForAnimationReadyToRestyle(animation);
      let markers = await observeStyling(5);
      is(markers.length, 0,
         'Animations on visibility hidden element having no visible ' +
         'descendants should never cause restyles');

      childA.style.visibility = 'visible';
      grandchildAA.style.visibility = 'visible';
      grandchildAB.style.visibility = 'visible';
      await waitForNextFrame();

      markers = await observeStyling(5);
      is(markers.length, 5,
         'Animations running on visibility hidden element but the element has ' +
         'visible children should not throttle restyling');

      // Make childA hidden again but both of grandchildAA and grandchildAB are
      // still visible.
      childA.style.visibility = 'hidden';
      await waitForNextFrame();

      markers = await observeStyling(5);
      is(markers.length, 5,
         'Animations running on visibility hidden element that a child has ' +
         'become invisible again but there are still visible children should ' +
         'not throttle restyling');

      // Make grandchildAA hidden but grandchildAB is still visible.
      grandchildAA.style.visibility = 'hidden';
      await waitForNextFrame();

      markers = await observeStyling(5);
      is(markers.length, 5,
         'Animations running on visibility hidden element that a grandchild ' +
         'become invisible again but another grandchild is still visible ' +
         'should not throttle restyling');


      // Make childB and grandchildBA visible.
      childB.style.visibility = 'visible';
      grandchildBA.style.visibility = 'visible';
      await waitForNextFrame();

      markers = await observeStyling(5);
      is(markers.length, 5,
         'Animations running on visibility hidden element but the element has ' +
         'visible descendants should not throttle restyling');

      // Make childB hidden but grandchildAB and grandchildBA are still visible.
      childB.style.visibility = 'hidden';
      await waitForNextFrame();

      markers = await observeStyling(5);
      is(markers.length, 5,
         'Animations running on visibility hidden element but the element has ' +
         'visible grandchildren should not throttle restyling');

      // Make grandchildAB hidden but grandchildBA is still visible.
      grandchildAB.style.visibility = 'hidden';
      await waitForNextFrame();

      markers = await observeStyling(5);
      is(markers.length, 5,
         'Animations running on visibility hidden element but the element has ' +
         'a visible grandchild should not throttle restyling');

      // Make grandchildBA hidden. Now all descedants are invisible.
      grandchildBA.style.visibility = 'hidden';
      await waitForNextFrame();

      markers = await observeStyling(5);
      todo_is(markers.length, 0,
              'Animations on visibility hidden element that all descendants have ' +
              'become invisible again should never cause restyles');

      // Make childB visible.
      childB.style.visibility = 'visible';
      await waitForNextFrame();

      markers = await observeStyling(5);
      is(markers.length, 5,
         'Animations on visibility hidden element that has a visible child ' +
         'should never cause restyles');

      // Make childB invisible again
      childB.style.visibility = 'hidden';
      await waitForNextFrame();

      markers = await observeStyling(5);
      todo_is(markers.length, 0,
              'Animations on visibility hidden element that the visible child ' +
              'has become invisible again should never cause restyles');

      await ensureElementRemoval(div);
    }
  );

  add_task_if_omta_enabled(async function no_restyling_compositor_animations_after_pause_is_called() {
    const div = addDiv(null, { style: 'animation: opacity 100s' });
    const animation = div.getAnimations()[0];

    await waitForAnimationReadyToRestyle(animation);
    ok(SpecialPowers.wrap(animation).isRunningOnCompositor);

    animation.pause();

    await animation.ready;

    let markers = await observeStyling(1);
    is(markers.length, 1,
       'Animations running on the compositor should restyle once after ' +
       'Animation.pause() was called');

    markers = await observeStyling(5);
    is(markers.length, 0,
       'Paused animations running on the compositor should never cause ' +
       'restyles');
    await ensureElementRemoval(div);
  });

  add_task(async function no_restyling_main_thread_animations_after_pause_is_called() {
    const div = addDiv(null, { style: 'animation: on-main-thread 100s' });
    const animation = div.getAnimations()[0];

    await waitForAnimationReadyToRestyle(animation);

    animation.pause();

    await animation.ready;

    let markers = await observeStyling(1);
    is(markers.length, 1,
       'Animations running on the main-thread should restyle once after ' +
       'Animation.pause() was called');

    markers = await observeStyling(5);
    is(markers.length, 0,
       'Paused animations running on the main-thread should never cause ' +
       'restyles');
    await ensureElementRemoval(div);
  });

  add_task_if_omta_enabled(async function only_one_restyling_when_current_time_is_set_to_middle_of_duration() {
    const div = addDiv(null, { style: 'animation: opacity 100s' });
    const animation = div.getAnimations()[0];

    await waitForAnimationReadyToRestyle(animation);

    animation.currentTime = 50 * MS_PER_SEC;

    const markers = await observeStyling(5);
    is(markers.length, 1,
       'Bug 1235478: Animations running on the compositor should only once ' +
       'update style when currentTime is set to middle of duration time');
    await ensureElementRemoval(div);
  });

  add_task_if_omta_enabled(async function change_duration_and_currenttime() {
    const div = addDiv(null);
    const animation = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);

    await waitForAnimationReadyToRestyle(animation);
    ok(SpecialPowers.wrap(animation).isRunningOnCompositor);

    // Set currentTime to a time longer than duration.
    animation.currentTime = 500 * MS_PER_SEC;

    // Now the animation immediately get back from compositor.
    ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);

    // Extend the duration.
    animation.effect.updateTiming({ duration: 800 * MS_PER_SEC });
    const markers = await observeStyling(5);
    is(markers.length, 1,
       'Animations running on the compositor should update style ' +
       'when duration is made longer than the current time');

    await ensureElementRemoval(div);
  });

  add_task(async function script_animation_on_display_none_element() {
    const div = addDiv(null);
    const animation = div.animate({ zIndex: [ '0', '999' ] },
                                100 * MS_PER_SEC);

    await waitForAnimationReadyToRestyle(animation);

    div.style.display = 'none';

    // We need to wait a frame to apply display:none style.
    await waitForNextFrame();

    is(animation.playState, 'running',
       'Script animations keep running even when the target element has ' +
       '"display: none" style');

    ok(!SpecialPowers.wrap(animation).isRunningOnCompositor,
       'Script animations on "display:none" element should not run on the ' +
       'compositor');

    let markers = await observeStyling(5);
    is(markers.length, 0,
       'Script animations on "display: none" element should not update styles');

    div.style.display = '';

    markers = await observeStyling(5);
    is(markers.length, 5,
       'Script animations restored from "display: none" state should update ' +
       'styles');

    await ensureElementRemoval(div);
  });

  add_task_if_omta_enabled(async function compositable_script_animation_on_display_none_element() {
    const div = addDiv(null);
    const animation = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);

    await waitForAnimationReadyToRestyle(animation);

    div.style.display = 'none';

    // We need to wait a frame to apply display:none style.
    await waitForNextFrame();

    is(animation.playState, 'running',
       'Opacity script animations keep running even when the target element ' +
       'has "display: none" style');

    ok(!SpecialPowers.wrap(animation).isRunningOnCompositor,
       'Opacity script animations on "display:none" element should not ' +
       'run on the compositor');

    let markers = await observeStyling(5);
    is(markers.length, 0,
       'Opacity script animations on "display: none" element should not ' +
       'update styles');

    div.style.display = '';

     markers = await observeStyling(1);
    is(markers.length, 1,
       'Script animations restored from "display: none" state should update ' +
       'styles soon');

    ok(SpecialPowers.wrap(animation).isRunningOnCompositor,
       'Opacity script animations restored from "display: none" should be ' +
       'run on the compositor in the next frame');

    await ensureElementRemoval(div);
  });

  add_task(async function restyling_for_empty_keyframes() {
    const div = addDiv(null);
    const animation = div.animate({ }, 100 * MS_PER_SEC);

    await waitForAnimationReadyToRestyle(animation);
    let markers = await observeStyling(5);

    is(markers.length, 0,
       'Animations with no keyframes should not cause restyles');

    animation.effect.setKeyframes({ zIndex: ['0', '999'] });
    markers = await observeStyling(5);

    is(markers.length, 5,
       'Setting valid keyframes should cause regular animation restyles to ' +
       'occur');

    animation.effect.setKeyframes({ });
    markers = await observeStyling(5);

    is(markers.length, 1,
       'Setting an empty set of keyframes should trigger a single restyle ' +
       'to remove the previous animated style');

    await ensureElementRemoval(div);
  });

  add_task_if_omta_enabled(async function no_restyling_when_animation_style_when_re_setting_same_animation_property() {
    const div = addDiv(null, { style: 'animation: opacity 100s' });
    const animation = div.getAnimations()[0];
    await waitForAnimationReadyToRestyle(animation);
    ok(SpecialPowers.wrap(animation).isRunningOnCompositor);
    // Apply the same animation style
    div.style.animation = 'opacity 100s';
    const markers = await observeStyling(5);
    is(markers.length, 0,
       'Applying same animation style '  +
       'should never cause restyles');
    await ensureElementRemoval(div);
  });

  add_task(async function necessary_update_should_be_invoked() {
    const div = addDiv(null, { style: 'animation: on-main-thread 100s' });
    const animation = div.getAnimations()[0];
    await waitForAnimationReadyToRestyle(animation);
    await waitForAnimationFrames(5);
    // Apply another animation style
    div.style.animation = 'on-main-thread 110s';
    const markers = await observeStyling(1);
    // There should be two restyles.
    // 1) Animation-only restyle for before applying the new animation style
    // 2) Animation-only restyle for after applying the new animation style
    is(markers.length, 2,
       'Applying animation style with different duration '  +
       'should restyle twice');
    await ensureElementRemoval(div);
  });

  add_task_if_omta_enabled(
    async function changing_cascading_result_for_main_thread_animation() {
      const div = addDiv(null, { style: 'on-main-thread: blue' });
      const animation = div.animate({ opacity: [0, 1],
                                    zIndex: ['0', '999'] },
                                  100 * MS_PER_SEC);
      await waitForAnimationReadyToRestyle(animation);
      ok(SpecialPowers.wrap(animation).isRunningOnCompositor,
         'The opacity animation is running on the compositor');
      // Make the z-index style as !important to cause an update
      // to the cascade.
      // Bug 1300982: The z-index animation should be no longer
      // running on the main thread.
      div.style.setProperty('z-index', '0', 'important');
      const markers = await observeStyling(5);
      todo_is(markers.length, 0,
         'Changing cascading result for the property running on the main ' +
         'thread does not cause synchronization layer of opacity animation ' +
         'running on the compositor');
      await ensureElementRemoval(div);
    }
  );

  add_task_if_omta_enabled(
    async function animation_visibility_and_opacity() {
      const div = addDiv(null);
      const animation1 = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
      const animation2 = div.animate({ visibility: ['hidden', 'visible'] }, 100 * MS_PER_SEC);
      await waitForAnimationReadyToRestyle(animation1);
      await waitForAnimationReadyToRestyle(animation2);
      const markers = await observeStyling(5);
      is(markers.length, 5, 'The animation should not be throttled');
      await ensureElementRemoval(div);
    }
  );

  add_task(async function restyling_for_animation_on_orphaned_element() {
    const div = addDiv(null);
    const animation = div.animate({ marginLeft: [ '0px', '100px' ] },
                                100 * MS_PER_SEC);

    await waitForAnimationReadyToRestyle(animation);

    div.remove();

    let markers = await observeStyling(5);
    is(markers.length, 0,
       'Animation on orphaned element should not cause restyles');

    document.body.appendChild(div);

    await waitForNextFrame();
    markers = await observeStyling(5);

    is(markers.length, 5,
       'Animation on re-attached to the document begins to update style, got ' + markers.length);

    await ensureElementRemoval(div);
  });

  add_task_if_omta_enabled(
    // Tests that if we remove an element from the document whose animation
    // cascade needs recalculating, that it is correctly updated when it is
    // re-attached to the document.
    async function restyling_for_opacity_animation_on_re_attached_element() {
      const div = addDiv(null, { style: 'opacity: 1 ! important' });
      const animation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);

      await waitForAnimationReadyToRestyle(animation);
      ok(!SpecialPowers.wrap(animation).isRunningOnCompositor,
         'The opacity animation overridden by an !important rule is NOT ' +
         'running on the compositor');

      // Drop the !important rule to update the cascade.
      div.style.setProperty('opacity', '1', '');

      div.remove();

      const markers = await observeStyling(5);
      is(markers.length, 0,
         'Opacity animation on orphaned element should not cause restyles');

      document.body.appendChild(div);

      // Need a frame to give the animation a chance to be sent to the
      // compositor.
      await waitForNextFrame();

      ok(SpecialPowers.wrap(animation).isRunningOnCompositor,
         'The opacity animation which is no longer overridden by the ' +
         '!important rule begins running on the compositor even if the ' +
         '!important rule had been dropped before the target element was ' +
         'removed');

      await ensureElementRemoval(div);
    }
  );

  add_task(
    async function no_throttling_additive_animations_out_of_view_element() {
      const div = addDiv(null, { style: 'transform: translateY(-400px);' });
      const animation =
        div.animate([{ visibility: 'visible' }],
                    { duration: 100 * MS_PER_SEC, composite: 'add' });

      await waitForAnimationReadyToRestyle(animation);

      const markers = await observeStyling(5);

      is(markers.length, 5,
         'Additive animation has no keyframe whose offset is 0 or 1 in an ' +
         'out-of-view element should not be throttled');
      await ensureElementRemoval(div);
    }
  );

  // Tests that missing keyframes animations don't throttle at all.
  add_task(async function no_throttling_animations_out_of_view_element() {
    const div = addDiv(null, { style: 'transform: translateY(-400px);' });
    const animation =
      div.animate([{ visibility: 'visible' }], 100 * MS_PER_SEC);

    await waitForAnimationReadyToRestyle(animation);

    const markers = await observeStyling(5);

    is(markers.length, 5,
       'Discrete animation has has no keyframe whose offset is 0 or 1 in an ' +
       'out-of-view element should not be throttled');
    await ensureElementRemoval(div);
  });

  // Tests that missing keyframes animation on scrolled out element that the
  // animation is not able to be throttled.
  add_task(
    async function no_throttling_missing_keyframe_animations_out_of_view_element() {
      const div =
        addDiv(null, { style: 'transform: translateY(-400px);' +
                              'visibility: collapse;' });
      const animation =
        div.animate([{ visibility: 'visible' }], 100 * MS_PER_SEC);
      await waitForAnimationReadyToRestyle(animation);

      const markers = await observeStyling(5);
      is(markers.length, 5,
         'visibility animation has no keyframe whose offset is 0 or 1 in an ' +
         'out-of-view element and produces change hint other than paint-only ' +
         'change hint should not be throttled');
      await ensureElementRemoval(div);
    }
  );

  // Counter part of the above test.
  add_task(async function no_restyling_discrete_animations_out_of_view_element() {
    const div = addDiv(null, { style: 'transform: translateY(-400px);' });
    const animation =
      div.animate({ visibility: ['visible', 'hidden'] }, 100 * MS_PER_SEC);

    await waitForAnimationReadyToRestyle(animation);

    const markers = await observeStyling(5);

    is(markers.length, 0,
       'Discrete animation running on the main-thread in an out-of-view ' +
       'element should never cause restyles');
    await ensureElementRemoval(div);
  });

  add_task(async function no_restyling_while_computed_timing_is_not_changed() {
    const div = addDiv(null);
    const animation = div.animate({ zIndex: [ '0', '999' ] },
                                  { duration: 100 * MS_PER_SEC,
                                    easing: 'step-end' });

    await waitForAnimationReadyToRestyle(animation);

    const markers = await observeStyling(5);

    // We possibly expect one restyle from the initial animation compose, in
    // order to update animations, but nothing else.
    ok(markers.length <= 1,
       'Animation running on the main-thread while computed timing is not ' +
       'changed should not cause extra restyles, got ' + markers.length);
    await ensureElementRemoval(div);
  });

  add_task(async function no_throttling_animations_in_view_svg() {
    const div = addDiv(null, { style: 'overflow: scroll;' +
                                    'height: 100px; width: 100px;' });
    const svg = addSVGElement(div, 'svg', { viewBox: '-10 -10 0.1 0.1',
                                          width:   '50px',
                                          height:  '50px' });
    const rect = addSVGElement(svg, 'rect', { x:      '-10',
                                            y:      '-10',
                                            width:  '10',
                                            height: '10',
                                            fill:   'red' });
    const animation = rect.animate({ fill: ['blue', 'lime'] }, 100 * MS_PER_SEC);
    await waitForAnimationReadyToRestyle(animation);

    const markers = await observeStyling(5);
    is(markers.length, 5,
       'CSS animations on an in-view svg element with post-transform should ' +
       'not be throttled.');

    await ensureElementRemoval(div);
  });

  add_task(async function no_throttling_animations_in_transformed_parent() {
    const div = addDiv(null, { style: 'overflow: scroll;' +
                                    'transform: translateX(50px);' });
    const svg = addSVGElement(div, 'svg', { viewBox: '0 0 1250 1250',
                                          width:   '40px',
                                          height:  '40px' });
    const rect = addSVGElement(svg, 'rect', { x:      '0',
                                            y:      '0',
                                            width:  '1250',
                                            height: '1250',
                                            fill:   'red' });
    const animation = rect.animate({ fill: ['blue', 'lime'] }, 100 * MS_PER_SEC);
    await waitForAnimationReadyToRestyle(animation);

    const markers = await observeStyling(5);
    is(markers.length, 5,
       'CSS animations on an in-view svg element which is inside transformed ' +
       'parent should not be throttled.');

    await ensureElementRemoval(div);
  });

  add_task(async function throttling_animations_out_of_view_svg() {
    const div = addDiv(null, { style: 'overflow: scroll;' +
                                    'height: 100px; width: 100px;' });
    const svg = addSVGElement(div, 'svg', { viewBox: '-10 -10 0.1 0.1',
                                          width: '50px',
                                          height: '50px' });
    const rect = addSVGElement(svg, 'rect', { width: '10',
                                            height: '10',
                                            fill: 'red' });

    const animation = rect.animate({ fill: ['blue', 'lime'] }, 100 * MS_PER_SEC);
    await waitForAnimationReadyToRestyle(animation);

    const markers = await observeStyling(5);
    is(markers.length, 0,
       'CSS animations on an out-of-view svg element with post-transform ' +
       'should be throttled.');

    await ensureElementRemoval(div);
  });

  add_task(async function no_throttling_animations_in_view_css_transform() {
    const scrollDiv = addDiv(null, { style: 'overflow: scroll; ' +
                                          'height: 100px; width: 100px;' });
    const targetDiv = addDiv(null,
                           { style: 'animation: on-main-thread 100s;' +
                                    'transform: translate(-50px, -50px);' });
    scrollDiv.appendChild(targetDiv);

    const animation = targetDiv.getAnimations()[0];
    await waitForAnimationReadyToRestyle(animation);

    const markers = await observeStyling(5);
    is(markers.length, 5,
       'CSS animation on an in-view element with pre-transform should not ' +
       'be throttled.');

    await ensureElementRemoval(scrollDiv);
  });

  add_task(async function throttling_animations_out_of_view_css_transform() {
    const scrollDiv = addDiv(null, { style: 'overflow: scroll;' +
                                          'height: 100px; width: 100px;' });
    const targetDiv = addDiv(null,
                           { style: 'animation: on-main-thread 100s;' +
                                    'transform: translate(100px, 100px);' });
    scrollDiv.appendChild(targetDiv);

    const animation = targetDiv.getAnimations()[0];
    await waitForAnimationReadyToRestyle(animation);

    const markers = await observeStyling(5);
    is(markers.length, 0,
       'CSS animation on an out-of-view element with pre-transform should be ' +
       'throttled.');

    await ensureElementRemoval(scrollDiv);
  });

  add_task(
    async function throttling_animations_in_out_of_view_position_absolute_element() {
      const parentDiv = addDiv(null,
                             { style: 'position: absolute; top: -1000px;' });
      const targetDiv = addDiv(null,
                             { style: 'animation: on-main-thread 100s;' });
      parentDiv.appendChild(targetDiv);

      const animation = targetDiv.getAnimations()[0];
      await waitForAnimationReadyToRestyle(animation);

      const markers = await observeStyling(5);
      is(markers.length, 0,
         'CSS animation in an out-of-view position absolute element should ' +
         'be throttled');

      await ensureElementRemoval(parentDiv);
    }
  );

  add_task(
    async function throttling_animations_on_out_of_view_position_absolute_element() {
      const div = addDiv(null,
                       { style: 'animation: on-main-thread 100s; ' +
                                'position: absolute; top: -1000px;' });

      const animation = div.getAnimations()[0];
      await waitForAnimationReadyToRestyle(animation);

      const markers = await observeStyling(5);
      is(markers.length, 0,
         'CSS animation on an out-of-view position absolute element should ' +
         'be throttled');

      await ensureElementRemoval(div);
    }
  );

  add_task(
    async function throttling_animations_in_out_of_view_position_fixed_element() {
      const parentDiv = addDiv(null,
                             { style: 'position: fixed; top: -1000px;' });
      const targetDiv = addDiv(null,
                             { style: 'animation: on-main-thread 100s;' });
      parentDiv.appendChild(targetDiv);

      const animation = targetDiv.getAnimations()[0];
      await waitForAnimationReadyToRestyle(animation);

      const markers = await observeStyling(5);
      is(markers.length, 0,
         'CSS animation on an out-of-view position:fixed element should be ' +
         'throttled');

      await ensureElementRemoval(parentDiv);
    }
  );

  add_task(
    async function throttling_animations_on_out_of_view_position_fixed_element() {
      const div = addDiv(null,
                       { style: 'animation: on-main-thread 100s; ' +
                                'position: fixed; top: -1000px;' });

      const animation = div.getAnimations()[0];
      await waitForAnimationReadyToRestyle(animation);

      const markers = await observeStyling(5);
      is(markers.length, 0,
         'CSS animation on an out-of-view position:fixed element should be ' +
         'throttled');

      await ensureElementRemoval(div);
    }
  );

  add_task(
    async function no_throttling_animations_in_view_position_fixed_element() {
      const iframe = document.createElement('iframe');
      iframe.setAttribute('srcdoc', '<div id="target"></div>');
      document.documentElement.appendChild(iframe);

      await new Promise(resolve => {
        iframe.addEventListener('load', () => {
          resolve();
        });
      });

      const target = iframe.contentDocument.getElementById('target');
      target.style= 'position: fixed; top: 20px; width: 100px; height: 100px;';

      const animation = target.animate({ zIndex: [ '0', '999' ] },
                                       { duration: 100 * MS_PER_SEC,
                                         iterations: Infinity });
      await waitForAnimationReadyToRestyle(animation);

      const markers = await observeStylingInTargetWindow(iframe.contentWindow, 5);
      is(markers.length, 5,
         'CSS animation on an in-view position:fixed element should NOT be ' +
         'throttled');

      await ensureElementRemoval(iframe);
    }
  );

  add_task(
    async function throttling_position_absolute_animations_in_collapsed_iframe() {
      const iframe = document.createElement('iframe');
      iframe.setAttribute('srcdoc', '<div id="target"></div>');
      iframe.style.height = '0px';
      document.documentElement.appendChild(iframe);

      await new Promise(resolve => {
        iframe.addEventListener('load', () => {
          resolve();
        });
      });

      const target = iframe.contentDocument.getElementById("target");
      target.style= 'position: absolute; top: 50%; width: 100px; height: 100px';

      const animation = target.animate({ opacity: [0, 1] },
                                     { duration: 100 * MS_PER_SEC,
                                       iterations: Infinity });
      await waitForAnimationReadyToRestyle(animation);

      const markers = await observeStylingInTargetWindow(iframe.contentWindow, 5);
      is(markers.length, 0,
         'Animation on position:absolute element in collapsed iframe should ' +
         'be throttled');

      await ensureElementRemoval(iframe);
    }
  );

  add_task(
    async function position_absolute_animations_in_collapsed_element() {
      const parent = addDiv(null, { style: 'overflow: scroll; height: 0px;' });
      const target = addDiv(null,
                          { style: 'animation: on-main-thread 100s infinite;' +
                                   'position: absolute; top: 50%;' +
                                   'width: 100px; height: 100px;' });
      parent.appendChild(target);

      const animation = target.getAnimations()[0];
      await waitForAnimationReadyToRestyle(animation);

      const markers = await observeStyling(5);
      is(markers.length, 5,
         'Animation on position:absolute element in collapsed element ' +
         'should not be throttled');

      await ensureElementRemoval(parent);
    }
  );

  add_task(
    async function throttling_position_absolute_animations_in_collapsed_element() {
      const parent = addDiv(null, { style: 'overflow: scroll; height: 0px;' });
      const target = addDiv(null,
                          { style: 'animation: on-main-thread 100s infinite;' +
                                   'position: absolute; top: 50%;' });
      parent.appendChild(target);

      const animation = target.getAnimations()[0];
      await waitForAnimationReadyToRestyle(animation);

      const markers = await observeStyling(5);
      todo_is(markers.length, 0,
              'Animation on collapsed position:absolute element in collapsed ' +
              'element should be throttled');

      await ensureElementRemoval(parent);
    }
  );

  add_task_if_omta_enabled(
    async function no_restyling_for_compositor_animation_on_unrelated_style_change() {
      const div = addDiv(null);
      const animation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);

      await waitForAnimationReadyToRestyle(animation);
      ok(SpecialPowers.wrap(animation).isRunningOnCompositor,
         'The opacity animation is running on the compositor');

      div.style.setProperty('color', 'blue', '');
      const markers = await observeStyling(5);
      is(markers.length, 0,
         'The opacity animation keeps running on the compositor when ' +
         'color style is changed');
      await ensureElementRemoval(div);
    }
  );

  add_task(
    async function no_overflow_transform_animations_in_scrollable_element() {
      const parentElement = addDiv(null,
        { style: 'overflow-y: scroll; height: 100px;' });
      const div = addDiv(null);
      const animation =
        div.animate({ transform: [ 'translateY(10px)', 'translateY(10px)' ] },
                    100 * MS_PER_SEC);
      parentElement.appendChild(div);

      await waitForAnimationReadyToRestyle(animation);
      ok(SpecialPowers.wrap(animation).isRunningOnCompositor);

      const markers = await observeStyling(20);
      is(markers.length, 0,
         'No-overflow transform animations running on the compositor should ' +
         'never update style on the main thread');

      await ensureElementRemoval(parentElement);
    }
  );

  add_task(async function no_flush_on_getAnimations() {
    const div = addDiv(null);
    const animation =
      div.animate({ opacity: [ '0', '1' ] }, 100 * MS_PER_SEC);
    await waitForAnimationReadyToRestyle(animation);

    ok(SpecialPowers.wrap(animation).isRunningOnCompositor);

    const markers = observeAnimSyncStyling(() => {
      is(div.getAnimations().length, 1, 'There should be one animation');
    });
    is(markers.length, 0,
       'Element.getAnimations() should not flush throttled animation style');

    await ensureElementRemoval(div);
  });

  add_task(async function restyling_for_throttled_animation_on_getAnimations() {
    const div = addDiv(null, { style: 'animation: opacity 100s' });
    const animation = div.getAnimations()[0];

    await waitForAnimationReadyToRestyle(animation);
    ok(SpecialPowers.wrap(animation).isRunningOnCompositor);

    const markers = observeAnimSyncStyling(() => {
      div.style.animationDuration = '0s';
      is(div.getAnimations().length, 0, 'There should be no animation');
    });

    is(markers.length, 1, // For discarding the throttled animation.
       'Element.getAnimations() should flush throttled animation style so ' +
       'that the throttled animation is discarded');

    await ensureElementRemoval(div);
  });

  add_task(
    async function no_restyling_for_throttled_animation_on_querying_play_state() {
      const div = addDiv(null, { style: 'animation: opacity 100s' });
      const animation = div.getAnimations()[0];
      const sibling = addDiv(null);

      await waitForAnimationReadyToRestyle(animation);
      ok(SpecialPowers.wrap(animation).isRunningOnCompositor);

      const markers = observeAnimSyncStyling(() => {
        sibling.style.opacity = '0.5';
        is(animation.playState, 'running',
           'Animation.playState should be running');
      });
      is(markers.length, 0,
         'Animation.playState should not flush throttled animation in the ' +
         'case where there are only style changes that don\'t affect the ' +
         'throttled animation');

      await ensureElementRemoval(div);
      await ensureElementRemoval(sibling);
    }
  );

  add_task(
    async function restyling_for_throttled_animation_on_querying_play_state() {
      const div = addDiv(null, { style: 'animation: opacity 100s' });
      const animation = div.getAnimations()[0];

      await waitForAnimationReadyToRestyle(animation);
      ok(SpecialPowers.wrap(animation).isRunningOnCompositor);

      const markers = observeAnimSyncStyling(() => {
        div.style.animationPlayState = 'paused';
        is(animation.playState, 'paused',
           'Animation.playState should be reflected by pending style');
      });

      is(markers.length, 1,
         'Animation.playState should flush throttled animation style that ' +
         'affects the throttled animation');

      await ensureElementRemoval(div);
    }
  );

  add_task(
    async function no_restyling_for_throttled_transition_on_querying_play_state() {
      const div = addDiv(null, { style: 'transition: opacity 100s; opacity: 0' });
      const sibling = addDiv(null);

      getComputedStyle(div).opacity;
      div.style.opacity = 1;

      const transition = div.getAnimations()[0];

      await waitForAnimationReadyToRestyle(transition);
      ok(SpecialPowers.wrap(transition).isRunningOnCompositor);

      const markers = observeAnimSyncStyling(() => {
        sibling.style.opacity = '0.5';
        is(transition.playState, 'running',
           'Animation.playState should be running');
      });

      is(markers.length, 0,
         'Animation.playState should not flush throttled transition in the ' +
         'case where there are only style changes that don\'t affect the ' +
         'throttled animation');

      await ensureElementRemoval(div);
      await ensureElementRemoval(sibling);
    }
  );

  add_task(
    async function restyling_for_throttled_transition_on_querying_play_state() {
      const div = addDiv(null, { style: 'transition: opacity 100s; opacity: 0' });
      getComputedStyle(div).opacity;
      div.style.opacity = '1';

      const transition = div.getAnimations()[0];

      await waitForAnimationReadyToRestyle(transition);
      ok(SpecialPowers.wrap(transition).isRunningOnCompositor);

      const markers = observeAnimSyncStyling(() => {
        div.style.transitionProperty = 'none';
        is(transition.playState, 'idle',
           'Animation.playState should be reflected by pending style change ' +
           'which cancel the transition');
      });

      is(markers.length, 1,
         'Animation.playState should flush throttled transition style that ' +
         'affects the throttled animation');

      await ensureElementRemoval(div);
    }
  );

  add_task(async function restyling_visibility_animations_on_in_view_element() {
    const div = addDiv(null);
    const animation =
      div.animate({ visibility: ['hidden', 'visible'] }, 100 * MS_PER_SEC);

    await waitForAnimationReadyToRestyle(animation);

    const markers = await observeStyling(5);

    is(markers.length, 5,
       'Visibility animation running on the main-thread on in-view element ' +
       'should not be throttled');
    await ensureElementRemoval(div);
  });

  add_task(async function restyling_outline_offset_animations_on_invisible_element() {
    const div = addDiv(null,
                       { style: 'visibility: hidden; ' +
                                'outline-style: solid; ' +
                                'outline-width: 1px;' });
    const animation =
      div.animate({ outlineOffset: [ '0px', '10px' ] },
                  { duration: 100 * MS_PER_SEC,
                    iterations: Infinity });

    await waitForAnimationReadyToRestyle(animation);

    const markers = await observeStyling(5);

    is(markers.length, 0,
       'Outline offset animation running on the main-thread on invisible ' +
       'element should be throttled');
    await ensureElementRemoval(div);
  });

  add_task(async function restyling_transform_animations_on_invisible_element() {
    const div = addDiv(null, { style: 'visibility: hidden;' });

    const animation =
      div.animate({ transform: [ 'none', 'rotate(360deg)' ] },
                  { duration: 100 * MS_PER_SEC,
                    iterations: Infinity });

    await waitForAnimationReadyToRestyle(animation);

    ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);

    const markers = await observeStyling(5);

    is(markers.length, 0,
       'Transform animations on visibility hidden element should be throttled');
    await ensureElementRemoval(div);
  });

  add_task(async function restyling_transform_animations_on_invisible_element() {
    const div = addDiv(null, { style: 'visibility: hidden;' });

    const animation =
      div.animate([ { transform: 'rotate(360deg)' } ],
                  { duration: 100 * MS_PER_SEC,
                    iterations: Infinity });

    await waitForAnimationReadyToRestyle(animation);

    ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);

    const markers = await observeStyling(5);

    is(markers.length, 0,
       'Transform animations without 100% keyframe on visibility hidden ' +
       'element should be throttled');
    await ensureElementRemoval(div);
  });

  add_task(async function restyling_translate_animations_on_invisible_element() {
    const div = addDiv(null, { style: 'visibility: hidden;' });

    const animation =
      div.animate([ { translate: '100px' } ],
                  { duration: 100 * MS_PER_SEC,
                    iterations: Infinity });

    await waitForAnimationReadyToRestyle(animation);

    ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);

    const markers = await observeStyling(5);

    is(markers.length, 0,
       'Translate animations without 100% keyframe on visibility hidden ' +
       'element should be throttled');
    await ensureElementRemoval(div);
  });

  add_task(async function restyling_rotate_animations_on_invisible_element() {
    const div = addDiv(null, { style: 'visibility: hidden;' });

    const animation =
      div.animate([ { rotate: '45deg' } ],
                  { duration: 100 * MS_PER_SEC,
                    iterations: Infinity });

    await waitForAnimationReadyToRestyle(animation);

    ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);

    const markers = await observeStyling(5);

    is(markers.length, 0,
       'Rotate animations without 100% keyframe on visibility hidden ' +
       'element should be throttled');
    await ensureElementRemoval(div);
  });

  add_task(async function restyling_scale_animations_on_invisible_element() {
    const div = addDiv(null, { style: 'visibility: hidden;' });

    const animation =
      div.animate([ { scale: '2 2' } ],
                  { duration: 100 * MS_PER_SEC,
                    iterations: Infinity });

    await waitForAnimationReadyToRestyle(animation);

    ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);

    const markers = await observeStyling(5);

    is(markers.length, 0,
       'Scale animations without 100% keyframe on visibility hidden ' +
       'element should be throttled');
    await ensureElementRemoval(div);
  });

  add_task(
    async function restyling_transform_animations_having_abs_pos_child_on_invisible_element() {
      const div = addDiv(null, { style: 'visibility: hidden;' });
      const child = addDiv(null, { style: 'position: absolute; top: 100px;' });
      div.appendChild(child);

      const animation =
        div.animate({ transform: [ 'none',  'rotate(360deg)' ] },
                    { duration: 100 * MS_PER_SEC,
                      iterations: Infinity });

      await waitForAnimationReadyToRestyle(animation);

      ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);

      const markers = await observeStyling(5);

      is(markers.length, 0,
         'Transform animation having an absolutely positioned child on ' +
         'visibility hidden element should be throttled');
      await ensureElementRemoval(div);
  });

  add_task(async function no_restyling_animations_in_out_of_view_iframe() {
    const div = addDiv(null, { style: 'overflow-y: scroll; height: 100px;' });

    const iframe = document.createElement('iframe');
    iframe.setAttribute(
      'srcdoc',
      '<div style="height: 100px;"></div><div id="target"></div>');
    div.appendChild(iframe);

    await new Promise(resolve => {
      iframe.addEventListener('load', () => {
        resolve();
      });
    });

    const target = iframe.contentDocument.getElementById("target");
    target.style= 'width: 100px; height: 100px;';

    const animation = target.animate({ zIndex: [ '0', '999' ] },
                                     100 * MS_PER_SEC);
    await waitForAnimationReadyToRestyle(animation);

    const markers = await observeStylingInTargetWindow(iframe.contentWindow, 5);
    is(markers.length, 0,
       'Animation in out-of-view iframe should be throttled');

    await ensureElementRemoval(div);
  });

  // Tests that transform animations are not able to run on the compositor due
  // to layout restrictions (e.g. animations on a large size frame) doesn't
  // flush layout at all.
  add_task(async function flush_layout_for_transform_animations() {
    // Set layout.animation.prerender.partial to disallow transform animations
    // on large frames to be sent to the compositor.
    await SpecialPowers.pushPrefEnv({
      set: [['layout.animation.prerender.partial', false]] });
    const div = addDiv(null, { style: 'width: 10000px; height: 10000px;' });

    const animation = div.animate([ { transform: 'rotate(360deg)', } ],
                                  { duration: 100 * MS_PER_SEC,
                                    // Set step-end to skip further restyles.
                                    easing: 'step-end' });

    const FLUSH_LAYOUT = SpecialPowers.DOMWindowUtils.FLUSH_LAYOUT;
    ok(SpecialPowers.DOMWindowUtils.needsFlush(FLUSH_LAYOUT),
       'Flush is needed for the appended div');

    await waitForAnimationReadyToRestyle(animation);

    ok(!SpecialPowers.wrap(animation).isRunningOnCompositor, "Shouldn't be running in the compositor");

    // We expect one restyle from the initial animation compose.
    await waitForNextFrame();

    ok(!SpecialPowers.wrap(animation).isRunningOnCompositor, "Still shouldn't be running in the compositor");
    ok(!SpecialPowers.DOMWindowUtils.needsFlush(FLUSH_LAYOUT),
       'No further layout flush needed');

    await ensureElementRemoval(div);
  });

  add_task(async function partial_prerendered_transform_animations() {
    await SpecialPowers.pushPrefEnv({
      set: [['layout.animation.prerender.partial', true]] });
    const div = addDiv(null, { style: 'width: 10000px; height: 10000px;' });

    const animation = div.animate(
      // Use the same value both for `from` and `to` to avoid jank on the
      // compositor.
      { transform: ['rotate(0deg)', 'rotate(0deg)'] },
      100 * MS_PER_SEC
    );

    await waitForAnimationReadyToRestyle(animation);

    const markers = await observeStyling(5);
    is(markers.length, 0,
       'Transform animation with partial pre-rendered should never cause ' +
       'restyles');

    await ensureElementRemoval(div);
  });

  add_task(async function restyling_on_create_animation() {
    const div = addDiv();
    const docShell = getDocShellForObservingRestylesForWindow(window);

    const animationA = div.animate(
      { transform: ['none', 'rotate(360deg)'] },
      100 * MS_PER_SEC
    );
    const animationB = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
    const animationC = div.animate(
      { color: ['blue', 'green'] },
      100 * MS_PER_SEC
    );
    const animationD = div.animate(
      { width: ['100px', '200px'] },
      100 * MS_PER_SEC
    );
    const animationE = div.animate(
      { height: ['100px', '200px'] },
      100 * MS_PER_SEC
    );

    const markers = docShell
      .popProfileTimelineMarkers()
      .filter(marker => marker.name === 'Styles' && !marker.isAnimationOnly);
    docShell.recordProfileTimelineMarkers = false;

    is(markers.length, 0, 'Creating animations should not flush styles');

    await ensureElementRemoval(div);
  });

  add_task(async function out_of_view_background_position() {
    const div = addDiv(null, {
      style: `
        background-image: linear-gradient(90deg, rgb(224, 224, 224), rgb(241, 241, 241) 30%, rgb(224, 224, 224) 60%);
        background-size: 80px;
        animation: background-position 100s infinite;
        transform: translateY(-400px);
      `,
    })

    const animation = div.getAnimations()[0];

    await waitForAnimationReadyToRestyle(animation);

    const markers = await observeStyling(5);

    is(markers.length, 0, 'background-position animations can be throttled');
    await ensureElementRemoval(div);
  });

  add_task_if_omta_enabled(async function no_restyling_animations_in_opacity_zero_element() {
    const div = addDiv(null, { style: 'animation: on-main-thread 100s infinite; opacity: 0' });
    const animation = div.getAnimations()[0];

    await waitForAnimationReadyToRestyle(animation);
    const markers = await observeStyling(5);
    is(markers.length, 0,
       'Animations running on the main thread in opacity: 0 element ' +
       'should never cause restyles');
    await ensureElementRemoval(div);
  });

  add_task_if_omta_enabled(async function no_restyling_compositor_animations_in_opacity_zero_descendant() {
    const container = addDiv(null, { style: 'opacity: 0' });
    const child = addDiv(null, { style: 'animation: background-color 100s infinite;' });
    container.appendChild(child);

    const animation = child.getAnimations()[0];
    await waitForAnimationReadyToRestyle(animation);
    ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);

    const markers = await observeStyling(5);

    is(markers.length, 0,
       'Animations running on the compositor in opacity zero descendant element ' +
       'should never cause restyles');
    await ensureElementRemoval(container);
  });

  add_task_if_omta_enabled(async function no_restyling_compositor_animations_in_opacity_zero_descendant_abspos() {
    const container = addDiv(null, { style: 'opacity: 0' });
    const child = addDiv(null, { style: 'position: absolute; animation: background-color 100s infinite;' });
    container.appendChild(child);

    const animation = child.getAnimations()[0];
    await waitForAnimationReadyToRestyle(animation);
    ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);

    const markers = await observeStyling(5);

    is(markers.length, 0,
       'Animations running on the compositor in opacity zero abspos descendant element ' +
       'should never cause restyles');
    await ensureElementRemoval(container);
  });

  add_task_if_omta_enabled(async function no_restyling_compositor_animations_in_opacity_zero_element() {
    const child = addDiv(null, { style: 'animation: background-color 100s infinite; opacity: 0' });

    const animation = child.getAnimations()[0];
    await waitForAnimationReadyToRestyle(animation);
    ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);

    const markers = await observeStyling(5);

    is(markers.length, 0,
       'Animations running on the compositor in opacity zero element ' +
       'should never cause restyles');
    await ensureElementRemoval(child);
  });

  add_task_if_omta_enabled(async function restyling_main_thread_animations_in_opacity_zero_descendant_after_root_opacity_animation() {
    const container = addDiv(null, { style: 'opacity: 0' });

    const child = addDiv(null, { style: 'animation: on-main-thread 100s infinite;' });
    container.appendChild(child);

    // Animate the container from 1 to zero opacity and ensure the child animation is throttled then.
    const containerAnimation = container.animate({ opacity: [ '1', '0' ] }, 100);
    await containerAnimation.finished;

    const animation = child.getAnimations()[0];
    await waitForAnimationReadyToRestyle(animation);

    const markers = await observeStyling(5);

    is(markers.length, 0,
       'Animations running on the compositor in opacity zero descendant element ' +
       'should never cause restyles after root animation has finished');
    await ensureElementRemoval(container);
  });

  add_task_if_omta_enabled(async function restyling_main_thread_animations_in_opacity_zero_descendant_during_root_opacity_animation() {
    const container = addDiv(null, { style: 'opacity: 0; animation: opacity 100s infinite' });

    const child = addDiv(null, { style: 'animation: on-main-thread 100s infinite;' });
    container.appendChild(child);

    const animation = child.getAnimations()[0];
    await waitForAnimationReadyToRestyle(animation);

    const markers = await observeStyling(5);

    is(markers.length, 5,
       'Animations in opacity zero descendant element ' +
       'should not be throttled if root is animating opacity');
    await ensureElementRemoval(container);
  });

  add_task_if_omta_enabled(async function transparent_background_color_animations() {
    const div = addDiv(null);
    const animation =
      div.animate({ backgroundColor: [ 'rgb(0, 200, 0, 0)',
                                       'rgb(200, 0, 0, 0.1)' ] },
                  { duration: 100 * MS_PER_SEC,
                    // An easing function producing zero in the first half of
                    // the duration.
                    easing: 'cubic-bezier(1, 0, 1, 0)' });
    await waitForAnimationReadyToRestyle(animation);

    ok(SpecialPowers.wrap(animation).isRunningOnCompositor);

    const markers = await observeStyling(5);
    is(markers.length, 0,
       'transparent background-color animation should not update styles on ' +
       'the main thread');

    await ensureElementRemoval(div);
  });

  add_task_if_omta_enabled(async function transform_animation_on_collapsed_element() {
    const iframe = document.createElement('iframe');
    document.body.appendChild(iframe);

    // Load a cross origin iframe.
    const targetURL = SimpleTest.getTestFileURL("empty.html")
      .replace(window.location.origin, "http://example.com/");
    iframe.src = targetURL;
    await new Promise(resolve => {
      iframe.onload = resolve;
    });

    await SpecialPowers.spawn(iframe, [MS_PER_SEC], async (MS_PER_SEC) => {
      // Create a flex item with "preserve-3d" having an abs-pos child inside
      // a grid container.
      // These styles make the the flex item size (0x0).
      const gridContainer = content.document.createElement("div");
      gridContainer.style.display = "grid";
      gridContainer.style.placeItems = "center";

      const target = content.document.createElement("div");
      target.style.display = "flex";
      target.style.transformStyle = "preserve-3d";
      gridContainer.appendChild(target);

      const child = content.document.createElement("div");
      child.style.position = "absolute";
      child.style.transform = "rotateY(0deg)";
      child.style.width = "100px";
      child.style.height = "100px";
      child.style.backgroundColor = "green";
      target.appendChild(child);

      content.document.body.appendChild(gridContainer);

      const animation =
        target.animate({ transform: [ "rotateY(0deg)", "rotateY(360deg)" ] },
                    { duration: 100 * MS_PER_SEC,
                      id: "test",
                      easing: 'step-end' });
      await content.wrappedJSObject.waitForAnimationReadyToRestyle(animation);
      ok(SpecialPowers.wrap(animation).isRunningOnCompositor,
         'transform animation on a collapsed element should run on the ' +
         'compositor');

      const markers = await content.wrappedJSObject.observeStyling(5);
      is(markers.length, 0,
         'transform animation on a collapsed element animation should not ' +
         'update styles on the main thread');
    });

    await ensureElementRemoval(iframe);
  });
});

</script>
</body>
