dom/animation/test/chrome/test_restyles.html
author Geoff Brown <gbrown@mozilla.com>
Mon, 29 May 2017 16:03:51 -0600
changeset 586211 8c149fcc52964640c3c5f0e278939ab97aaf2def
parent 552414 6d4a53b6c48b101167059a2c76dec9dc3a5bfecd
child 598936 b0dd91a6a1fd347587ecaef9f81c04de937d7f31
permissions -rw-r--r--
Bug 1332970 - Allow assertion in test_restyles.html; r=hiro Frequent intermittent assertions have been happening for a long time.

<!doctype html>
<head>
<meta charset=utf-8>
<title>Tests restyles caused by animations</title>
<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
<script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
<script src="chrome://mochikit/content/tests/SimpleTest/paint_listener.js"></script>
<script src="../testcommon.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<style>
@keyframes opacity {
  from { opacity: 1; }
  to { opacity: 0; }
}
@keyframes background-color {
  from { background-color: red; }
  to { background-color: blue; }
}
@keyframes rotate {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}
div {
  /* Element needs geometry to be eligible for layerization */
  width: 100px;
  height: 100px;
  background-color: white;
}
</style>
</head>
<body>
<script>
'use strict';

function observeStyling(frameCount, onFrame) {
  var docShell = window.QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor)
                       .getInterface(SpecialPowers.Ci.nsIWebNavigation)
                       .QueryInterface(SpecialPowers.Ci.nsIDocShell);

  docShell.recordProfileTimelineMarkers = true;
  docShell.popProfileTimelineMarkers();

  return new Promise(function(resolve) {
    return waitForAnimationFrames(frameCount, onFrame).then(function() {
      var markers = docShell.popProfileTimelineMarkers();
      docShell.recordProfileTimelineMarkers = false;
      var stylingMarkers = markers.filter(function(marker, index) {
        return marker.name == 'Styles' &&
               (marker.restyleHint == 'eRestyle_CSSAnimations' ||
                marker.restyleHint == 'eRestyle_CSSTransitions');
      });
      resolve(stylingMarkers);
    });
  });
}

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

SimpleTest.expectAssertions(0, 1); // bug 1332970
SimpleTest.waitForExplicitFinish();

var omtaEnabled = isOMTAEnabled();

