summaryrefslogtreecommitdiffstats
path: root/dom/animation/test/mozilla/file_restyles.html
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--dom/animation/test/mozilla/file_restyles.html2275
1 files changed, 2275 insertions, 0 deletions
diff --git a/dom/animation/test/mozilla/file_restyles.html b/dom/animation/test/mozilla/file_restyles.html
new file mode 100644
index 0000000000..8d72cb6c44
--- /dev/null
+++ b/dom/animation/test/mozilla/file_restyles.html
@@ -0,0 +1,2275 @@
+<!doctype html>
+<head>
+<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
+<meta charset=utf-8>
+<title>Tests restyles caused by animations</title>
+<script>
+const ok = opener.ok.bind(opener);
+const is = opener.is.bind(opener);
+const todo = opener.todo.bind(opener);
+const todo_is = opener.todo_is.bind(opener);
+const info = opener.info.bind(opener);
+const original_finish = opener.SimpleTest.finish;
+const SimpleTest = opener.SimpleTest;
+const add_task = opener.add_task;
+SimpleTest.finish = function finish() {
+ self.close();
+ original_finish();
+}
+</script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<script src="../testcommon.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+<style>
+@keyframes background-position {
+ 0% {
+ background-position: -25px center;
+ }
+
+ 40%,
+ 100% {
+ background-position: 36px center;
+ }
+}
+@keyframes opacity {
+ from { opacity: 1; }
+ to { opacity: 0; }
+}
+@keyframes opacity-without-end-value {
+ from { opacity: 0; }
+}
+@keyframes on-main-thread {
+ from { z-index: 0; }
+ to { z-index: 999; }
+}
+@keyframes rotate {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+}
+@keyframes move-in {
+ from { transform: translate(120%, 120%); }
+ to { transform: translate(0%, 0%); }
+}
+@keyframes background-color {
+ from { background-color: rgb(255, 0, 0,); }
+ to { background-color: rgb(0, 255, 0,); }
+}
+div {
+ /* Element needs geometry to be eligible for layerization */
+ width: 100px;
+ height: 100px;
+ background-color: white;
+}
+progress:not(.stop)::-moz-progress-bar {
+ animation: on-main-thread 100s;
+}
+body {
+ /*
+ * set overflow:hidden to avoid accidentally unthrottling animations to update
+ * the overflow region.
+ */
+ overflow: hidden;
+}
+</style>
+</head>
+<body>
+<script>
+'use strict';
+
+// Returns observed animation restyle markers when |funcToMakeRestyleHappen|
+// is called.
+// NOTE: This function is synchronous version of the above observeStyling().
+// Unlike the above observeStyling, this function takes a callback function,
+// |funcToMakeRestyleHappen|, which may be expected to trigger a synchronous
+// restyles, and returns any restyle markers produced by calling that function.
+function observeAnimSyncStyling(funcToMakeRestyleHappen) {
+ const docShell = getDocShellForObservingRestylesForWindow(window);
+
+ funcToMakeRestyleHappen();
+
+ const markers = docShell.popProfileTimelineMarkers();
+ docShell.recordProfileTimelineMarkers = false;
+ return Array.prototype.filter.call(markers, (marker, index) => {
+ return marker.name == 'Styles' && marker.isAnimationOnly;
+ });
+}
+
+function ensureElementRemoval(aElement) {
+ return new Promise(resolve => {
+ aElement.remove();
+ waitForAllPaintsFlushed(resolve);
+ });
+}
+
+function waitForWheelEvent(aTarget) {
+ return new Promise(resolve => {
+ // Get the scrollable target element position in this window coordinate
+ // system to send a wheel event to the element.
+ const targetRect = aTarget.getBoundingClientRect();
+ const centerX = targetRect.left + targetRect.width / 2;
+ const centerY = targetRect.top + targetRect.height / 2;
+
+ sendWheelAndPaintNoFlush(aTarget, centerX, centerY,
+ { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
+ deltaY: targetRect.height },
+ resolve);
+ });
+}
+
+const omtaEnabled = isOMTAEnabled();
+
+function add_task_if_omta_enabled(test) {
+ if (!omtaEnabled) {
+ info(test.name + " is skipped because OMTA is disabled");
+ return;
+ }
+ add_task(test);
+}
+
+// We need to wait for all paints before running tests to avoid contaminations
+// from styling of this document itself.
+waitForAllPaints(() => {
+ add_task(async function () {
+ // Start vsync rate measurement in after a RAF callback.
+ await waitForNextFrame();
+
+ const timeAtStart = document.timeline.currentTime;
+ await waitForAnimationFrames(5);
+ const vsyncRate = (document.timeline.currentTime - timeAtStart) / 5;
+
+ // In this test we basically observe restyling counts in 5 frames, if it
+ // takes over 200ms during the 5 frames, this test will fail. So
+ // "200ms / 5 = 40ms" is a threshold whether the test works as expected or
+ // not. We'd take 5ms additional tolerance here.
+ // Note that the 200ms is a period we unthrottle throttled animations that
+ // at least one of the animating styles produces change hints causing
+ // overflow, the value is defined in
+ // KeyframeEffect::OverflowRegionRefreshInterval.
+ if (vsyncRate > 40 - 5) {
+ ok(true, `the vsync rate ${vsyncRate} on this machine is too slow to run this test`);
+ SimpleTest.finish();
+ }
+ });
+
+ add_task(async function restyling_for_main_thread_animations() {
+ const div = addDiv(null, { style: 'animation: on-main-thread 100s' });
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const markers = await observeStyling(5);
+ is(markers.length, 5,
+ 'CSS animations running on the main-thread should update style ' +
+ 'on the main thread');
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function restyling_for_main_thread_animations_progress_bar_pseudo() {
+ const progress = document.createElement("progress");
+ document.body.appendChild(progress);
+
+ await waitForNextFrame();
+ await waitForNextFrame();
+
+ let markers = await observeStyling(5);
+ is(markers.length, 5,
+ 'CSS animations running on the main-thread should update style ' +
+ 'on the main thread on ::-moz-progress-bar');
+ progress.classList.add("stop");
+ await waitForNextFrame();
+ await waitForNextFrame();
+
+ markers = await observeStyling(5);
+ is(markers.length, 0, 'Animation is correctly removed');
+ await ensureElementRemoval(progress);
+ });
+
+ add_task_if_omta_enabled(async function no_restyling_for_compositor_animations() {
+ const div = addDiv(null, { style: 'animation: opacity 100s' });
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'CSS animations running on the compositor should not update style ' +
+ 'on the main thread');
+ await ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(async function no_restyling_for_compositor_transitions() {
+ const div = addDiv(null, { style: 'transition: opacity 100s; opacity: 0' });
+ getComputedStyle(div).opacity;
+ div.style.opacity = 1;
+
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'CSS transitions running on the compositor should not update style ' +
+ 'on the main thread');
+ await ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(async function no_restyling_when_animation_duration_is_changed() {
+ const div = addDiv(null, { style: 'animation: opacity 100s' });
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ div.animationDuration = '200s';
+
+ const markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'Animations running on the compositor should not update style ' +
+ 'on the main thread');
+ await ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(async function only_one_restyling_after_finish_is_called() {
+ const div = addDiv(null, { style: 'animation: opacity 100s' });
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ animation.finish();
+
+ let markers = await observeStyling(1);
+ is(markers.length, 1,
+ 'Animations running on the compositor should only update style once ' +
+ 'after finish() is called');
+
+ markers = await observeStyling(1);
+ todo_is(markers.length, 0,
+ 'Bug 1415457: Animations running on the compositor should only ' +
+ 'update style once after finish() is called');
+
+ markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'Finished animations should never update style after one ' +
+ 'restyle happened for finish()');
+
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function no_restyling_mouse_movement_on_finished_transition() {
+ const div = addDiv(null, { style: 'transition: opacity 1ms; opacity: 0' });
+ getComputedStyle(div).opacity;
+ div.style.opacity = 1;
+
+ const animation = div.getAnimations()[0];
+ const initialRect = div.getBoundingClientRect();
+
+ await animation.finished;
+ let markers = await observeStyling(1);
+ is(markers.length, 1,
+ 'Finished transitions should restyle once after Animation.finished ' +
+ 'was fulfilled');
+
+ let mouseX = initialRect.left + initialRect.width / 2;
+ let mouseY = initialRect.top + initialRect.height / 2;
+ markers = await observeStyling(5, () => {
+ // We can't use synthesizeMouse here since synthesizeMouse causes
+ // layout flush.
+ synthesizeMouseAtPoint(mouseX++, mouseY++,
+ { type: 'mousemove' }, window);
+ });
+
+ is(markers.length, 0,
+ 'Finished transitions should never cause restyles when mouse is moved ' +
+ 'on the transitions');
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function no_restyling_mouse_movement_on_finished_animation() {
+ const div = addDiv(null, { style: 'animation: opacity 1ms' });
+ const animation = div.getAnimations()[0];
+
+ const initialRect = div.getBoundingClientRect();
+
+ await animation.finished;
+ let markers = await observeStyling(1);
+ is(markers.length, 1,
+ 'Finished animations should restyle once after Animation.finished ' +
+ 'was fulfilled');
+
+ let mouseX = initialRect.left + initialRect.width / 2;
+ let mouseY = initialRect.top + initialRect.height / 2;
+ markers = await observeStyling(5, () => {
+ // We can't use synthesizeMouse here since synthesizeMouse causes
+ // layout flush.
+ synthesizeMouseAtPoint(mouseX++, mouseY++,
+ { type: 'mousemove' }, window);
+ });
+
+ is(markers.length, 0,
+ 'Finished animations should never cause restyles when mouse is moved ' +
+ 'on the animations');
+ await ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(async function no_restyling_compositor_animations_out_of_view_element() {
+ const div = addDiv(null,
+ { style: 'animation: opacity 100s; transform: translateY(-400px);' });
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations running on the compositor in an out-of-view element ' +
+ 'should never cause restyles');
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function no_restyling_main_thread_animations_out_of_view_element() {
+ const div = addDiv(null,
+ { style: 'animation: on-main-thread 100s; transform: translateY(-400px);' });
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ const markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations running on the main-thread in an out-of-view element ' +
+ 'should never cause restyles');
+ await ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(async function no_restyling_compositor_animations_in_scrolled_out_element() {
+ const parentElement = addDiv(null,
+ { style: 'overflow-y: scroll; height: 20px;' });
+ const div = addDiv(null,
+ { style: 'animation: opacity 100s; position: relative; top: 100px;' });
+ parentElement.appendChild(div);
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations running on the compositor for elements ' +
+ 'which are scrolled out should never cause restyles');
+
+ await ensureElementRemoval(parentElement);
+ });
+
+ add_task(
+ async function no_restyling_missing_keyframe_opacity_animations_on_scrolled_out_element() {
+ const parentElement = addDiv(null,
+ { style: 'overflow-y: scroll; height: 20px;' });
+ const div = addDiv(null,
+ { style: 'animation: opacity-without-end-value 100s; ' +
+ 'position: relative; top: 100px;' });
+ parentElement.appendChild(div);
+ const animation = div.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Opacity animations on scrolled out elements should never cause ' +
+ 'restyles even if the animation has missing keyframes');
+
+ await ensureElementRemoval(parentElement);
+ }
+ );
+
+ add_task(
+ async function restyling_transform_animations_in_scrolled_out_element() {
+ // Make sure we start from the state right after requestAnimationFrame.
+ await waitForFrame();
+
+ const parentElement = addDiv(null,
+ { style: 'overflow-y: scroll; height: 20px;' });
+ const div = addDiv(null,
+ { style: 'animation: rotate 100s infinite; position: relative; top: 100px;' });
+ parentElement.appendChild(div);
+ const animation = div.getAnimations()[0];
+ let timeAtStart = document.timeline.currentTime;
+
+ ok(!animation.isRunningOnCompositor,
+ 'The transform animation is not running on the compositor');
+
+ let markers;
+ let now;
+ let elapsed;
+ while (true) {
+ now = document.timeline.currentTime;
+ elapsed = (now - timeAtStart);
+ markers = await observeStyling(1);
+ if (markers.length) {
+ break;
+ }
+ }
+ // If the current time has elapsed over 200ms since the animation was
+ // created, it means that the animation should have already
+ // unthrottled in this tick, let's see what we observe in this tick's
+ // restyling process.
+ // We use toPrecision here and below so 199.99999999999977 will turn into 200.
+ ok(elapsed.toPrecision(10) >= 200,
+ 'Transform animation running on the element which is scrolled out ' +
+ 'should be throttled until 200ms is elapsed. now: ' +
+ now + ' start time: ' + timeAtStart + ' elapsed:' + elapsed);
+
+ timeAtStart = document.timeline.currentTime;
+ markers = await observeStyling(1);
+ now = document.timeline.currentTime;
+ elapsed = (now - timeAtStart);
+
+ let expectedMarkersLengthValid;
+ // On the fence of 200 ms, we probably have 1 marker; but if we hit a bad rounding
+ // we might still have 0. But if it's > 200, we should have 1; and less we should have 0.
+ if (elapsed.toPrecision(10) == 200)
+ expectedMarkersLengthValid = markers.length < 2;
+ else if (elapsed.toPrecision(10) > 200)
+ expectedMarkersLengthValid = markers.length == 1;
+ else
+ expectedMarkersLengthValid = !markers.length;
+ ok(expectedMarkersLengthValid,
+ 'Transform animation running on the element which is scrolled out ' +
+ 'should be unthrottled after around 200ms have elapsed. now: ' +
+ now + ' start time: ' + timeAtStart + ' elapsed: ' + elapsed);
+
+ await ensureElementRemoval(parentElement);
+ }
+ );
+
+ add_task(
+ async function restyling_out_of_view_transform_animations_in_another_element() {
+ // Make sure we start from the state right after requestAnimationFrame.
+ await waitForFrame();
+
+ const parentElement = addDiv(null,
+ { style: 'overflow: hidden;' });
+ const div = addDiv(null,
+ { style: 'animation: move-in 100s infinite;' });
+ parentElement.appendChild(div);
+ const animation = div.getAnimations()[0];
+ let timeAtStart = document.timeline.currentTime;
+
+ ok(!animation.isRunningOnCompositor,
+ 'The transform animation on out of view element ' +
+ 'is not running on the compositor');
+
+ // Structure copied from restyling_transform_animations_in_scrolled_out_element
+ let markers;
+ let now;
+ let elapsed;
+ while (true) {
+ now = document.timeline.currentTime;
+ elapsed = (now - timeAtStart);
+ markers = await observeStyling(1);
+ if (markers.length) {
+ break;
+ }
+ }
+
+ ok(elapsed.toPrecision(10) >= 200,
+ 'Transform animation running on out of view element ' +
+ 'should be throttled until 200ms is elapsed. now: ' +
+ now + ' start time: ' + timeAtStart + ' elapsed:' + elapsed);
+
+ timeAtStart = document.timeline.currentTime;
+ markers = await observeStyling(1);
+ now = document.timeline.currentTime;
+ elapsed = (now - timeAtStart);
+
+ let expectedMarkersLengthValid;
+ // On the fence of 200 ms, we probably have 1 marker; but if we hit a bad rounding
+ // we might still have 0. But if it's > 200, we should have 1; and less we should have 0.
+ if (elapsed.toPrecision(10) == 200)
+ expectedMarkersLengthValid = markers.length < 2;
+ else if (elapsed.toPrecision(10) > 200)
+ expectedMarkersLengthValid = markers.length == 1;
+ else
+ expectedMarkersLengthValid = !markers.length;
+ ok(expectedMarkersLengthValid,
+ 'Transform animation running on out of view element ' +
+ 'should be unthrottled after around 200ms have elapsed. now: ' +
+ now + ' start time: ' + timeAtStart + ' elapsed: ' + elapsed);
+
+ await ensureElementRemoval(parentElement);
+ }
+ );
+
+ add_task(async function finite_transform_animations_in_out_of_view_element() {
+ const parentElement = addDiv(null, { style: 'overflow: hidden;' });
+ const div = addDiv(null);
+ const animation =
+ div.animate({ transform: [ 'translateX(120%)', 'translateX(100%)' ] },
+ // This animation will move a bit but
+ // will remain out-of-view.
+ 100 * MS_PER_SEC);
+ parentElement.appendChild(div);
+
+ await waitForAnimationReadyToRestyle(animation);
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor, "Should not be running in compositor");
+
+ const markers = await observeStyling(20);
+ is(markers.length, 20,
+ 'Finite transform animation in out-of-view element should never be ' +
+ 'throttled');
+
+ await ensureElementRemoval(parentElement);
+ });
+
+ add_task(async function restyling_main_thread_animations_in_scrolled_out_element() {
+ const parentElement = addDiv(null,
+ { style: 'overflow-y: scroll; height: 20px;' });
+ const div = addDiv(null,
+ { style: 'animation: on-main-thread 100s; position: relative; top: 20px;' });
+ parentElement.appendChild(div);
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ let markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations running on the main-thread for elements ' +
+ 'which are scrolled out should never cause restyles');
+
+ await waitForWheelEvent(parentElement);
+
+ // Make sure we are ready to restyle before counting restyles.
+ await waitForFrame();
+
+ markers = await observeStyling(5);
+ is(markers.length, 5,
+ 'Animations running on the main-thread which were in scrolled out ' +
+ 'elements should update restyling soon after the element moved in ' +
+ 'view by scrolling');
+
+ await ensureElementRemoval(parentElement);
+ });
+
+ add_task(async function restyling_main_thread_animations_in_nested_scrolled_out_element() {
+ const grandParent = addDiv(null,
+ { style: 'overflow-y: scroll; height: 20px;' });
+ const parentElement = addDiv(null,
+ { style: 'overflow-y: scroll; height: 100px;' });
+ const div = addDiv(null,
+ { style: 'animation: on-main-thread 100s; ' +
+ 'position: relative; ' +
+ 'top: 20px;' }); // This element is in-view in the parent, but
+ // out of view in the grandparent.
+ grandParent.appendChild(parentElement);
+ parentElement.appendChild(div);
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ let markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations running on the main-thread which are in nested elements ' +
+ 'which are scrolled out should never cause restyles');
+
+ await waitForWheelEvent(grandParent);
+
+ await waitForFrame();
+
+ markers = await observeStyling(5);
+ is(markers.length, 5,
+ 'Animations running on the main-thread which were in nested scrolled ' +
+ 'out elements should update restyle soon after the element moved ' +
+ 'in view by scrolling');
+
+ await ensureElementRemoval(grandParent);
+ });
+
+ add_task_if_omta_enabled(async function no_restyling_compositor_animations_in_visibility_hidden_element() {
+ const div = addDiv(null,
+ { style: 'animation: opacity 100s; visibility: hidden' });
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations running on the compositor in visibility hidden element ' +
+ 'should never cause restyles');
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function restyling_main_thread_animations_move_out_of_view_by_scrolling() {
+ const parentElement = addDiv(null,
+ { style: 'overflow-y: scroll; height: 200px;' });
+ const div = addDiv(null,
+ { style: 'animation: on-main-thread 100s;' });
+ const pad = addDiv(null,
+ { style: 'height: 400px;' });
+ parentElement.appendChild(div);
+ parentElement.appendChild(pad);
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ await waitForWheelEvent(parentElement);
+
+ await waitForFrame();
+
+ const markers = await observeStyling(5);
+
+ // FIXME: We should reduce a redundant restyle here.
+ ok(markers.length >= 0,
+ 'Animations running on the main-thread which are in scrolled out ' +
+ 'elements should throttle restyling');
+
+ await ensureElementRemoval(parentElement);
+ });
+
+ add_task(async function restyling_main_thread_animations_moved_in_view_by_resizing() {
+ const parentElement = addDiv(null,
+ { style: 'overflow-y: scroll; height: 20px;' });
+ const div = addDiv(null,
+ { style: 'animation: on-main-thread 100s; position: relative; top: 100px;' });
+ parentElement.appendChild(div);
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ let markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'Animations running on the main-thread which is in scrolled out ' +
+ 'elements should not update restyling');
+
+ parentElement.style.height = '100px';
+ markers = await observeStyling(1);
+
+ is(markers.length, 1,
+ 'Animations running on the main-thread which was in scrolled out ' +
+ 'elements should update restyling soon after the element moved in ' +
+ 'view by resizing');
+
+ await ensureElementRemoval(parentElement);
+ });
+
+ add_task(
+ async function restyling_animations_on_visibility_changed_element_having_child() {
+ const div = addDiv(null,
+ { style: 'animation: on-main-thread 100s;' });
+ const childElement = addDiv(null);
+ div.appendChild(childElement);
+
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ // We don't check the animation causes restyles here since we already
+ // check it in the first test case.
+
+ div.style.visibility = 'hidden';
+ await waitForNextFrame();
+
+ const markers = await observeStyling(5);
+ todo_is(markers.length, 0,
+ 'Animations running on visibility hidden element which ' +
+ 'has a child whose visiblity is inherited from the element and ' +
+ 'the element was initially visible');
+
+ await ensureElementRemoval(div);
+ }
+ );
+
+ add_task(
+ async function restyling_animations_on_visibility_hidden_element_which_gets_visible() {
+ const div = addDiv(null,
+ { style: 'animation: on-main-thread 100s; visibility: hidden' });
+ const animation = div.getAnimations()[0];
+
+
+ await waitForAnimationReadyToRestyle(animation);
+ let markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations running on visibility hidden element should never ' +
+ 'cause restyles');
+
+ div.style.visibility = 'visible';
+ await waitForNextFrame();
+
+ markers = await observeStyling(5);
+ is(markers.length, 5,
+ 'Animations running that was on visibility hidden element which ' +
+ 'gets visible should not throttle restyling any more');
+
+ await ensureElementRemoval(div);
+ }
+ );
+
+ add_task(async function restyling_animations_in_visibility_changed_parent() {
+ const parentDiv = addDiv(null, { style: 'visibility: hidden' });
+ const div = addDiv(null, { style: 'animation: on-main-thread 100s;' });
+ parentDiv.appendChild(div);
+
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ let markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations running in visibility hidden parent should never cause ' +
+ 'restyles');
+
+ parentDiv.style.visibility = 'visible';
+ await waitForNextFrame();
+
+ markers = await observeStyling(5);
+ is(markers.length, 5,
+ 'Animations that was in visibility hidden parent should not ' +
+ 'throttle restyling any more');
+
+ parentDiv.style.visibility = 'hidden';
+ await waitForNextFrame();
+
+ markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'Animations that the parent element became visible should throttle ' +
+ 'restyling again');
+
+ await ensureElementRemoval(parentDiv);
+ });
+
+ add_task(
+ async function restyling_animations_on_visibility_hidden_element_with_visibility_changed_children() {
+ const div = addDiv(null,
+ { style: 'animation: on-main-thread 100s; visibility: hidden' });
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ let markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations on visibility hidden element having no visible children ' +
+ 'should never cause restyles');
+
+ const childElement = addDiv(null, { style: 'visibility: visible' });
+ div.appendChild(childElement);
+ await waitForNextFrame();
+
+ markers = await observeStyling(5);
+ is(markers.length, 5,
+ 'Animations running on visibility hidden element but the element has ' +
+ 'a visible child should not throttle restyling');
+
+ childElement.style.visibility = 'hidden';
+ await waitForNextFrame();
+
+ markers = await observeStyling(5);
+ todo_is(markers.length, 0,
+ 'Animations running on visibility hidden element that a child ' +
+ 'has become invisible should throttle restyling');
+
+ childElement.style.visibility = 'visible';
+ await waitForNextFrame();
+
+ markers = await observeStyling(5);
+ is(markers.length, 5,
+ 'Animations running on visibility hidden element should not throttle ' +
+ 'restyling after the invisible element changed to visible');
+
+ childElement.remove();
+ await waitForNextFrame();
+
+ markers = await observeStyling(5);
+ todo_is(markers.length, 0,
+ 'Animations running on visibility hidden element should throttle ' +
+ 'restyling again after all visible descendants were removed');
+
+ await ensureElementRemoval(div);
+ }
+ );
+
+ add_task(
+ async function restyling_animations_on_visiblity_hidden_element_having_oof_child() {
+ const div = addDiv(null,
+ { style: 'animation: on-main-thread 100s; position: absolute' });
+ const childElement = addDiv(null,
+ { style: 'float: left; visibility: hidden' });
+ div.appendChild(childElement);
+
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ // We don't check the animation causes restyles here since we already
+ // check it in the first test case.
+
+ div.style.visibility = 'hidden';
+ await waitForNextFrame();
+
+ const markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'Animations running on visibility hidden element which has an ' +
+ 'out-of-flow child should throttle restyling');
+
+ await ensureElementRemoval(div);
+ }
+ );
+
+ add_task(
+ async function restyling_animations_on_visibility_hidden_element_having_grandchild() {
+ // element tree:
+ //
+ // root(visibility:hidden)
+ // / \
+ // childA childB
+ // / \ / \
+ // AA AB BA BB
+
+ const div = addDiv(null,
+ { style: 'animation: on-main-thread 100s; visibility: hidden' });
+
+ const childA = addDiv(null);
+ div.appendChild(childA);
+ const childB = addDiv(null);
+ div.appendChild(childB);
+
+ const grandchildAA = addDiv(null);
+ childA.appendChild(grandchildAA);
+ const grandchildAB = addDiv(null);
+ childA.appendChild(grandchildAB);
+
+ const grandchildBA = addDiv(null);
+ childB.appendChild(grandchildBA);
+ const grandchildBB = addDiv(null);
+ childB.appendChild(grandchildBB);
+
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ let markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'Animations on visibility hidden element having no visible ' +
+ 'descendants should never cause restyles');
+
+ childA.style.visibility = 'visible';
+ grandchildAA.style.visibility = 'visible';
+ grandchildAB.style.visibility = 'visible';
+ await waitForNextFrame();
+
+ markers = await observeStyling(5);
+ is(markers.length, 5,
+ 'Animations running on visibility hidden element but the element has ' +
+ 'visible children should not throttle restyling');
+
+ // Make childA hidden again but both of grandchildAA and grandchildAB are
+ // still visible.
+ childA.style.visibility = 'hidden';
+ await waitForNextFrame();
+
+ markers = await observeStyling(5);
+ is(markers.length, 5,
+ 'Animations running on visibility hidden element that a child has ' +
+ 'become invisible again but there are still visible children should ' +
+ 'not throttle restyling');
+
+ // Make grandchildAA hidden but grandchildAB is still visible.
+ grandchildAA.style.visibility = 'hidden';
+ await waitForNextFrame();
+
+ markers = await observeStyling(5);
+ is(markers.length, 5,
+ 'Animations running on visibility hidden element that a grandchild ' +
+ 'become invisible again but another grandchild is still visible ' +
+ 'should not throttle restyling');
+
+
+ // Make childB and grandchildBA visible.
+ childB.style.visibility = 'visible';
+ grandchildBA.style.visibility = 'visible';
+ await waitForNextFrame();
+
+ markers = await observeStyling(5);
+ is(markers.length, 5,
+ 'Animations running on visibility hidden element but the element has ' +
+ 'visible descendants should not throttle restyling');
+
+ // Make childB hidden but grandchildAB and grandchildBA are still visible.
+ childB.style.visibility = 'hidden';
+ await waitForNextFrame();
+
+ markers = await observeStyling(5);
+ is(markers.length, 5,
+ 'Animations running on visibility hidden element but the element has ' +
+ 'visible grandchildren should not throttle restyling');
+
+ // Make grandchildAB hidden but grandchildBA is still visible.
+ grandchildAB.style.visibility = 'hidden';
+ await waitForNextFrame();
+
+ markers = await observeStyling(5);
+ is(markers.length, 5,
+ 'Animations running on visibility hidden element but the element has ' +
+ 'a visible grandchild should not throttle restyling');
+
+ // Make grandchildBA hidden. Now all descedants are invisible.
+ grandchildBA.style.visibility = 'hidden';
+ await waitForNextFrame();
+
+ markers = await observeStyling(5);
+ todo_is(markers.length, 0,
+ 'Animations on visibility hidden element that all descendants have ' +
+ 'become invisible again should never cause restyles');
+
+ // Make childB visible.
+ childB.style.visibility = 'visible';
+ await waitForNextFrame();
+
+ markers = await observeStyling(5);
+ is(markers.length, 5,
+ 'Animations on visibility hidden element that has a visible child ' +
+ 'should never cause restyles');
+
+ // Make childB invisible again
+ childB.style.visibility = 'hidden';
+ await waitForNextFrame();
+
+ markers = await observeStyling(5);
+ todo_is(markers.length, 0,
+ 'Animations on visibility hidden element that the visible child ' +
+ 'has become invisible again should never cause restyles');
+
+ await ensureElementRemoval(div);
+ }
+ );
+
+ add_task_if_omta_enabled(async function no_restyling_compositor_animations_after_pause_is_called() {
+ const div = addDiv(null, { style: 'animation: opacity 100s' });
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ animation.pause();
+
+ await animation.ready;
+
+ let markers = await observeStyling(1);
+ is(markers.length, 1,
+ 'Animations running on the compositor should restyle once after ' +
+ 'Animation.pause() was called');
+
+ markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'Paused animations running on the compositor should never cause ' +
+ 'restyles');
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function no_restyling_main_thread_animations_after_pause_is_called() {
+ const div = addDiv(null, { style: 'animation: on-main-thread 100s' });
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ animation.pause();
+
+ await animation.ready;
+
+ let markers = await observeStyling(1);
+ is(markers.length, 1,
+ 'Animations running on the main-thread should restyle once after ' +
+ 'Animation.pause() was called');
+
+ markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'Paused animations running on the main-thread should never cause ' +
+ 'restyles');
+ await ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(async function only_one_restyling_when_current_time_is_set_to_middle_of_duration() {
+ const div = addDiv(null, { style: 'animation: opacity 100s' });
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ animation.currentTime = 50 * MS_PER_SEC;
+
+ const markers = await observeStyling(5);
+ is(markers.length, 1,
+ 'Bug 1235478: Animations running on the compositor should only once ' +
+ 'update style when currentTime is set to middle of duration time');
+ await ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(async function change_duration_and_currenttime() {
+ const div = addDiv(null);
+ const animation = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ // Set currentTime to a time longer than duration.
+ animation.currentTime = 500 * MS_PER_SEC;
+
+ // Now the animation immediately get back from compositor.
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ // Extend the duration.
+ animation.effect.updateTiming({ duration: 800 * MS_PER_SEC });
+ const markers = await observeStyling(5);
+ is(markers.length, 1,
+ 'Animations running on the compositor should update style ' +
+ 'when duration is made longer than the current time');
+
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function script_animation_on_display_none_element() {
+ const div = addDiv(null);
+ const animation = div.animate({ zIndex: [ '0', '999' ] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ div.style.display = 'none';
+
+ // We need to wait a frame to apply display:none style.
+ await waitForNextFrame();
+
+ is(animation.playState, 'running',
+ 'Script animations keep running even when the target element has ' +
+ '"display: none" style');
+
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor,
+ 'Script animations on "display:none" element should not run on the ' +
+ 'compositor');
+
+ let markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'Script animations on "display: none" element should not update styles');
+
+ div.style.display = '';
+
+ markers = await observeStyling(5);
+ is(markers.length, 5,
+ 'Script animations restored from "display: none" state should update ' +
+ 'styles');
+
+ await ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(async function compositable_script_animation_on_display_none_element() {
+ const div = addDiv(null);
+ const animation = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ div.style.display = 'none';
+
+ // We need to wait a frame to apply display:none style.
+ await waitForNextFrame();
+
+ is(animation.playState, 'running',
+ 'Opacity script animations keep running even when the target element ' +
+ 'has "display: none" style');
+
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor,
+ 'Opacity script animations on "display:none" element should not ' +
+ 'run on the compositor');
+
+ let markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'Opacity script animations on "display: none" element should not ' +
+ 'update styles');
+
+ div.style.display = '';
+
+ markers = await observeStyling(1);
+ is(markers.length, 1,
+ 'Script animations restored from "display: none" state should update ' +
+ 'styles soon');
+
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor,
+ 'Opacity script animations restored from "display: none" should be ' +
+ 'run on the compositor in the next frame');
+
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function restyling_for_empty_keyframes() {
+ const div = addDiv(null);
+ const animation = div.animate({ }, 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ let markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations with no keyframes should not cause restyles');
+
+ animation.effect.setKeyframes({ zIndex: ['0', '999'] });
+ markers = await observeStyling(5);
+
+ is(markers.length, 5,
+ 'Setting valid keyframes should cause regular animation restyles to ' +
+ 'occur');
+
+ animation.effect.setKeyframes({ });
+ markers = await observeStyling(5);
+
+ is(markers.length, 1,
+ 'Setting an empty set of keyframes should trigger a single restyle ' +
+ 'to remove the previous animated style');
+
+ await ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(async function no_restyling_when_animation_style_when_re_setting_same_animation_property() {
+ const div = addDiv(null, { style: 'animation: opacity 100s' });
+ const animation = div.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor);
+ // Apply the same animation style
+ div.style.animation = 'opacity 100s';
+ const markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'Applying same animation style ' +
+ 'should never cause restyles');
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function necessary_update_should_be_invoked() {
+ const div = addDiv(null, { style: 'animation: on-main-thread 100s' });
+ const animation = div.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForAnimationFrames(5);
+ // Apply another animation style
+ div.style.animation = 'on-main-thread 110s';
+ const markers = await observeStyling(1);
+ // There should be two restyles.
+ // 1) Animation-only restyle for before applying the new animation style
+ // 2) Animation-only restyle for after applying the new animation style
+ is(markers.length, 2,
+ 'Applying animation style with different duration ' +
+ 'should restyle twice');
+ await ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(
+ async function changing_cascading_result_for_main_thread_animation() {
+ const div = addDiv(null, { style: 'on-main-thread: blue' });
+ const animation = div.animate({ opacity: [0, 1],
+ zIndex: ['0', '999'] },
+ 100 * MS_PER_SEC);
+ await waitForAnimationReadyToRestyle(animation);
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor,
+ 'The opacity animation is running on the compositor');
+ // Make the z-index style as !important to cause an update
+ // to the cascade.
+ // Bug 1300982: The z-index animation should be no longer
+ // running on the main thread.
+ div.style.setProperty('z-index', '0', 'important');
+ const markers = await observeStyling(5);
+ todo_is(markers.length, 0,
+ 'Changing cascading result for the property running on the main ' +
+ 'thread does not cause synchronization layer of opacity animation ' +
+ 'running on the compositor');
+ await ensureElementRemoval(div);
+ }
+ );
+
+ add_task_if_omta_enabled(
+ async function animation_visibility_and_opacity() {
+ const div = addDiv(null);
+ const animation1 = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
+ const animation2 = div.animate({ visibility: ['hidden', 'visible'] }, 100 * MS_PER_SEC);
+ await waitForAnimationReadyToRestyle(animation1);
+ await waitForAnimationReadyToRestyle(animation2);
+ const markers = await observeStyling(5);
+ is(markers.length, 5, 'The animation should not be throttled');
+ await ensureElementRemoval(div);
+ }
+ );
+
+ add_task(async function restyling_for_animation_on_orphaned_element() {
+ const div = addDiv(null);
+ const animation = div.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ div.remove();
+
+ let markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'Animation on orphaned element should not cause restyles');
+
+ document.body.appendChild(div);
+
+ await waitForNextFrame();
+ markers = await observeStyling(5);
+
+ is(markers.length, 5,
+ 'Animation on re-attached to the document begins to update style, got ' + markers.length);
+
+ await ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(
+ // Tests that if we remove an element from the document whose animation
+ // cascade needs recalculating, that it is correctly updated when it is
+ // re-attached to the document.
+ async function restyling_for_opacity_animation_on_re_attached_element() {
+ const div = addDiv(null, { style: 'opacity: 1 ! important' });
+ const animation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor,
+ 'The opacity animation overridden by an !important rule is NOT ' +
+ 'running on the compositor');
+
+ // Drop the !important rule to update the cascade.
+ div.style.setProperty('opacity', '1', '');
+
+ div.remove();
+
+ const markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'Opacity animation on orphaned element should not cause restyles');
+
+ document.body.appendChild(div);
+
+ // Need a frame to give the animation a chance to be sent to the
+ // compositor.
+ await waitForNextFrame();
+
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor,
+ 'The opacity animation which is no longer overridden by the ' +
+ '!important rule begins running on the compositor even if the ' +
+ '!important rule had been dropped before the target element was ' +
+ 'removed');
+
+ await ensureElementRemoval(div);
+ }
+ );
+
+ add_task(
+ async function no_throttling_additive_animations_out_of_view_element() {
+ const div = addDiv(null, { style: 'transform: translateY(-400px);' });
+ const animation =
+ div.animate([{ visibility: 'visible' }],
+ { duration: 100 * MS_PER_SEC, composite: 'add' });
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+
+ is(markers.length, 5,
+ 'Additive animation has no keyframe whose offset is 0 or 1 in an ' +
+ 'out-of-view element should not be throttled');
+ await ensureElementRemoval(div);
+ }
+ );
+
+ // Tests that missing keyframes animations don't throttle at all.
+ add_task(async function no_throttling_animations_out_of_view_element() {
+ const div = addDiv(null, { style: 'transform: translateY(-400px);' });
+ const animation =
+ div.animate([{ visibility: 'visible' }], 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+
+ is(markers.length, 5,
+ 'Discrete animation has has no keyframe whose offset is 0 or 1 in an ' +
+ 'out-of-view element should not be throttled');
+ await ensureElementRemoval(div);
+ });
+
+ // Tests that missing keyframes animation on scrolled out element that the
+ // animation is not able to be throttled.
+ add_task(
+ async function no_throttling_missing_keyframe_animations_out_of_view_element() {
+ const div =
+ addDiv(null, { style: 'transform: translateY(-400px);' +
+ 'visibility: collapse;' });
+ const animation =
+ div.animate([{ visibility: 'visible' }], 100 * MS_PER_SEC);
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+ is(markers.length, 5,
+ 'visibility animation has no keyframe whose offset is 0 or 1 in an ' +
+ 'out-of-view element and produces change hint other than paint-only ' +
+ 'change hint should not be throttled');
+ await ensureElementRemoval(div);
+ }
+ );
+
+ // Counter part of the above test.
+ add_task(async function no_restyling_discrete_animations_out_of_view_element() {
+ const div = addDiv(null, { style: 'transform: translateY(-400px);' });
+ const animation =
+ div.animate({ visibility: ['visible', 'hidden'] }, 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Discrete animation running on the main-thread in an out-of-view ' +
+ 'element should never cause restyles');
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function no_restyling_while_computed_timing_is_not_changed() {
+ const div = addDiv(null);
+ const animation = div.animate({ zIndex: [ '0', '999' ] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'step-end' });
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+
+ // We possibly expect one restyle from the initial animation compose, in
+ // order to update animations, but nothing else.
+ ok(markers.length <= 1,
+ 'Animation running on the main-thread while computed timing is not ' +
+ 'changed should not cause extra restyles, got ' + markers.length);
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function no_throttling_animations_in_view_svg() {
+ const div = addDiv(null, { style: 'overflow: scroll;' +
+ 'height: 100px; width: 100px;' });
+ const svg = addSVGElement(div, 'svg', { viewBox: '-10 -10 0.1 0.1',
+ width: '50px',
+ height: '50px' });
+ const rect = addSVGElement(svg, 'rect', { x: '-10',
+ y: '-10',
+ width: '10',
+ height: '10',
+ fill: 'red' });
+ const animation = rect.animate({ fill: ['blue', 'lime'] }, 100 * MS_PER_SEC);
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+ is(markers.length, 5,
+ 'CSS animations on an in-view svg element with post-transform should ' +
+ 'not be throttled.');
+
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function no_throttling_animations_in_transformed_parent() {
+ const div = addDiv(null, { style: 'overflow: scroll;' +
+ 'transform: translateX(50px);' });
+ const svg = addSVGElement(div, 'svg', { viewBox: '0 0 1250 1250',
+ width: '40px',
+ height: '40px' });
+ const rect = addSVGElement(svg, 'rect', { x: '0',
+ y: '0',
+ width: '1250',
+ height: '1250',
+ fill: 'red' });
+ const animation = rect.animate({ fill: ['blue', 'lime'] }, 100 * MS_PER_SEC);
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+ is(markers.length, 5,
+ 'CSS animations on an in-view svg element which is inside transformed ' +
+ 'parent should not be throttled.');
+
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function throttling_animations_out_of_view_svg() {
+ const div = addDiv(null, { style: 'overflow: scroll;' +
+ 'height: 100px; width: 100px;' });
+ const svg = addSVGElement(div, 'svg', { viewBox: '-10 -10 0.1 0.1',
+ width: '50px',
+ height: '50px' });
+ const rect = addSVGElement(svg, 'rect', { width: '10',
+ height: '10',
+ fill: 'red' });
+
+ const animation = rect.animate({ fill: ['blue', 'lime'] }, 100 * MS_PER_SEC);
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'CSS animations on an out-of-view svg element with post-transform ' +
+ 'should be throttled.');
+
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function no_throttling_animations_in_view_css_transform() {
+ const scrollDiv = addDiv(null, { style: 'overflow: scroll; ' +
+ 'height: 100px; width: 100px;' });
+ const targetDiv = addDiv(null,
+ { style: 'animation: on-main-thread 100s;' +
+ 'transform: translate(-50px, -50px);' });
+ scrollDiv.appendChild(targetDiv);
+
+ const animation = targetDiv.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+ is(markers.length, 5,
+ 'CSS animation on an in-view element with pre-transform should not ' +
+ 'be throttled.');
+
+ await ensureElementRemoval(scrollDiv);
+ });
+
+ add_task(async function throttling_animations_out_of_view_css_transform() {
+ const scrollDiv = addDiv(null, { style: 'overflow: scroll;' +
+ 'height: 100px; width: 100px;' });
+ const targetDiv = addDiv(null,
+ { style: 'animation: on-main-thread 100s;' +
+ 'transform: translate(100px, 100px);' });
+ scrollDiv.appendChild(targetDiv);
+
+ const animation = targetDiv.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'CSS animation on an out-of-view element with pre-transform should be ' +
+ 'throttled.');
+
+ await ensureElementRemoval(scrollDiv);
+ });
+
+ add_task(
+ async function throttling_animations_in_out_of_view_position_absolute_element() {
+ const parentDiv = addDiv(null,
+ { style: 'position: absolute; top: -1000px;' });
+ const targetDiv = addDiv(null,
+ { style: 'animation: on-main-thread 100s;' });
+ parentDiv.appendChild(targetDiv);
+
+ const animation = targetDiv.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'CSS animation in an out-of-view position absolute element should ' +
+ 'be throttled');
+
+ await ensureElementRemoval(parentDiv);
+ }
+ );
+
+ add_task(
+ async function throttling_animations_on_out_of_view_position_absolute_element() {
+ const div = addDiv(null,
+ { style: 'animation: on-main-thread 100s; ' +
+ 'position: absolute; top: -1000px;' });
+
+ const animation = div.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'CSS animation on an out-of-view position absolute element should ' +
+ 'be throttled');
+
+ await ensureElementRemoval(div);
+ }
+ );
+
+ add_task(
+ async function throttling_animations_in_out_of_view_position_fixed_element() {
+ const parentDiv = addDiv(null,
+ { style: 'position: fixed; top: -1000px;' });
+ const targetDiv = addDiv(null,
+ { style: 'animation: on-main-thread 100s;' });
+ parentDiv.appendChild(targetDiv);
+
+ const animation = targetDiv.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'CSS animation on an out-of-view position:fixed element should be ' +
+ 'throttled');
+
+ await ensureElementRemoval(parentDiv);
+ }
+ );
+
+ add_task(
+ async function throttling_animations_on_out_of_view_position_fixed_element() {
+ const div = addDiv(null,
+ { style: 'animation: on-main-thread 100s; ' +
+ 'position: fixed; top: -1000px;' });
+
+ const animation = div.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'CSS animation on an out-of-view position:fixed element should be ' +
+ 'throttled');
+
+ await ensureElementRemoval(div);
+ }
+ );
+
+ add_task(
+ async function no_throttling_animations_in_view_position_fixed_element() {
+ const iframe = document.createElement('iframe');
+ iframe.setAttribute('srcdoc', '<div id="target"></div>');
+ document.documentElement.appendChild(iframe);
+
+ await new Promise(resolve => {
+ iframe.addEventListener('load', () => {
+ resolve();
+ });
+ });
+
+ const target = iframe.contentDocument.getElementById('target');
+ target.style= 'position: fixed; top: 20px; width: 100px; height: 100px;';
+
+ const animation = target.animate({ zIndex: [ '0', '999' ] },
+ { duration: 100 * MS_PER_SEC,
+ iterations: Infinity });
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStylingInTargetWindow(iframe.contentWindow, 5);
+ is(markers.length, 5,
+ 'CSS animation on an in-view position:fixed element should NOT be ' +
+ 'throttled');
+
+ await ensureElementRemoval(iframe);
+ }
+ );
+
+ add_task(
+ async function throttling_position_absolute_animations_in_collapsed_iframe() {
+ const iframe = document.createElement('iframe');
+ iframe.setAttribute('srcdoc', '<div id="target"></div>');
+ iframe.style.height = '0px';
+ document.documentElement.appendChild(iframe);
+
+ await new Promise(resolve => {
+ iframe.addEventListener('load', () => {
+ resolve();
+ });
+ });
+
+ const target = iframe.contentDocument.getElementById("target");
+ target.style= 'position: absolute; top: 50%; width: 100px; height: 100px';
+
+ const animation = target.animate({ opacity: [0, 1] },
+ { duration: 100 * MS_PER_SEC,
+ iterations: Infinity });
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStylingInTargetWindow(iframe.contentWindow, 5);
+ is(markers.length, 0,
+ 'Animation on position:absolute element in collapsed iframe should ' +
+ 'be throttled');
+
+ await ensureElementRemoval(iframe);
+ }
+ );
+
+ add_task(
+ async function position_absolute_animations_in_collapsed_element() {
+ const parent = addDiv(null, { style: 'overflow: scroll; height: 0px;' });
+ const target = addDiv(null,
+ { style: 'animation: on-main-thread 100s infinite;' +
+ 'position: absolute; top: 50%;' +
+ 'width: 100px; height: 100px;' });
+ parent.appendChild(target);
+
+ const animation = target.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+ is(markers.length, 5,
+ 'Animation on position:absolute element in collapsed element ' +
+ 'should not be throttled');
+
+ await ensureElementRemoval(parent);
+ }
+ );
+
+ add_task(
+ async function throttling_position_absolute_animations_in_collapsed_element() {
+ const parent = addDiv(null, { style: 'overflow: scroll; height: 0px;' });
+ const target = addDiv(null,
+ { style: 'animation: on-main-thread 100s infinite;' +
+ 'position: absolute; top: 50%;' });
+ parent.appendChild(target);
+
+ const animation = target.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+ todo_is(markers.length, 0,
+ 'Animation on collapsed position:absolute element in collapsed ' +
+ 'element should be throttled');
+
+ await ensureElementRemoval(parent);
+ }
+ );
+
+ add_task_if_omta_enabled(
+ async function no_restyling_for_compositor_animation_on_unrelated_style_change() {
+ const div = addDiv(null);
+ const animation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor,
+ 'The opacity animation is running on the compositor');
+
+ div.style.setProperty('color', 'blue', '');
+ const markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'The opacity animation keeps running on the compositor when ' +
+ 'color style is changed');
+ await ensureElementRemoval(div);
+ }
+ );
+
+ add_task(
+ async function no_overflow_transform_animations_in_scrollable_element() {
+ const parentElement = addDiv(null,
+ { style: 'overflow-y: scroll; height: 100px;' });
+ const div = addDiv(null);
+ const animation =
+ div.animate({ transform: [ 'translateY(10px)', 'translateY(10px)' ] },
+ 100 * MS_PER_SEC);
+ parentElement.appendChild(div);
+
+ await waitForAnimationReadyToRestyle(animation);
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const markers = await observeStyling(20);
+ is(markers.length, 0,
+ 'No-overflow transform animations running on the compositor should ' +
+ 'never update style on the main thread');
+
+ await ensureElementRemoval(parentElement);
+ }
+ );
+
+ add_task(async function no_flush_on_getAnimations() {
+ const div = addDiv(null);
+ const animation =
+ div.animate({ opacity: [ '0', '1' ] }, 100 * MS_PER_SEC);
+ await waitForAnimationReadyToRestyle(animation);
+
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const markers = observeAnimSyncStyling(() => {
+ is(div.getAnimations().length, 1, 'There should be one animation');
+ });
+ is(markers.length, 0,
+ 'Element.getAnimations() should not flush throttled animation style');
+
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function restyling_for_throttled_animation_on_getAnimations() {
+ const div = addDiv(null, { style: 'animation: opacity 100s' });
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const markers = observeAnimSyncStyling(() => {
+ div.style.animationDuration = '0s';
+ is(div.getAnimations().length, 0, 'There should be no animation');
+ });
+
+ is(markers.length, 1, // For discarding the throttled animation.
+ 'Element.getAnimations() should flush throttled animation style so ' +
+ 'that the throttled animation is discarded');
+
+ await ensureElementRemoval(div);
+ });
+
+ add_task(
+ async function no_restyling_for_throttled_animation_on_querying_play_state() {
+ const div = addDiv(null, { style: 'animation: opacity 100s' });
+ const animation = div.getAnimations()[0];
+ const sibling = addDiv(null);
+
+ await waitForAnimationReadyToRestyle(animation);
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const markers = observeAnimSyncStyling(() => {
+ sibling.style.opacity = '0.5';
+ is(animation.playState, 'running',
+ 'Animation.playState should be running');
+ });
+ is(markers.length, 0,
+ 'Animation.playState should not flush throttled animation in the ' +
+ 'case where there are only style changes that don\'t affect the ' +
+ 'throttled animation');
+
+ await ensureElementRemoval(div);
+ await ensureElementRemoval(sibling);
+ }
+ );
+
+ add_task(
+ async function restyling_for_throttled_animation_on_querying_play_state() {
+ const div = addDiv(null, { style: 'animation: opacity 100s' });
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const markers = observeAnimSyncStyling(() => {
+ div.style.animationPlayState = 'paused';
+ is(animation.playState, 'paused',
+ 'Animation.playState should be reflected by pending style');
+ });
+
+ is(markers.length, 1,
+ 'Animation.playState should flush throttled animation style that ' +
+ 'affects the throttled animation');
+
+ await ensureElementRemoval(div);
+ }
+ );
+
+ add_task(
+ async function no_restyling_for_throttled_transition_on_querying_play_state() {
+ const div = addDiv(null, { style: 'transition: opacity 100s; opacity: 0' });
+ const sibling = addDiv(null);
+
+ getComputedStyle(div).opacity;
+ div.style.opacity = 1;
+
+ const transition = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(transition);
+ ok(SpecialPowers.wrap(transition).isRunningOnCompositor);
+
+ const markers = observeAnimSyncStyling(() => {
+ sibling.style.opacity = '0.5';
+ is(transition.playState, 'running',
+ 'Animation.playState should be running');
+ });
+
+ is(markers.length, 0,
+ 'Animation.playState should not flush throttled transition in the ' +
+ 'case where there are only style changes that don\'t affect the ' +
+ 'throttled animation');
+
+ await ensureElementRemoval(div);
+ await ensureElementRemoval(sibling);
+ }
+ );
+
+ add_task(
+ async function restyling_for_throttled_transition_on_querying_play_state() {
+ const div = addDiv(null, { style: 'transition: opacity 100s; opacity: 0' });
+ getComputedStyle(div).opacity;
+ div.style.opacity = '1';
+
+ const transition = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(transition);
+ ok(SpecialPowers.wrap(transition).isRunningOnCompositor);
+
+ const markers = observeAnimSyncStyling(() => {
+ div.style.transitionProperty = 'none';
+ is(transition.playState, 'idle',
+ 'Animation.playState should be reflected by pending style change ' +
+ 'which cancel the transition');
+ });
+
+ is(markers.length, 1,
+ 'Animation.playState should flush throttled transition style that ' +
+ 'affects the throttled animation');
+
+ await ensureElementRemoval(div);
+ }
+ );
+
+ add_task(async function restyling_visibility_animations_on_in_view_element() {
+ const div = addDiv(null);
+ const animation =
+ div.animate({ visibility: ['hidden', 'visible'] }, 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+
+ is(markers.length, 5,
+ 'Visibility animation running on the main-thread on in-view element ' +
+ 'should not be throttled');
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function restyling_outline_offset_animations_on_invisible_element() {
+ const div = addDiv(null,
+ { style: 'visibility: hidden; ' +
+ 'outline-style: solid; ' +
+ 'outline-width: 1px;' });
+ const animation =
+ div.animate({ outlineOffset: [ '0px', '10px' ] },
+ { duration: 100 * MS_PER_SEC,
+ iterations: Infinity });
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Outline offset animation running on the main-thread on invisible ' +
+ 'element should be throttled');
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function restyling_transform_animations_on_invisible_element() {
+ const div = addDiv(null, { style: 'visibility: hidden;' });
+
+ const animation =
+ div.animate({ transform: [ 'none', 'rotate(360deg)' ] },
+ { duration: 100 * MS_PER_SEC,
+ iterations: Infinity });
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Transform animations on visibility hidden element should be throttled');
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function restyling_transform_animations_on_invisible_element() {
+ const div = addDiv(null, { style: 'visibility: hidden;' });
+
+ const animation =
+ div.animate([ { transform: 'rotate(360deg)' } ],
+ { duration: 100 * MS_PER_SEC,
+ iterations: Infinity });
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Transform animations without 100% keyframe on visibility hidden ' +
+ 'element should be throttled');
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function restyling_translate_animations_on_invisible_element() {
+ const div = addDiv(null, { style: 'visibility: hidden;' });
+
+ const animation =
+ div.animate([ { translate: '100px' } ],
+ { duration: 100 * MS_PER_SEC,
+ iterations: Infinity });
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Translate animations without 100% keyframe on visibility hidden ' +
+ 'element should be throttled');
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function restyling_rotate_animations_on_invisible_element() {
+ const div = addDiv(null, { style: 'visibility: hidden;' });
+
+ const animation =
+ div.animate([ { rotate: '45deg' } ],
+ { duration: 100 * MS_PER_SEC,
+ iterations: Infinity });
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Rotate animations without 100% keyframe on visibility hidden ' +
+ 'element should be throttled');
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function restyling_scale_animations_on_invisible_element() {
+ const div = addDiv(null, { style: 'visibility: hidden;' });
+
+ const animation =
+ div.animate([ { scale: '2 2' } ],
+ { duration: 100 * MS_PER_SEC,
+ iterations: Infinity });
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Scale animations without 100% keyframe on visibility hidden ' +
+ 'element should be throttled');
+ await ensureElementRemoval(div);
+ });
+
+ add_task(
+ async function restyling_transform_animations_having_abs_pos_child_on_invisible_element() {
+ const div = addDiv(null, { style: 'visibility: hidden;' });
+ const child = addDiv(null, { style: 'position: absolute; top: 100px;' });
+ div.appendChild(child);
+
+ const animation =
+ div.animate({ transform: [ 'none', 'rotate(360deg)' ] },
+ { duration: 100 * MS_PER_SEC,
+ iterations: Infinity });
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Transform animation having an absolutely positioned child on ' +
+ 'visibility hidden element should be throttled');
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function no_restyling_animations_in_out_of_view_iframe() {
+ const div = addDiv(null, { style: 'overflow-y: scroll; height: 100px;' });
+
+ const iframe = document.createElement('iframe');
+ iframe.setAttribute(
+ 'srcdoc',
+ '<div style="height: 100px;"></div><div id="target"></div>');
+ div.appendChild(iframe);
+
+ await new Promise(resolve => {
+ iframe.addEventListener('load', () => {
+ resolve();
+ });
+ });
+
+ const target = iframe.contentDocument.getElementById("target");
+ target.style= 'width: 100px; height: 100px;';
+
+ const animation = target.animate({ zIndex: [ '0', '999' ] },
+ 100 * MS_PER_SEC);
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStylingInTargetWindow(iframe.contentWindow, 5);
+ is(markers.length, 0,
+ 'Animation in out-of-view iframe should be throttled');
+
+ await ensureElementRemoval(div);
+ });
+
+ // Tests that transform animations are not able to run on the compositor due
+ // to layout restrictions (e.g. animations on a large size frame) doesn't
+ // flush layout at all.
+ add_task(async function flush_layout_for_transform_animations() {
+ // Set layout.animation.prerender.partial to disallow transform animations
+ // on large frames to be sent to the compositor.
+ await SpecialPowers.pushPrefEnv({
+ set: [['layout.animation.prerender.partial', false]] });
+ const div = addDiv(null, { style: 'width: 10000px; height: 10000px;' });
+
+ const animation = div.animate([ { transform: 'rotate(360deg)', } ],
+ { duration: 100 * MS_PER_SEC,
+ // Set step-end to skip further restyles.
+ easing: 'step-end' });
+
+ const FLUSH_LAYOUT = SpecialPowers.DOMWindowUtils.FLUSH_LAYOUT;
+ ok(SpecialPowers.DOMWindowUtils.needsFlush(FLUSH_LAYOUT),
+ 'Flush is needed for the appended div');
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor, "Shouldn't be running in the compositor");
+
+ // We expect one restyle from the initial animation compose.
+ await waitForNextFrame();
+
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor, "Still shouldn't be running in the compositor");
+ ok(!SpecialPowers.DOMWindowUtils.needsFlush(FLUSH_LAYOUT),
+ 'No further layout flush needed');
+
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function partial_prerendered_transform_animations() {
+ await SpecialPowers.pushPrefEnv({
+ set: [['layout.animation.prerender.partial', true]] });
+ const div = addDiv(null, { style: 'width: 10000px; height: 10000px;' });
+
+ const animation = div.animate(
+ // Use the same value both for `from` and `to` to avoid jank on the
+ // compositor.
+ { transform: ['rotate(0deg)', 'rotate(0deg)'] },
+ 100 * MS_PER_SEC
+ );
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'Transform animation with partial pre-rendered should never cause ' +
+ 'restyles');
+
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function restyling_on_create_animation() {
+ const div = addDiv();
+ const docShell = getDocShellForObservingRestylesForWindow(window);
+
+ const animationA = div.animate(
+ { transform: ['none', 'rotate(360deg)'] },
+ 100 * MS_PER_SEC
+ );
+ const animationB = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
+ const animationC = div.animate(
+ { color: ['blue', 'green'] },
+ 100 * MS_PER_SEC
+ );
+ const animationD = div.animate(
+ { width: ['100px', '200px'] },
+ 100 * MS_PER_SEC
+ );
+ const animationE = div.animate(
+ { height: ['100px', '200px'] },
+ 100 * MS_PER_SEC
+ );
+
+ const markers = docShell
+ .popProfileTimelineMarkers()
+ .filter(marker => marker.name === 'Styles' && !marker.isAnimationOnly);
+ docShell.recordProfileTimelineMarkers = false;
+
+ is(markers.length, 0, 'Creating animations should not flush styles');
+
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function out_of_view_background_position() {
+ const div = addDiv(null, {
+ style: `
+ background-image: linear-gradient(90deg, rgb(224, 224, 224), rgb(241, 241, 241) 30%, rgb(224, 224, 224) 60%);
+ background-size: 80px;
+ animation: background-position 100s infinite;
+ transform: translateY(-400px);
+ `,
+ })
+
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+
+ is(markers.length, 0, 'background-position animations can be throttled');
+ await ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(async function no_restyling_animations_in_opacity_zero_element() {
+ const div = addDiv(null, { style: 'animation: on-main-thread 100s infinite; opacity: 0' });
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ const markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'Animations running on the main thread in opacity: 0 element ' +
+ 'should never cause restyles');
+ await ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(async function no_restyling_compositor_animations_in_opacity_zero_descendant() {
+ const container = addDiv(null, { style: 'opacity: 0' });
+ const child = addDiv(null, { style: 'animation: background-color 100s infinite;' });
+ container.appendChild(child);
+
+ const animation = child.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations running on the compositor in opacity zero descendant element ' +
+ 'should never cause restyles');
+ await ensureElementRemoval(container);
+ });
+
+ add_task_if_omta_enabled(async function no_restyling_compositor_animations_in_opacity_zero_descendant_abspos() {
+ const container = addDiv(null, { style: 'opacity: 0' });
+ const child = addDiv(null, { style: 'position: absolute; animation: background-color 100s infinite;' });
+ container.appendChild(child);
+
+ const animation = child.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations running on the compositor in opacity zero abspos descendant element ' +
+ 'should never cause restyles');
+ await ensureElementRemoval(container);
+ });
+
+ add_task_if_omta_enabled(async function no_restyling_compositor_animations_in_opacity_zero_element() {
+ const child = addDiv(null, { style: 'animation: background-color 100s infinite; opacity: 0' });
+
+ const animation = child.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations running on the compositor in opacity zero element ' +
+ 'should never cause restyles');
+ await ensureElementRemoval(child);
+ });
+
+ add_task_if_omta_enabled(async function restyling_main_thread_animations_in_opacity_zero_descendant_after_root_opacity_animation() {
+ const container = addDiv(null, { style: 'opacity: 0' });
+
+ const child = addDiv(null, { style: 'animation: on-main-thread 100s infinite;' });
+ container.appendChild(child);
+
+ // Animate the container from 1 to zero opacity and ensure the child animation is throttled then.
+ const containerAnimation = container.animate({ opacity: [ '1', '0' ] }, 100);
+ await containerAnimation.finished;
+
+ const animation = child.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations running on the compositor in opacity zero descendant element ' +
+ 'should never cause restyles after root animation has finished');
+ await ensureElementRemoval(container);
+ });
+
+ add_task_if_omta_enabled(async function restyling_main_thread_animations_in_opacity_zero_descendant_during_root_opacity_animation() {
+ const container = addDiv(null, { style: 'opacity: 0; animation: opacity 100s infinite' });
+
+ const child = addDiv(null, { style: 'animation: on-main-thread 100s infinite;' });
+ container.appendChild(child);
+
+ const animation = child.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+
+ is(markers.length, 5,
+ 'Animations in opacity zero descendant element ' +
+ 'should not be throttled if root is animating opacity');
+ await ensureElementRemoval(container);
+ });
+
+ add_task_if_omta_enabled(async function transparent_background_color_animations() {
+ const div = addDiv(null);
+ const animation =
+ div.animate({ backgroundColor: [ 'rgb(0, 200, 0, 0)',
+ 'rgb(200, 0, 0, 0.1)' ] },
+ { duration: 100 * MS_PER_SEC,
+ // An easing function producing zero in the first half of
+ // the duration.
+ easing: 'cubic-bezier(1, 0, 1, 0)' });
+ await waitForAnimationReadyToRestyle(animation);
+
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'transparent background-color animation should not update styles on ' +
+ 'the main thread');
+
+ await ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(async function transform_animation_on_collapsed_element() {
+ const iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+
+ // Load a cross origin iframe.
+ const targetURL = SimpleTest.getTestFileURL("empty.html")
+ .replace(window.location.origin, "http://example.com/");
+ iframe.src = targetURL;
+ await new Promise(resolve => {
+ iframe.onload = resolve;
+ });
+
+ await SpecialPowers.spawn(iframe, [MS_PER_SEC], async (MS_PER_SEC) => {
+ // Create a flex item with "preserve-3d" having an abs-pos child inside
+ // a grid container.
+ // These styles make the the flex item size (0x0).
+ const gridContainer = content.document.createElement("div");
+ gridContainer.style.display = "grid";
+ gridContainer.style.placeItems = "center";
+
+ const target = content.document.createElement("div");
+ target.style.display = "flex";
+ target.style.transformStyle = "preserve-3d";
+ gridContainer.appendChild(target);
+
+ const child = content.document.createElement("div");
+ child.style.position = "absolute";
+ child.style.transform = "rotateY(0deg)";
+ child.style.width = "100px";
+ child.style.height = "100px";
+ child.style.backgroundColor = "green";
+ target.appendChild(child);
+
+ content.document.body.appendChild(gridContainer);
+
+ const animation =
+ target.animate({ transform: [ "rotateY(0deg)", "rotateY(360deg)" ] },
+ { duration: 100 * MS_PER_SEC,
+ id: "test",
+ easing: 'step-end' });
+ await content.wrappedJSObject.waitForAnimationReadyToRestyle(animation);
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor,
+ 'transform animation on a collapsed element should run on the ' +
+ 'compositor');
+
+ const markers = await content.wrappedJSObject.observeStyling(5);
+ is(markers.length, 0,
+ 'transform animation on a collapsed element animation should not ' +
+ 'update styles on the main thread');
+ });
+
+ await ensureElementRemoval(iframe);
+ });
+});
+
+</script>
+</body>