Source code

Revision control

Copy as Markdown

Other Tools

<!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-from-zero {
from { opacity: 0; }
to { opacity: 1; }
}
@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) {
let priorAnimationTriggeredRestyles = SpecialPowers.DOMWindowUtils.animationTriggeredRestyles;
funcToMakeRestyleHappen();
const restyleCount = SpecialPowers.DOMWindowUtils.animationTriggeredRestyles - priorAnimationTriggeredRestyles;
return restyleCount;
}
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);
}
async function estimateVsyncRate() {
await waitForNextFrame();
const timeAtStart = document.timeline.currentTime;
await waitForAnimationFrames(5);
return (document.timeline.currentTime - timeAtStart) / 5;
}
// We need to wait for all paints before running tests to avoid contaminations
// from styling of this document itself.
waitForAllPaints(async () => {
const vsyncRate = await estimateVsyncRate();
// 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();
return;
}
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 restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount;
restyleCount = await observeStyling(5);
// TODO(bug 1784931): Figure out why we only see four markers sometimes.
// That's not the point of this test tho.
let maybe_todo_is = restyleCount == 4 ? todo_is : is;
maybe_todo_is(restyleCount, 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();
restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount;
restyleCount = await observeStyling(1);
is(restyleCount, 1,
'Animations running on the compositor should only update style once ' +
'after finish() is called');
restyleCount = await observeStyling(1);
todo_is(restyleCount, 0,
'Bug 1415457: Animations running on the compositor should only ' +
'update style once after finish() is called');
restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount;
restyleCount = await observeStyling(1);
is(restyleCount, 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;
restyleCount = await observeStyling(5, () => {
// We can't use synthesizeMouse here since synthesizeMouse causes
// layout flush.
synthesizeMouseAtPoint(mouseX++, mouseY++,
{ type: 'mousemove' }, window);
});
is(restyleCount, 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 restyleCount;
restyleCount = await observeStyling(1);
is(restyleCount, 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;
restyleCount = await observeStyling(5, () => {
// We can't use synthesizeMouse here since synthesizeMouse causes
// layout flush.
synthesizeMouseAtPoint(mouseX++, mouseY++,
{ type: 'mousemove' }, window);
});
is(restyleCount, 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 restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount
let now;
let elapsed;
while (true) {
now = document.timeline.currentTime;
elapsed = (now - timeAtStart);
restyleCount = await observeStyling(1);
if (restyleCount) {
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;
restyleCount = 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 = restyleCount < 2;
else if (elapsed.toPrecision(10) > 200)
expectedMarkersLengthValid = restyleCount == 1;
else
expectedMarkersLengthValid = !restyleCount;
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 restyleCount
let now;
let elapsed;
while (true) {
now = document.timeline.currentTime;
elapsed = (now - timeAtStart);
restyleCount = await observeStyling(1);
if (restyleCount) {
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;
restyleCount = 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 = restyleCount < 2;
else if (elapsed.toPrecision(10) > 200)
expectedMarkersLengthValid = restyleCount == 1;
else
expectedMarkersLengthValid = !restyleCount;
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 restyleCount = await observeStyling(20);
is(restyleCount, 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 restyleCount;
restyleCount = await observeStyling(5);
is(restyleCount, 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();
restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount;
restyleCount = await observeStyling(5);
is(restyleCount, 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();
restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount = await observeStyling(5);
// FIXME: We should reduce a redundant restyle here.
ok(restyleCount >= 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 restyleCount;
restyleCount = await observeStyling(5);
is(restyleCount, 0,
'Animations running on the main-thread which is in scrolled out ' +
'elements should not update restyling');
parentElement.style.height = '100px';
restyleCount = await observeStyling(1);
is(restyleCount, 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 restyleCount = await observeStyling(5);
todo_is(restyleCount, 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 restyleCount;
restyleCount = await observeStyling(5);
is(restyleCount, 0,
'Animations running on visibility hidden element should never ' +
'cause restyles');
div.style.visibility = 'visible';
await waitForNextFrame();
restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount;
restyleCount = await observeStyling(5);
is(restyleCount, 0,
'Animations running in visibility hidden parent should never cause ' +
'restyles');
parentDiv.style.visibility = 'visible';
await waitForNextFrame();
restyleCount = await observeStyling(5);
is(restyleCount, 5,
'Animations that was in visibility hidden parent should not ' +
'throttle restyling any more');
parentDiv.style.visibility = 'hidden';
await waitForNextFrame();
restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount;
restyleCount = await observeStyling(5);
is(restyleCount, 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();
restyleCount = await observeStyling(5);
is(restyleCount, 5,
'Animations running on visibility hidden element but the element has ' +
'a visible child should not throttle restyling');
childElement.style.visibility = 'hidden';
await waitForNextFrame();
restyleCount = await observeStyling(5);
todo_is(restyleCount, 0,
'Animations running on visibility hidden element that a child ' +
'has become invisible should throttle restyling');
childElement.style.visibility = 'visible';
await waitForNextFrame();
restyleCount = await observeStyling(5);
is(restyleCount, 5,
'Animations running on visibility hidden element should not throttle ' +
'restyling after the invisible element changed to visible');
childElement.remove();
await waitForNextFrame();
restyleCount = await observeStyling(5);
todo_is(restyleCount, 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 restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount;
restyleCount = await observeStyling(5);
is(restyleCount, 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();
restyleCount = await observeStyling(5);
is(restyleCount, 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();
restyleCount = await observeStyling(5);
is(restyleCount, 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();
restyleCount = await observeStyling(5);
is(restyleCount, 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();
restyleCount = await observeStyling(5);
is(restyleCount, 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();
restyleCount = await observeStyling(5);
is(restyleCount, 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();
restyleCount = await observeStyling(5);
is(restyleCount, 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();
restyleCount = await observeStyling(5);
todo_is(restyleCount, 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();
restyleCount = await observeStyling(5);
is(restyleCount, 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();
restyleCount = await observeStyling(5);
todo_is(restyleCount, 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 restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount;
restyleCount = await observeStyling(5);
is(restyleCount, 0,
'Script animations on "display: none" element should not update styles');
div.style.display = '';
restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount;
restyleCount = await observeStyling(5);
is(restyleCount, 0,
'Opacity script animations on "display: none" element should not ' +
'update styles');
div.style.display = '';
restyleCount = await observeStyling(1);
is(restyleCount, 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 restyleCount;
restyleCount = await observeStyling(5);
is(restyleCount, 0,
'Animations with no keyframes should not cause restyles');
animation.effect.setKeyframes({ zIndex: ['0', '999'] });
restyleCount = await observeStyling(5);
is(restyleCount, 5,
'Setting valid keyframes should cause regular animation restyles to ' +
'occur');
animation.effect.setKeyframes({ });
restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount = 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(restyleCount, 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 restyleCount = await observeStyling(5);
todo_is(restyleCount, 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 restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount;
restyleCount = await observeStyling(5);
is(restyleCount, 0,
'Animation on orphaned element should not cause restyles');
document.body.appendChild(div);
await waitForNextFrame();
restyleCount = await observeStyling(5);
is(restyleCount, 5,
'Animation on re-attached to the document begins to update style, got ' + restyleCount);
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 restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount = await observeStyling(5);
is(restyleCount, 0,
'Additive animation has no keyframe whose offset is 0 or 1 in an ' +
'out-of-view element should 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 restyleCount = await observeStyling(5);
is(restyleCount, 0,
'Discrete animation has has no keyframe whose offset is 0 or 1 in an ' +
'out-of-view element should 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 restyleCount = await observeStyling(5);
is(restyleCount, 0,
'visibility animation has no keyframe whose offset is 0 or 1 in an ' +
'out-of-view element should 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 restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount = await observeStyling(5);
// We possibly expect one restyle from the initial animation compose, in
// order to update animations, but nothing else.
ok(restyleCount <= 1,
'Animation running on the main-thread while computed timing is not ' +
'changed should not cause extra restyles, got ' + restyleCount);
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 restyleCount = await observeStyling(5);
is(restyleCount, 5,
'CSS animations on an in-view svg element with post-transform should ' +
'not be throttled.');
await ensureElementRemoval(div);
});
add_task_if_omta_enabled(async function svg_non_scaling_stroke_animation() {
const div = addDiv(null, { style: 'overflow: scroll;' +
'height: 100px; width: 100px;' });
const svg = addSVGElement(div, 'svg', { viewBox: '0 0 250 250',
width: '40px',
height: '40px' });
const rect = addSVGElement(svg, 'rect', { x: '0',
y: '0',
width: '250',
height: '250',
fill: 'red',
style: 'vector-effect: non-scaling-stroke; animation: rotate 100s infinite;'});
const animation = rect.getAnimations()[0];
await waitForAnimationReadyToRestyle(animation);
ok(!SpecialPowers.wrap(animation).isRunningOnCompositor,
'The animation of a non-scaling-stroke element is not running on the compositor');
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 restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount = await observeStylingInTargetWindow(iframe.contentWindow, 5);
is(restyleCount, 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 restyleCount = await observeStylingInTargetWindow(iframe.contentWindow, 5);
is(restyleCount, 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 restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount = await observeStyling(5);
todo_is(restyleCount, 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 restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount = await observeStyling(20);
is(restyleCount, 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 restyleCount = observeAnimSyncStyling(() => {
is(div.getAnimations().length, 1, 'There should be one animation');
});
is(restyleCount, 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 restyleCount = observeAnimSyncStyling(() => {
div.style.animationDuration = '0s';
is(div.getAnimations().length, 0, 'There should be no animation');
});
is(restyleCount, 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 restyleCount = observeAnimSyncStyling(() => {
sibling.style.opacity = '0.5';
is(animation.playState, 'running',
'Animation.playState should be running');
});
is(restyleCount, 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 restyleCount = observeAnimSyncStyling(() => {
div.style.animationPlayState = 'paused';
is(animation.playState, 'paused',
'Animation.playState should be reflected by pending style');
});
is(restyleCount, 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 restyleCount = observeAnimSyncStyling(() => {
sibling.style.opacity = '0.5';
is(transition.playState, 'running',
'Animation.playState should be running');
});
is(restyleCount, 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 restyleCount = observeAnimSyncStyling(() => {
div.style.transitionProperty = 'none';
is(transition.playState, 'idle',
'Animation.playState should be reflected by pending style change ' +
'which cancel the transition');
});
is(restyleCount, 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 restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount = await observeStylingInTargetWindow(iframe.contentWindow, 5);
is(restyleCount, 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 restyleCount = await observeStyling(5)
is(restyleCount, 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();
let priorAnimationTriggeredRestyles = SpecialPowers.DOMWindowUtils.animationTriggeredRestyles;
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 restyleCount = SpecialPowers.DOMWindowUtils.animationTriggeredRestyles - priorAnimationTriggeredRestyles;
is(restyleCount, 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 restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount = await observeStyling(5);
is(restyleCount, 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 restyleCount = await observeStyling(5);
is(restyleCount, 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-from-zero 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 restyleCount = await observeStyling(5);
is(restyleCount, 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 restyling_omt_animations_in_opacity_zero_descendant_during_root_opacity_animation() {
const container = addDiv(null, { style: 'opacity: 0; animation: opacity-from-zero 100s infinite' });
const child = addDiv(null, { style: 'animation: rotate 100s infinite' });
container.appendChild(child);
const animation = child.getAnimations()[0];
await waitForAnimationReadyToRestyle(animation);
ok(SpecialPowers.wrap(animation).isRunningOnCompositor);
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 restyleCount = await observeStyling(5);
is(restyleCount, 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 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 restyleCount = await content.wrappedJSObject.observeStyling(5);
is(restyleCount, 0,
'transform animation on a collapsed element animation should not ' +
'update styles on the main thread');
});
await ensureElementRemoval(iframe);
});
});
</script>
</body>