var isAndroid = !!navigator.userAgent.includes("Android");

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(function() {
  add_task(function* restyling_for_main_thread_animations() {
    var div = addDiv(null, { style: 'animation: background-color 100s' });
    var animation = div.getAnimations()[0];

    yield animation.ready;
    ok(!animation.isRunningOnCompositor);

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

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

    yield animation.ready;
    ok(animation.isRunningOnCompositor);

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

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

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

    yield animation.ready;
    ok(animation.isRunningOnCompositor);

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

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

    yield animation.ready;
    ok(animation.isRunningOnCompositor);

    div.animationDuration = '200s';

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

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

    yield animation.ready;
    ok(animation.isRunningOnCompositor);

    animation.finish();

    var markers = yield observeStyling(5);
    is(markers.length, 1,
       'Animations running on the compositor should only update style ' +
       'once after finish() is called');
    yield ensureElementRemoval(div);
  });

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

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

    yield animation.finished;

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

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

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

    var initialRect = div.getBoundingClientRect();

    yield animation.finished;

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

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

  add_task_if_omta_enabled(function* no_restyling_compositor_animations_out_of_view_element() {
    if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
      return;
    }

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

    yield animation.ready;
    ok(!animation.isRunningOnCompositor);

    var markers = yield observeStyling(5);

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

  add_task(function* no_restyling_main_thread_animations_out_of_view_element() {
    if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
      return;
    }

    var div = addDiv(null,
      { style: 'animation: background-color 100s; transform: translateY(-400px);' });
    var animation = div.getAnimations()[0];

    yield animation.ready;
    var markers = yield observeStyling(5);

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

  add_task_if_omta_enabled(function* no_restyling_compositor_animations_in_scrolled_out_element() {
    if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
      return;
    }

    /*
     On Android the opacity animation runs on the compositor even if it is
     scrolled out of view.  We will fix this in bug 1247800.
     */
    if (isAndroid) {
      return;
    }
    var parentElement = addDiv(null,
      { style: 'overflow-y: scroll; height: 20px;' });
    var div = addDiv(null,
      { style: 'animation: opacity 100s; position: relative; top: 100px;' });
    parentElement.appendChild(div);
    var animation = div.getAnimations()[0];

    yield animation.ready;

    var markers = yield observeStyling(5);

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

    yield ensureElementRemoval(parentElement);
  });

  add_task(function* no_restyling_main_thread_animations_in_scrolled_out_element() {
    if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
      return;
    }

    /*
     On Android throttled animations are left behind on the main thread in some
     frames, We will fix this in bug 1247800.
     */
    if (isAndroid) {
      return;
    }

    var parentElement = addDiv(null,
      { style: 'overflow-y: scroll; height: 20px;' });
    var div = addDiv(null,
      { style: 'animation: background-color 100s; position: relative; top: 100px;' });
    parentElement.appendChild(div);
    var animation = div.getAnimations()[0];

    yield animation.ready;
    var markers = yield observeStyling(5);

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

    yield ensureElementRemoval(parentElement);
  });

  add_task(function* no_restyling_main_thread_animations_in_nested_scrolled_out_element() {
    if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
      return;
    }

    /*
     On Android throttled animations are left behind on the main thread in some
     frames, We will fix this in bug 1247800.
     */
    if (isAndroid) {
      return;
    }

    var grandParent = addDiv(null,
      { style: 'overflow-y: scroll; height: 20px;' });
    var parentElement = addDiv(null,
      { style: 'overflow-y: scroll; height: 100px;' });
    var div = addDiv(null,
      { style: 'animation: background-color 100s; position: relative; top: 100px;' });
    grandParent.appendChild(parentElement);
    parentElement.appendChild(div);
    var animation = div.getAnimations()[0];

    yield animation.ready;
    var markers = yield 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');

    yield ensureElementRemoval(grandParent);
  });

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

    yield animation.ready;
    ok(!animation.isRunningOnCompositor);

    var markers = yield observeStyling(5);

    todo_is(markers.length, 0,
            'Bug 1237454: Animations running on the compositor in ' +
            'visibility hidden element should never cause restyles');
    yield ensureElementRemoval(div);
  });

  add_task(function* restyling_main_thread_animations_moved_in_view_by_scrolling() {
    if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
      return;
    }

    /*
     On Android throttled animations are left behind on the main thread in some
     frames, We will fix this in bug 1247800.
     */
    if (isAndroid) {
      return;
    }

    var parentElement = addDiv(null,
      { style: 'overflow-y: scroll; height: 20px;' });
    var div = addDiv(null,
      { style: 'animation: background-color 100s; position: relative; top: 100px;' });
    parentElement.appendChild(div);
    var animation = div.getAnimations()[0];

    var parentRect = parentElement.getBoundingClientRect();
    var centerX = parentRect.left + parentRect.width / 2;
    var centerY = parentRect.top + parentRect.height / 2;

    yield animation.ready;

    var markers = yield observeStyling(1, function() {
      // We can't use synthesizeWheel here since synthesizeWheel causes
      // layout flush.
      synthesizeWheelAtPoint(centerX, centerY,
                             { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
                               deltaY: 100 });
    });

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

    yield ensureElementRemoval(parentElement);
  });

  add_task(function* restyling_main_thread_animations_moved_in_view_by_scrolling() {
    if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
      return;
    }

    var grandParent = addDiv(null,
      { style: 'overflow-y: scroll; height: 20px;' });
    var parentElement = addDiv(null,
      { style: 'overflow-y: scroll; height: 200px;' });
    var div = addDiv(null,
      { style: 'animation: background-color 100s; position: relative; top: 100px;' });
    grandParent.appendChild(parentElement);
    parentElement.appendChild(div);
    var animation = div.getAnimations()[0];

    var parentRect = grandParent.getBoundingClientRect();
    var centerX = parentRect.left + parentRect.width / 2;
    var centerY = parentRect.top + parentRect.height / 2;

    yield animation.ready;

    var markers = yield observeStyling(1, function() {
      // We can't use synthesizeWheel here since synthesizeWheel causes
      // layout flush.
      synthesizeWheelAtPoint(centerX, centerY,
                             { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
                               deltaY: 100 });
    });

    // FIXME: We should reduce a redundant restyle here.
    ok(markers.length >= 1,
       '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');

    yield ensureElementRemoval(grandParent);
  });

  add_task(function* restyling_main_thread_animations_move_out_of_view_by_scrolling() {
    if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
      return;
    }

    /*
     On Android throttled animations are left behind on the main thread in some
     frames, We will fix this in bug 1247800.
     */
    if (isAndroid) {
      return;
    }

    var parentElement = addDiv(null,
      { style: 'overflow-y: scroll; height: 200px;' });
    var div = addDiv(null,
      { style: 'animation: background-color 100s;' });
    var pad = addDiv(null,
      { style: 'height: 400px;' });
    parentElement.appendChild(div);
    parentElement.appendChild(pad);
    var animation = div.getAnimations()[0];

    var parentRect = parentElement.getBoundingClientRect();
    var centerX = parentRect.left + parentRect.width / 2;
    var centerY = parentRect.top + parentRect.height / 2;

    yield animation.ready;

    // We can't use synthesizeWheel here since synthesizeWheel causes
    // layout flush.
    synthesizeWheelAtPoint(centerX, centerY,
                           { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
                             deltaY: 200 });

    var markers = yield 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');

    yield ensureElementRemoval(parentElement);
  });

  add_task(function* restyling_main_thread_animations_moved_in_view_by_resizing() {
    if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
      return;
    }

    var parentElement = addDiv(null,
      { style: 'overflow-y: scroll; height: 20px;' });
    var div = addDiv(null,
      { style: 'animation: background-color 100s; position: relative; top: 100px;' });
    parentElement.appendChild(div);
    var animation = div.getAnimations()[0];

    yield animation.ready;

    var markers = yield observeStyling(1, function() {
      parentElement.style.height = '100px';
    });

    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');

    yield ensureElementRemoval(parentElement);
  });

  add_task(function* no_restyling_main_thread_animations_in_visiblily_hidden_element() {
    var div = addDiv(null,
     { style: 'animation: background-color 100s; visibility: hidden' });
    var animation = div.getAnimations()[0];

    yield animation.ready;
    var markers = yield observeStyling(5);

    todo_is(markers.length, 0,
            'Bug 1237454: Animations running on the main-thread in ' +
            'visibility hidden element should never cause restyles');
    yield ensureElementRemoval(div);
  });

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

    yield animation.ready;
    ok(animation.isRunningOnCompositor);

    animation.pause();

    yield animation.ready;

    var markers = yield observeStyling(5);
    is(markers.length, 0,
       'Bug 1232563: Paused animations running on the compositor should ' +
       'never cause restyles once after pause() is called');
    yield ensureElementRemoval(div);
  });

  add_task(function* no_restyling_main_thread_animations_after_pause_is_called() {
    var div = addDiv(null, { style: 'animation: background-color 100s' });
    var animation = div.getAnimations()[0];

    yield animation.ready;

    animation.pause();

    yield animation.ready;

    var markers = yield observeStyling(5);
    is(markers.length, 0,
       'Bug 1232563: Paused animations running on the main-thread should ' +
       'never cause restyles after pause() is called');
    yield ensureElementRemoval(div);
  });

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

    yield animation.ready;

    animation.currentTime = 50 * MS_PER_SEC;

    var markers = yield 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');
    yield ensureElementRemoval(div);
  });

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

    yield animation.ready;
    ok(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(!animation.isRunningOnCompositor);

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

    yield ensureElementRemoval(div);
  });

  add_task(function* script_animation_on_display_none_element() {
    var div = addDiv(null);
    var animation = div.animate({ backgroundColor: [ 'red', 'blue' ] },
                                100 * MS_PER_SEC);

    yield animation.ready;

    div.style.display = 'none';

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

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

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

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

    div.style.display = '';

    // We need to wait a frame to unapply display:none style.
    yield waitForFrame();

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

    yield ensureElementRemoval(div);
  });

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

    yield animation.ready;

    div.style.display = 'none';

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

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

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

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

    div.style.display = '';

    // We need to wait a frame to unapply display:none style.
    yield waitForFrame();

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

    yield ensureElementRemoval(div);
  });

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

    yield animation.ready;
    var markers = yield observeStyling(5);

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

    animation.effect.setKeyframes({ backgroundColor: ['red', 'blue'] });
    markers = yield observeStyling(5);

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

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

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

    yield ensureElementRemoval(div);
  });

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

  add_task(function* necessary_update_should_be_invoked() {
    var div = addDiv(null, { style: 'animation: background-color 100s' });
    var animation = div.getAnimations()[0];
    yield animation.ready;
    yield waitForAnimationFrames(5);
    // Apply another animation style
    div.style.animation = 'background-color 110s';
    var animation = div.getAnimations()[0];
    var markers = yield observeStyling(5);
    is(markers.length, 5,
       'Applying animation style with different duration '  +
       'should cause restyles on every frame.');
    yield ensureElementRemoval(div);
  });

  add_task_if_omta_enabled(
    function* changing_cascading_result_for_main_thread_animation() {
      var div = addDiv(null, { style: 'background-color: blue' });
      var animation = div.animate({ opacity: [0, 1],
                                    backgroundColor: ['green', 'red'] },
                                  100 * MS_PER_SEC);
      yield animation.ready;
      ok(animation.isRunningOnCompositor,
         'The opacity animation is running on the compositor');
      // Make the background-color style as !important to cause an update
      // to the cascade.
      // Bug 1300982: The background-color animation should be no longer
      // running on the main thread.
      div.style.setProperty('background-color', '1', 'important');
      var markers = yield 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');
      yield ensureElementRemoval(div);
    }
  );

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

    yield animation.ready;

    div.remove();

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

    document.body.appendChild(div);

    markers = yield observeStyling(1);
    // We are observing restyles in rAF callback which is processed before
    // restyling process in each frame, so in the first frame there should be
    // no observed restyle since we don't process restyle while the element
    // is not attached to the document.
    is(markers.length, 0,
       'We observe no restyle in the first frame right after re-atatching ' +
       'to the document');
    markers = yield observeStyling(5);
    is(markers.length, 5,
       'Animation on re-attached to the document begins to update style');

    yield 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.
    function* restyling_for_opacity_animation_on_re_attached_element() {
      var div = addDiv(null, { style: 'opacity: 1 ! important' });
      var animation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);

      yield animation.ready;
      ok(!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();

      var markers = yield 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.
      yield waitForFrame();

      ok(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');

      yield ensureElementRemoval(div);
    }
  );

  // Tests that additive animations don't throttle at all.
  add_task(function* no_throttling_animations_out_of_view_element() {
    if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling') ||
        !SpecialPowers.getBoolPref('dom.animations-api.core.enabled')) {
      return;
    }

    var div = addDiv(null, { style: 'transform: translateY(-400px);' });
    var animation =
      div.animate([{ visibility: 'visible' }], 100 * MS_PER_SEC);

    yield animation.ready;

    var markers = yield 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');
    yield ensureElementRemoval(div);
  });

  // Counter part of the above test.
  add_task(function* no_restyling_discrete_animations_out_of_view_element() {
    if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling') ||
        !SpecialPowers.getBoolPref('dom.animations-api.core.enabled')) {
      return;
    }

    var div = addDiv(null, { style: 'transform: translateY(-400px);' });
    var animation =
      div.animate({ visibility: ['visible', 'hidden'] }, 100 * MS_PER_SEC);

    yield animation.ready;

    var markers = yield observeStyling(5);

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

  add_task(function* no_restyling_while_computed_timing_is_not_changed() {
    var div = addDiv(null);
    var animation = div.animate({ backgroundColor: [ 'red', 'blue' ] },
                                { duration: 100 * MS_PER_SEC,
                                  easing: 'step-end' });

    yield animation.ready;

    var markers = yield observeStyling(5);

    is(markers.length, 0,
       'Animation running on the main-thread while computed timing is not ' +
       'changed should never cause restyles');
    yield ensureElementRemoval(div);
  });

  add_task(function *no_throttling_animations_in_view_svg() {
    /*
     On Android throttled animations are left behind on the main thread in some
     frames, We will fix this in bug 1247800.
     */
    if (isAndroid) {
      return;
    }

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

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

    yield ensureElementRemoval(div);
  });

  add_task(function *throttling_animations_out_of_view_svg() {
    if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
      return;
    }

    /*
     On Android throttled animations are left behind on the main thread in some
     frames, We will fix this in bug 1247800.
     */
    if (isAndroid) {
      return;
    }

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

    var animation = rect.animate({ fill: ['blue', 'lime'] }, 100 * MS_PER_SEC);
    yield animation.ready;

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

    yield ensureElementRemoval(div);
  });

  add_task(function *no_throttling_animations_in_view_css_transform() {
    /*
     On Android throttled animations are left behind on the main thread in some
     frames, We will fix this in bug 1247800.
     */
    if (isAndroid) {
      return;
    }

    var scrollDiv = addDiv(null, { style: 'overflow: scroll; ' +
                                          'height: 100px; width: 100px;' });
    var targetDiv = addDiv(null,
                           { style: 'animation: background-color 100s;' +
                                    'transform: translate(-50px, -50px);' });
    scrollDiv.appendChild(targetDiv);

    var animation = targetDiv.getAnimations()[0];
    yield animation.ready;

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

    yield ensureElementRemoval(scrollDiv);
  });

  add_task(function *throttling_animations_out_of_view_css_transform() {
    if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
      return;
    }

    /*
     On Android throttled animations are left behind on the main thread in some
     frames, We will fix this in bug 1247800.
     */
    if (isAndroid) {
      return;
    }

    var scrollDiv = addDiv(null, { style: 'overflow: scroll;' +
                                          'height: 100px; width: 100px;' });
    var targetDiv = addDiv(null,
                           { style: 'animation: background-color 100s;' +
                                    'transform: translate(100px, 100px);' });
    scrollDiv.appendChild(targetDiv);

    var animation = targetDiv.getAnimations()[0];
    yield animation.ready;

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

    yield ensureElementRemoval(scrollDiv);
  });

});

</script>
</body>