diff options
Diffstat (limited to 'dom/animation/test/mozilla/file_restyles.html')
-rw-r--r-- | dom/animation/test/mozilla/file_restyles.html | 2275 |
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> |