diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /testing/web-platform/tests/animation-worklet | |
parent | Initial commit. (diff) | |
download | firefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/animation-worklet')
50 files changed, 3348 insertions, 0 deletions
diff --git a/testing/web-platform/tests/animation-worklet/META.yml b/testing/web-platform/tests/animation-worklet/META.yml new file mode 100644 index 0000000000..88e7d924aa --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/META.yml @@ -0,0 +1,4 @@ +spec: https://drafts.css-houdini.org/css-animationworklet/ +suggested_reviewers: + - flackr + - majido diff --git a/testing/web-platform/tests/animation-worklet/animate-multiple-effects-on-different-targets-via-main-thread.https.html b/testing/web-platform/tests/animation-worklet/animate-multiple-effects-on-different-targets-via-main-thread.https.html new file mode 100644 index 0000000000..d22ed4cd25 --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/animate-multiple-effects-on-different-targets-via-main-thread.https.html @@ -0,0 +1,65 @@ +<!DOCTYPE html> +<title>Animate multiple effects on different targets via main thread</title> +<link rel="help" href="https://drafts.css-houdini.org/css-animationworklet/"> + +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="common.js"></script> + +<style> + #target { + width: 100px; + height: 100px; + background-color: green; + } + #target2 { + width: 100px; + height: 100px; + background-color: blue; + box-shadow: 4px 4px 25px blue; + } +</style> + +<div id="target"></div> +<div id="target2"></div> + +<script id="simple_animate" type="text/worklet"> + registerAnimator("test_animator", class { + animate(currentTime, effect) { + let effects = effect.getChildren(); + effects[0].localTime = 1000; + effects[1].localTime = 1000; + } + }); +</script> + +<script> + promise_test(async t => { + await runInAnimationWorklet(document.getElementById('simple_animate').textContent); + + const effect = new KeyframeEffect( + document.getElementById("target"), + [ + { background: 'green' }, + { background: 'blue' }, + ], + { duration: 2000 } + ); + const effect2 = new KeyframeEffect( + document.getElementById("target2"), + [ + { boxShadow: '4px 4px 25px blue' }, + { boxShadow: '4px 4px 25px green' } + ], + { duration: 2000 } + ); + + const animation = new WorkletAnimation('test_animator', [effect, effect2]); + animation.play(); + await waitForAsyncAnimationFrames(1); + + assert_equals(getComputedStyle(document.getElementById('target')).backgroundColor, "rgb(0, 64, 128)"); + assert_equals(getComputedStyle(document.getElementById('target2')).boxShadow, "rgb(0, 64, 128) 4px 4px 25px 0px"); + }, 'Animating multiple effects on different targets via main thread should produce new output values accordingly'); +</script>
\ No newline at end of file diff --git a/testing/web-platform/tests/animation-worklet/animate-non-accelerated-property.https.html b/testing/web-platform/tests/animation-worklet/animate-non-accelerated-property.https.html new file mode 100644 index 0000000000..8e30387530 --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/animate-non-accelerated-property.https.html @@ -0,0 +1,44 @@ +<!DOCTYPE html> +<title>Animate non-accelerated property using worklet animation</title> +<link rel="help" href="https://drafts.css-houdini.org/css-animationworklet/"> + +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="common.js"></script> + +<div id="target"></div> +<div id="target2"></div> + +<script> + promise_test(async t => { + await registerConstantLocalTimeAnimator(1000); + const target = document.getElementById("target"); + const effect = new KeyframeEffect( + target, + [ + { background: 'green' }, + { background: 'blue' }, + ], + { duration: 2000 } + ); + + const target2 = document.getElementById("target2"); + const effect2 = new KeyframeEffect( + target2, + [ + { boxShadow: '4px 4px 25px blue' }, + { boxShadow: '4px 4px 25px green' } + ], + { duration: 2000 } + ); + const animation = new WorkletAnimation('constant_time', effect); + animation.play(); + const animation2 = new WorkletAnimation('constant_time', effect2); + animation2.play(); + + await waitForAsyncAnimationFrames(1); + assert_equals(getComputedStyle(target).backgroundColor, "rgb(0, 64, 128)"); + assert_equals(getComputedStyle(target2).boxShadow, "rgb(0, 64, 128) 4px 4px 25px 0px"); + }, "Individual worklet animation should output values at specified local time for corresponding targets and effects"); +</script> diff --git a/testing/web-platform/tests/animation-worklet/animation-worklet-inside-iframe.https.html b/testing/web-platform/tests/animation-worklet/animation-worklet-inside-iframe.https.html new file mode 100644 index 0000000000..415f394401 --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/animation-worklet-inside-iframe.https.html @@ -0,0 +1,61 @@ +<!DOCTYPE html> +<title>Test that AnimationWorklet inside frames with different origin causes new global scopes</title> +<link rel="help" href="https://drafts.css-houdini.org/css-animationworklet/"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="common.js"></script> + +<style> +.redbox { + width: 100px; + height: 100px; + background-color: #ff0000; +} +</style> + +<div id="target" class="redbox"></div> + +<script id="main_worklet" type="text/worklet"> +registerAnimator("duplicate_animator", class { + animate(currentTime, effect) { + effect.localTime = 500; + } +}); +</script> + +<script> +async_test(t => { + // Wait for iframe to load and start its animations. + window.onmessage = function(msg) { + window.requestAnimationFrame( _ => { + run_test(msg.data); + }); + }; + + // Create and load the iframe to avoid racy cases. + var iframe = document.createElement('iframe'); + iframe.src = 'resources/animator-iframe.html'; + document.body.appendChild(iframe); + + function run_test(data) { + runInAnimationWorklet( + document.getElementById('main_worklet').textContent + ).then(_ => { + // Create an animation for duplicate animator. + const target = document.getElementById('target'); + const animation = new WorkletAnimation('duplicate_animator', new KeyframeEffect(target, [{ opacity: 0 }], { duration: 1000 })); + animation.play(); + + assert_equals(data, '0.4'); + + // wait until local times are synced back to the main thread. + waitForAnimationFrameWithCondition(_ => { + return getComputedStyle(target).opacity != '1'; + }).then(t.step_func_done(() => { + assert_equals(getComputedStyle(target).opacity, '0.5'); + })); + }); + } +}, 'Both main frame and iframe should update the opacity of their target'); +</script> diff --git a/testing/web-platform/tests/animation-worklet/animator-with-options.https.html b/testing/web-platform/tests/animation-worklet/animator-with-options.https.html new file mode 100644 index 0000000000..975c57f038 --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/animator-with-options.https.html @@ -0,0 +1,38 @@ +<!DOCTYPE html> +<title>Worklet Animation with options</title> +<link rel="help" href="https://drafts.css-houdini.org/css-animationworklet/"> + +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="common.js"></script> + +<div id="target"></div> + +<script id="animate_with_options" type="text/worklet"> + registerAnimator("test_animator", class { + constructor(options) { + this.time_ = options.time; + } + animate(currentTime, effect) { + effect.localTime = this.time_; + } + }); +</script> + +<script> + promise_test(async t => { + await runInAnimationWorklet(document.getElementById('animate_with_options').textContent); + const target = document.getElementById('target'); + const effect = new KeyframeEffect(target, [{ opacity: 0 }], { duration: 1000 }); + const options = {'time': 500}; + const animation = new WorkletAnimation('test_animator', effect, document.timeline, options); + animation.play(); + + // wait until local times are synced back to the main thread. + await waitForAnimationFrameWithCondition(_ => { + return getComputedStyle(target).opacity != '1'; + }); + assert_equals(getComputedStyle(target).opacity, "0.5"); + }, "Animator should be able to use options to update the animation"); +</script>
\ No newline at end of file diff --git a/testing/web-platform/tests/animation-worklet/cancel-non-accelerated-property.https.html b/testing/web-platform/tests/animation-worklet/cancel-non-accelerated-property.https.html new file mode 100644 index 0000000000..594da4c419 --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/cancel-non-accelerated-property.https.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<title>Cancel non accelerated property using worklet animation</title> +<link rel="help" href="https://drafts.css-houdini.org/css-animationworklet/"> + +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="common.js"></script> + +<style> +#target { + background-color: red; +} +</style> + +<div id="target"></div> + +<script> + promise_test(async t => { + await registerConstantLocalTimeAnimator(1000); + const target = document.getElementById('target'); + const effect = new KeyframeEffect( + target, + [ + { background: 'green' }, + { background: 'blue' }, + ], + { + duration: 2000, + iteration: Infinity + } + ); + const animation = new WorkletAnimation('constant_time', effect); + animation.play(); + + await waitForAsyncAnimationFrames(1); + // establish that the animation started + assert_equals(getComputedStyle(target).backgroundColor, "rgb(0, 64, 128)"); + animation.cancel(); + + await waitForAsyncAnimationFrames(1); + // confirm the animation is cancelled + assert_equals(getComputedStyle(target).backgroundColor, "rgb(255, 0, 0)"); + }, "Animation should update the outputs after starting and then return to pre-animated values after being cancelled"); +</script>
\ No newline at end of file diff --git a/testing/web-platform/tests/animation-worklet/common.js b/testing/web-platform/tests/animation-worklet/common.js new file mode 100644 index 0000000000..ceb430b718 --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/common.js @@ -0,0 +1,61 @@ +'use strict'; + +function registerPassthroughAnimator() { + return runInAnimationWorklet(` + registerAnimator('passthrough', class { + animate(currentTime, effect) { + effect.localTime = currentTime; + } + }); + `); +} + +function registerConstantLocalTimeAnimator(localTime) { + return runInAnimationWorklet(` + registerAnimator('constant_time', class { + animate(currentTime, effect) { effect.localTime = ${localTime}; } + }); + `); +} + +function runInAnimationWorklet(code) { + return CSS.animationWorklet.addModule( + URL.createObjectURL(new Blob([code], {type: 'text/javascript'})) + ); +} + +function approxEquals(actual, expected){ + // precision in ms + const epsilon = 0.005; + const lowerBound = (expected - epsilon) < actual; + const upperBound = (expected + epsilon) > actual; + return lowerBound && upperBound; +} + +function waitForAsyncAnimationFrames(count) { + // In Chrome, waiting for N+1 main thread frames guarantees that compositor has produced + // at least N frames. + // TODO(majidvp): re-evaluate this choice once other browsers have implemented + // AnimationWorklet. + return waitForAnimationFrames(count + 1); +} + +async function waitForAnimationFrameWithCondition(condition) { + do { + await new Promise(window.requestAnimationFrame); + } while (!condition()) +} + +async function waitForDocumentTimelineAdvance() { + const timeAtStart = document.timeline.currentTime; + do { + await new Promise(window.requestAnimationFrame); + } while (timeAtStart === document.timeline.currentTime) +} + +// Wait until animation's effect has a non-null localTime. +async function waitForNotNullLocalTime(animation) { + await waitForAnimationFrameWithCondition(_ => { + return animation.effect.getComputedTiming().localTime !== null; + }); +}
\ No newline at end of file diff --git a/testing/web-platform/tests/animation-worklet/current-time.https.html b/testing/web-platform/tests/animation-worklet/current-time.https.html new file mode 100644 index 0000000000..a445d5b004 --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/current-time.https.html @@ -0,0 +1,64 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>The current time of a worklet animation</title> +<link rel="help" href="https://drafts.css-houdini.org/css-animationworklet/"> + +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="common.js"></script> + +<div id="box"></div> + +<script> +'use strict'; + +function CreateAnimation() { + const box = document.getElementById('box'); + const effect = new KeyframeEffect( + box, + { height: ['100px', '50px'] }, + 10000); + + return new WorkletAnimation('passthrough', effect); +} + +setup(setupAndRegisterTests, {explicit_done: true}); + +function setupAndRegisterTests() { + registerPassthroughAnimator().then(() => { + promise_test(async t => { + const animation = CreateAnimation(); + animation.play(); + + assert_equals(animation.currentTime, 0, + 'Current time returns the hold time set when entering the play-pending' + + 'state'); + + animation.cancel(); + }, 'The current time returns the hold time when set'); + + promise_test(async t => { + const animation = CreateAnimation(); + animation.play(); + + // Allow one async animation frame to pass so that animation is running. + await waitForAsyncAnimationFrames(1); + assert_equals(animation.playState, "running"); + // Allow time to advance so that we have a non-zero current time. + await waitForDocumentTimelineAdvance(); + const timelineTime = document.timeline.currentTime; + assert_greater_than(animation.currentTime, 0); + assert_times_equal(animation.currentTime, (timelineTime - animation.startTime)); + + animation.cancel(); + }, 'The current time is calculated from the timeline time and start time'); + + done(); + }); +} + +// TODO(majidvp): Add tests for playbackRate and animations that are not +// associated with a timeline once these are supported in WorkletAnimation. +// http://crbug.com/833846 +</script> diff --git a/testing/web-platform/tests/animation-worklet/idlharness.any.js b/testing/web-platform/tests/animation-worklet/idlharness.any.js new file mode 100644 index 0000000000..a53ac739f3 --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/idlharness.any.js @@ -0,0 +1,18 @@ +// META: script=/resources/WebIDLParser.js +// META: script=/resources/idlharness.js +// META: timeout=long + +'use strict'; + +// https://wicg.github.io/animation-worklet/ + +idl_test( + ['css-animation-worklet'], + ['web-animations', 'html', 'cssom', 'dom'], + idl_array => { + idl_array.add_objects({ + WorkletAnimation: ['new WorkletAnimation("name")'], + // TODO: WorkletGroupEffect + }); + } +); diff --git a/testing/web-platform/tests/animation-worklet/inactive-timeline.https.html b/testing/web-platform/tests/animation-worklet/inactive-timeline.https.html new file mode 100644 index 0000000000..3938cb3092 --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/inactive-timeline.https.html @@ -0,0 +1,139 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Correctness of worklet animation state when timeline becomes newly + active or inactive.</title> +<link rel="help" href="https://drafts.css-houdini.org/css-animationworklet/"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="common.js"></script> +<style> + .scroller { + overflow: auto; + height: 100px; + width: 100px; + } + .contents { + height: 1000px; + width: 100%; + } +</style> +<body> +<div id="log"></div> +<script> +'use strict'; + +function createScroller(test) { + var scroller = createDiv(test); + scroller.innerHTML = "<div class='contents'></div>"; + scroller.classList.add('scroller'); + return scroller; +} + +function createScrollLinkedWorkletAnimation(test) { + const timeline = new ScrollTimeline({ + scrollSource: createScroller(test), + }); + const DURATION = 1000; // ms + const KEYFRAMES = { transform: ['translateY(100px)', 'translateY(200px)'] }; + return new WorkletAnimation('passthrough', new KeyframeEffect(createDiv(test), + KEYFRAMES, DURATION), timeline); +} + +setup(setupAndRegisterTests, {explicit_done: true}); + +function setupAndRegisterTests() { + registerPassthroughAnimator().then(() => { + + promise_test(async t => { + const animation = createScrollLinkedWorkletAnimation(t); + const scroller = animation.timeline.scrollSource; + const target = animation.effect.target; + + // There is no direct way to control when local times of composited + // animations are synced to the main thread. This test uses another + // composited worklet animation with an always active timeline as an + // indicator of when the sync is ready. The sync is done when animation + // effect's output has changed as a result of advancing the timeline. + const animationRef = createScrollLinkedWorkletAnimation(t); + const scrollerRef = animationRef.timeline.scrollSource; + const targetRef = animationRef.effect.target; + + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.2 * maxScroll; + + // Make the timeline inactive. + scroller.style.display = "none" + // Force relayout. + scroller.scrollTop; + + animation.play(); + animationRef.play(); + assert_equals(animation.currentTime, null, + 'Initial current time must be unresolved in idle state.'); + assert_equals(animation.startTime, null, + 'Initial start time must be unresolved in idle state.'); + waitForAnimationFrameWithCondition(_=> { + return animation.playState == "running" + }); + assert_equals(animation.currentTime, null, + 'Initial current time must be unresolved in playing state.'); + assert_equals(animation.startTime, null, + 'Initial start time must be unresolved in playing state.'); + + scrollerRef.scrollTop = 0.2 * maxScroll; + + // Wait until local times are synced back to the main thread. + await waitForAnimationFrameWithCondition(_ => { + return animationRef.effect.getComputedTiming().localTime == 200; + }); + + assert_equals(animation.effect.getComputedTiming().localTime, null, + 'The underlying effect local time must be undefined while the ' + + 'timeline is inactive.'); + + // Make the timeline active. + scroller.style.display = ""; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + + assert_times_equal(animation.currentTime, 200, + 'Current time must be initialized.'); + assert_times_equal(animation.startTime, 0, + 'Start time must be initialized.'); + + scrollerRef.scrollTop = 0.4 * maxScroll; + // Wait until local times are synced back to the main thread. + await waitForAnimationFrameWithCondition(_ => { + return animationRef.effect.getComputedTiming().localTime == 400; + }); + assert_times_equal(animation.effect.getComputedTiming().localTime, 200, + 'When the timeline becomes newly active, the underlying effect\'s ' + + 'timing should be properly updated.'); + + // Make the timeline inactive again. + scroller.style.display = "none" + await waitForNextFrame(); + + assert_times_equal(animation.currentTime, 200, + 'Current time must be the previous current time.'); + assert_equals(animation.startTime, null, + 'Initial start time must be unresolved.'); + + scrollerRef.scrollTop = 0.6 * maxScroll; + // Wait until local times are synced back to the main thread. + await waitForAnimationFrameWithCondition(_ => { + return animationRef.effect.getComputedTiming().localTime == 600; + }); + + assert_times_equal(animation.effect.getComputedTiming().localTime, 200, + 'When the timeline becomes newly inactive, the underlying effect\'s ' + + 'timing should stay unchanged.'); + }, 'When timeline time becomes inactive previous current time must be ' + + 'the current time and start time unresolved'); + done(); + }); +} +</script> +</body>
\ No newline at end of file diff --git a/testing/web-platform/tests/animation-worklet/multiple-effects-on-same-target-driven-by-individual-local-time.https.html b/testing/web-platform/tests/animation-worklet/multiple-effects-on-same-target-driven-by-individual-local-time.https.html new file mode 100644 index 0000000000..edf8488ded --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/multiple-effects-on-same-target-driven-by-individual-local-time.https.html @@ -0,0 +1,65 @@ +<!DOCTYPE html> +<title>Multiple effects on same target driven by individual local time</title> +<link rel="help" href="https://drafts.css-houdini.org/css-animationworklet/"> + +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="common.js"></script> + +<style> + #target { + width: 100px; + height: 100px; + background-color: green; + } + #target2 { + width: 100px; + height: 100px; + background-color: blue; + box-shadow: 4px 4px 25px blue; + } +</style> + +<div id="target"></div> + +<script id="simple_animate" type="text/worklet"> + registerAnimator("test_animator", class { + animate(currentTime, effect) { + let effects = effect.getChildren(); + effects[0].localTime = 0; + effects[1].localTime = 1000; + } + }); +</script> + +<script> + promise_test(async t => { + await runInAnimationWorklet(document.getElementById('simple_animate').textContent); + + const effect = new KeyframeEffect( + document.getElementById("target"), + [ + { background: 'green' }, + { background: 'blue' }, + ], + { duration: 2000 } + ); + const effect2 = new KeyframeEffect( + document.getElementById("target"), + [ + { width: '100px' }, + { width: '200px' } + ], + { duration: 2000 } + ); + + const animation = new WorkletAnimation('test_animator', [effect, effect2]); + animation.play(); + await waitForAsyncAnimationFrames(1); + + assert_equals(getComputedStyle(document.getElementById('target')).backgroundColor, "rgb(0, 128, 0)"); + assert_equals(getComputedStyle(document.getElementById('target')).width, "150px"); + }, `Animating multiple effects on the same target using effect specific local time should output values + relative to each effects unique local time`); +</script>
\ No newline at end of file diff --git a/testing/web-platform/tests/animation-worklet/playback-rate.https.html b/testing/web-platform/tests/animation-worklet/playback-rate.https.html new file mode 100644 index 0000000000..2e2fb9a099 --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/playback-rate.https.html @@ -0,0 +1,345 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>The playback rate of a worklet animation</title> +<link rel="help" href="https://drafts.css-houdini.org/css-animationworklet/"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +'use strict'; +// Presence of playback rate adds FP operations to calculating start_time +// and current_time of animations. That's why it's needed to increase FP error +// for comparing times in these tests. +window.assert_times_equal = (actual, expected, description) => { + assert_approx_equals(actual, expected, 0.002, description); +}; +</script> +<script src="/web-animations/testcommon.js"></script> +<script src="common.js"></script> +<style> + .scroller { + overflow: auto; + height: 100px; + width: 100px; + } + .contents { + height: 1000px; + width: 100%; + } +</style> +<body> +<div id="log"></div> +<script> +'use strict'; + +function createWorkletAnimation(test) { + const DURATION = 10000; // ms + const KEYFRAMES = { transform: ['translateY(100px)', 'translateY(200px)'] }; + return new WorkletAnimation('passthrough', new KeyframeEffect(createDiv(test), + KEYFRAMES, DURATION), document.timeline); +} + +function createScroller(test) { + var scroller = createDiv(test); + scroller.innerHTML = "<div class='contents'></div>"; + scroller.classList.add('scroller'); + return scroller; +} + +function createScrollLinkedWorkletAnimation(test) { + const timeline = new ScrollTimeline({ + scrollSource: createScroller(test) + }); + const DURATION = 10000; // ms + const KEYFRAMES = { transform: ['translateY(100px)', 'translateY(200px)'] }; + return new WorkletAnimation('passthrough', new KeyframeEffect(createDiv(test), + KEYFRAMES, DURATION), timeline); +} + +setup(setupAndRegisterTests, {explicit_done: true}); + +function setupAndRegisterTests() { + registerPassthroughAnimator().then(() => { + + promise_test(async t => { + const animation = createWorkletAnimation(t); + + animation.playbackRate = 0.5; + animation.play(); + assert_equals(animation.currentTime, 0, + 'Zero current time is not affected by playbackRate.'); + }, 'Zero current time is not affected by playbackRate set while the ' + + 'animation is in idle state.'); + + promise_test(async t => { + const animation = createWorkletAnimation(t); + + animation.play(); + animation.playbackRate = 0.5; + assert_equals(animation.currentTime, 0, + 'Zero current time is not affected by playbackRate.'); + }, 'Zero current time is not affected by playbackRate set while the ' + + 'animation is in play-pending state.'); + + promise_test(async t => { + const animation = createWorkletAnimation(t); + const playbackRate = 2; + + animation.play(); + + await waitForAnimationFrameWithCondition(_=> { + return animation.playState == "running" + }); + // Make sure the current time is not Zero. + await waitForDocumentTimelineAdvance(); + + // Set playback rate while the animation is playing. + const prevCurrentTime = animation.currentTime; + animation.playbackRate = playbackRate; + + assert_times_equal(animation.currentTime, prevCurrentTime, + 'The current time should stay unaffected by setting playback rate.'); + }, 'Non zero current time is not affected by playbackRate set while the ' + + 'animation is in play state.'); + + promise_test(async t => { + const animation = createWorkletAnimation(t); + const playbackRate = 0.2; + + animation.play(); + + await waitForAnimationFrameWithCondition(_=> { + return animation.playState == "running" + }); + + // Set playback rate while the animation is playing. + const prevCurrentTime = animation.currentTime; + const prevTimelineTime = document.timeline.currentTime; + animation.playbackRate = playbackRate; + + // Play the animation some more. + await waitForDocumentTimelineAdvance(); + + const currentTime = animation.currentTime; + const currentTimelineTime = document.timeline.currentTime; + + assert_times_equal( + currentTime - prevCurrentTime, + (currentTimelineTime - prevTimelineTime) * playbackRate, + 'The current time should increase 0.2 times faster than timeline.'); + }, 'The playback rate affects the rate of progress of the current time.'); + + promise_test(async t => { + const animation = createWorkletAnimation(t); + const playbackRate = 2; + + // Set playback rate while the animation is in 'idle' state. + animation.playbackRate = playbackRate; + const prevTimelineTime = document.timeline.currentTime; + animation.play(); + + await waitForAnimationFrameWithCondition(_=> { + return animation.playState == "running" + }); + await waitForDocumentTimelineAdvance(); + + const currentTime = animation.currentTime; + const timelineTime = document.timeline.currentTime; + assert_times_equal( + currentTime, + (timelineTime - prevTimelineTime) * playbackRate, + 'The current time should increase two times faster than timeline.'); + }, 'The playback rate set before the animation started playing affects ' + + 'the rate of progress of the current time'); + + promise_test(async t => { + const timing = { duration: 100, + easing: 'linear', + fill: 'none', + iterations: 1 + }; + // TODO(crbug.com/937382): Currently composited + // workletAnimation.currentTime and the corresponding + // effect.getComputedTiming().localTime are computed by main and + // compositing threads respectively and, as a result, don't match. + // To workaround this limitation we compare the output of two identical + // animations that only differ in playback rate. The expectation is that + // their output matches after taking their playback rates into + // consideration. This works since these two animations start at the same + // time on the same thread. + // Once the issue is fixed, this test needs to change so expected + // effect.getComputedTiming().localTime is compared against + // workletAnimation.currentTime. + const target = createDiv(t); + const targetRef = createDiv(t); + const keyframeEffect = new KeyframeEffect( + target, { opacity: [1, 0] }, timing); + const keyframeEffectRef = new KeyframeEffect( + targetRef, { opacity: [1, 0] }, timing); + const animation = new WorkletAnimation( + 'passthrough', keyframeEffect, document.timeline); + const animationRef = new WorkletAnimation( + 'passthrough', keyframeEffectRef, document.timeline); + const playbackRate = 2; + animation.playbackRate = playbackRate; + animation.play(); + animationRef.play(); + + // wait until local times are synced back to the main thread. + await waitForAnimationFrameWithCondition(_ => { + return getComputedStyle(target).opacity != '1'; + }); + + assert_times_equal( + keyframeEffect.getComputedTiming().localTime, + keyframeEffectRef.getComputedTiming().localTime * playbackRate, + 'When playback rate is set on WorkletAnimation, the underlying ' + + 'effect\'s timing should be properly updated.'); + + assert_approx_equals( + 1 - Number(getComputedStyle(target).opacity), + (1 - Number(getComputedStyle(targetRef).opacity)) * playbackRate, + 0.001, + 'When playback rate is set on WorkletAnimation, the underlying effect' + + ' should produce correct visual result.'); + }, 'When playback rate is updated, the underlying effect is properly ' + + 'updated with the current time of its WorkletAnimation and produces ' + + 'correct visual result.'); + + promise_test(async t => { + const animation = createScrollLinkedWorkletAnimation(t); + const scroller = animation.timeline.scrollSource; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.2 * maxScroll; + + animation.playbackRate = 0.5; + animation.play(); + await waitForAnimationFrameWithCondition(_=> { + return animation.playState == "running" + }); + assert_percents_equal(animation.currentTime, 10, + 'Initial current time is scaled by playbackRate.'); + }, 'Initial current time is scaled by playbackRate set while ' + + 'scroll-linked animation is in idle state.'); + + promise_test(async t => { + const animation = createScrollLinkedWorkletAnimation(t); + const scroller = animation.timeline.scrollSource; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.2 * maxScroll; + + animation.play(); + animation.playbackRate = 0.5; + + assert_percents_equal(animation.currentTime, 20, + 'Initial current time is not affected by playbackRate.'); + }, 'Initial current time is not affected by playbackRate set while '+ + 'scroll-linked animation is in play-pending state.'); + + promise_test(async t => { + const animation = createScrollLinkedWorkletAnimation(t); + const scroller = animation.timeline.scrollSource; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + const playbackRate = 2; + + animation.play(); + scroller.scrollTop = 0.2 * maxScroll; + await waitForAnimationFrameWithCondition(_=> { + return animation.playState == "running" + }); + // Set playback rate while the animation is playing. + animation.playbackRate = playbackRate; + assert_percents_equal(animation.currentTime, 20, + 'The current time should stay unaffected by setting playback rate.'); + }, 'The current time is not affected by playbackRate set while the ' + + 'scroll-linked animation is in play state.'); + + promise_test(async t => { + const animation = createScrollLinkedWorkletAnimation(t); + const scroller = animation.timeline.scrollSource; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + const playbackRate = 2; + + animation.play(); + await waitForAnimationFrameWithCondition(_=> { + return animation.playState == "running" + }); + scroller.scrollTop = 0.1 * maxScroll; + + // Set playback rate while the animation is playing. + animation.playbackRate = playbackRate; + + scroller.scrollTop = 0.2 * maxScroll; + + assert_equals( + animation.currentTime.value - 10, 10 * playbackRate, + 'The current time should increase twice faster than scroll timeline.'); + }, 'Scroll-linked animation playback rate affects the rate of progress ' + + 'of the current time.'); + + promise_test(async t => { + const animation = createScrollLinkedWorkletAnimation(t); + const scroller = animation.timeline.scrollSource; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + const playbackRate = 2; + + // Set playback rate while the animation is in 'idle' state. + animation.playbackRate = playbackRate; + animation.play(); + await waitForAnimationFrameWithCondition(_=> { + return animation.playState == "running" + }); + scroller.scrollTop = 0.2 * maxScroll; + + assert_percents_equal(animation.currentTime, 20 * playbackRate, + 'The current time should increase two times faster than timeline.'); + }, 'The playback rate set before scroll-linked animation started playing ' + + 'affects the rate of progress of the current time'); + + promise_test(async t => { + const scroller = createScroller(t); + const timeline = new ScrollTimeline({ + scrollSource: scroller + }); + const timing = { duration: 1000, + easing: 'linear', + fill: 'none', + iterations: 1 + }; + const target = createDiv(t); + const keyframeEffect = new KeyframeEffect( + target, { opacity: [1, 0] }, timing); + const animation = new WorkletAnimation( + 'passthrough', keyframeEffect, timeline); + const playbackRate = 2; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + + animation.play(); + animation.playbackRate = playbackRate; + await waitForAnimationFrameWithCondition(_=> { + return animation.playState == "running" + }); + + scroller.scrollTop = 0.2 * maxScroll; + // wait until local times are synced back to the main thread. + await waitForAnimationFrameWithCondition(_ => { + return getComputedStyle(target).opacity != '1'; + }); + + assert_percents_equal( + keyframeEffect.getComputedTiming().localTime, + 20 * playbackRate, + 'When playback rate is set on WorkletAnimation, the underlying ' + + 'effect\'s timing should be properly updated.'); + assert_approx_equals( + Number(getComputedStyle(target).opacity), + 1 - 20 * playbackRate / 1000, 0.001, + 'When playback rate is set on WorkletAnimation, the underlying ' + + 'effect should produce correct visual result.'); + }, 'When playback rate is updated, the underlying effect is properly ' + + 'updated with the current time of its scroll-linked WorkletAnimation ' + + 'and produces correct visual result.'); + done(); + }); +} +</script> +</body>
\ No newline at end of file diff --git a/testing/web-platform/tests/animation-worklet/references/not-translated-box-ref.html b/testing/web-platform/tests/animation-worklet/references/not-translated-box-ref.html new file mode 100644 index 0000000000..96acf1ad96 --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/references/not-translated-box-ref.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<title>Reference for Animation Worklet local time set after duration</title> +<style> +#box { + width: 100px; + height: 100px; + background-color: green; + will-change: transform; +} +</style> + +<div id="box"></div> diff --git a/testing/web-platform/tests/animation-worklet/references/translated-box-ref.html b/testing/web-platform/tests/animation-worklet/references/translated-box-ref.html new file mode 100644 index 0000000000..f1dde2e19b --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/references/translated-box-ref.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<style> +#box { + width: 100px; + height: 100px; + transform: translateY(100px); + background-color: green; + will-change: transform; +} +</style> + +<div id="box"></div> diff --git a/testing/web-platform/tests/animation-worklet/resources/animator-iframe.html b/testing/web-platform/tests/animation-worklet/resources/animator-iframe.html new file mode 100644 index 0000000000..f9a5fab9b7 --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/resources/animator-iframe.html @@ -0,0 +1,44 @@ +<!DOCTYPE html> +<style> +.greenbox { + width: 100px; + height: 100px; + background-color: #00ff00; +} +</style> +<script src="/web-animations/testcommon.js"></script> +<script src="../common.js"></script> + +<script id="iframe_worklet" type="text/worklet"> +registerAnimator("iframe_animator", class { + animate(currentTime, effect) { + effect.localTime = 600; + } +}); +registerAnimator("duplicate_animator", class { + animate(currentTime, effect) { + effect.localTime = 800; + } +}); +</script> + +<div id="iframe_target" class="greenbox"></div> + +<script> +runInAnimationWorklet( + document.getElementById('iframe_worklet').textContent +).then(_ => { + const target = document.getElementById('iframe_target'); + // Only create an animation for iframe_animator. + const effect = new KeyframeEffect(target, [{ opacity: 0 }], { duration: 1000 }); + const animation = new WorkletAnimation('iframe_animator', effect); + animation.play(); + + // wait until local times are synced back to the main thread. + waitForAnimationFrameWithCondition(_ => { + return getComputedStyle(target).opacity != '1'; + }).then(_ => { + window.parent.postMessage(getComputedStyle(target).opacity, '*'); + }); + }); +</script> diff --git a/testing/web-platform/tests/animation-worklet/resources/iframe.html b/testing/web-platform/tests/animation-worklet/resources/iframe.html new file mode 100644 index 0000000000..e128fa53e4 --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/resources/iframe.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<style> +#iframe_box { + width: 100px; + height: 100px; + background-color: green; +} +</style> +<div id='iframe_box'></div> diff --git a/testing/web-platform/tests/animation-worklet/scroll-timeline-writing-modes.https.html b/testing/web-platform/tests/animation-worklet/scroll-timeline-writing-modes.https.html new file mode 100644 index 0000000000..2bd17a89da --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/scroll-timeline-writing-modes.https.html @@ -0,0 +1,168 @@ +<!DOCTYPE html> +<title>Tests that ScrollTimeline works properly with writing mode and directionality</title> +<link rel="help" href="https://drafts.css-houdini.org/css-animationworklet/"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="common.js"></script> + + +<script> +// Creates a DOM structure like: +// - container +// - box {100x100} +// - scroller {100x100, writing-mode, direction} +// - contents +function createTestDOM(x_scroll_axis, writing_mode, direction) { + const elements = {}; + + elements.container = document.createElement('div'); + + elements.box = document.createElement('div'); + elements.box.style.height = '100px'; + elements.box.style.width = '100px'; + + elements.scroller = document.createElement('div'); + elements.scroller.style.height = '100px'; + elements.scroller.style.width = '100px'; + if (x_scroll_axis) + elements.scroller.style.overflowX = 'scroll'; + else + elements.scroller.style.overflowY = 'scroll'; + elements.scroller.style.direction = direction; + elements.scroller.style.writingMode = writing_mode; + + // Callers don't need access to this. + const contents = document.createElement('div'); + contents.style.height = x_scroll_axis ? '100%' : '1000px'; + contents.style.width = x_scroll_axis ? '1000px' : '100%'; + + elements.scroller.appendChild(contents); + elements.container.appendChild(elements.box); + elements.container.appendChild(elements.scroller); + document.body.appendChild(elements.container); + + return elements; +} + +function createAndPlayTestAnimation(elements, timeline_orientation) { + const effect = new KeyframeEffect( + elements.box, + [{transform: 'translateY(0)'}, {transform: 'translateY(200px)'}], { + duration: 1000, + }); + + const timeline = new ScrollTimeline({ + scrollSource: elements.scroller, + orientation: timeline_orientation + }); + const animation = new WorkletAnimation('passthrough', effect, timeline); + animation.play(); + return animation; +} + +setup(setupAndRegisterTests, {explicit_done: true}); + +function setupAndRegisterTests() { + registerPassthroughAnimator().then(() => { + // Note that block horizontal-tb is tested implicitly in the basic + // ScrollTimeline tests (as it is the default). + async_test( + block_vertical_lr, + 'A block ScrollTimeline should produce the correct current time for vertical-lr'); + async_test( + block_vertical_rl, + 'A block ScrollTimeline should produce the correct current time for vertical-rl'); + // Again, inline for horizontal-tb and direction: ltr is the default + // inline mode and so is tested elsewhere. + async_test( + inline_horizontal_tb_rtl, + 'An inline ScrollTimeline should produce the correct current time for horizontal-tb and direction: rtl'); + async_test( + inline_vertical_writing_mode_ltr, + 'An inline ScrollTimeline should produce the correct current time for vertical writing mode'); + async_test( + inline_vertical_writing_mode_rtl, + 'An inline ScrollTimeline should produce the correct current time for vertical writing mode and direction: rtl'); + done(); + }); +} + +function block_vertical_lr(t) { + const elements = createTestDOM(true, 'vertical-lr', 'ltr'); + const animation = createAndPlayTestAnimation(elements, 'block'); + + // Move the scroller to the 25% point. + const maxScroll = + elements.scroller.scrollWidth - elements.scroller.clientWidth; + elements.scroller.scrollLeft = 0.25 * maxScroll; + + waitForNotNullLocalTime(animation).then(t.step_func_done(() => { + assert_equals( + getComputedStyle(elements.box).transform, 'matrix(1, 0, 0, 1, 0, 50)'); + })); +} + +function block_vertical_rl(t) { + const elements = createTestDOM(true, 'vertical-rl', 'ltr'); + const animation = createAndPlayTestAnimation(elements, 'block'); + + // Move the scroller to the left 25% point, since it is vertical-rl, + // i.e leftwards overflow direction, scrollLeft is -25% point. + const maxScroll = + elements.scroller.scrollWidth - elements.scroller.clientWidth; + elements.scroller.scrollLeft = -0.25 * maxScroll; + + waitForNotNullLocalTime(animation).then(t.step_func_done(() => { + assert_equals( + getComputedStyle(elements.box).transform, 'matrix(1, 0, 0, 1, 0, 50)'); + })); +} + +function inline_horizontal_tb_rtl(t) { + const elements = createTestDOM(true, 'horizontal-tb', 'rtl'); + const animation = createAndPlayTestAnimation(elements, 'inline'); + + // Move the scroller to the left 25% point, since it is direction: rtl, + // i.e leftwards overflow direction, scrollLeft is -25% point. + const maxScroll = + elements.scroller.scrollWidth - elements.scroller.clientWidth; + elements.scroller.scrollLeft = -0.25 * maxScroll; + + waitForNotNullLocalTime(animation).then(t.step_func_done(() => { + assert_equals( + getComputedStyle(elements.box).transform, 'matrix(1, 0, 0, 1, 0, 50)'); + })); +} + +function inline_vertical_writing_mode_ltr(t) { + const elements = createTestDOM(false, 'vertical-lr', 'ltr'); + const animation = createAndPlayTestAnimation(elements, 'inline'); + + // Move the scroller to the 25% point. + const maxScroll = + elements.scroller.scrollHeight - elements.scroller.clientHeight; + elements.scroller.scrollTop = 0.25 * maxScroll; + + waitForNotNullLocalTime(animation).then(t.step_func_done(() => { + assert_equals( + getComputedStyle(elements.box).transform, 'matrix(1, 0, 0, 1, 0, 50)'); + })); +} + +function inline_vertical_writing_mode_rtl(t) { + const elements = createTestDOM(false, 'vertical-lr', 'rtl'); + const animation = createAndPlayTestAnimation(elements, 'inline'); + + // Move the scroller to the top 25% point, since it is a vertical-lr writing mode + // and direction: rtl, i.e upwards overflow direction, scrollTop is -25% point. + const maxScroll = + elements.scroller.scrollHeight - elements.scroller.clientHeight; + elements.scroller.scrollTop = -0.25 * maxScroll; + + waitForNotNullLocalTime(animation).then(t.step_func_done(() => { + assert_equals( + getComputedStyle(elements.box).transform, 'matrix(1, 0, 0, 1, 0, 50)'); + })); +} +</script> diff --git a/testing/web-platform/tests/animation-worklet/stateful-animator.https.html b/testing/web-platform/tests/animation-worklet/stateful-animator.https.html new file mode 100644 index 0000000000..be29fa109c --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/stateful-animator.https.html @@ -0,0 +1,216 @@ +<!DOCTYPE html> +<title>Basic use of stateful animator</title> +<link rel="help" href="https://drafts.css-houdini.org/css-animationworklet/"> + +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="common.js"></script> + +<div id="target"></div> + +<script id="stateful_animator_basic" type="text/worklet"> + registerAnimator("stateful_animator_basic", class { + constructor(options, state = { test_local_time: 0 }) { + this.test_local_time = state.test_local_time; + } + animate(currentTime, effect) { + effect.localTime = this.test_local_time++; + } + state() { + return { + test_local_time: this.test_local_time + }; + } + }); +</script> + +<script id="stateless_animator_basic" type="text/worklet"> + registerAnimator("stateless_animator_basic", class { + constructor(options, state = { test_local_time: 0 }) { + this.test_local_time = state.test_local_time; + } + animate(currentTime, effect) { + effect.localTime = this.test_local_time++; + } + // Unless a valid state function is provided, the animator is considered + // stateless. e.g. animator with incorrect state function name. + State() { + return { + test_local_time: this.test_local_time + }; + } + }); +</script> + +<script id="stateless_animator_preserves_effect_local_time" type="text/worklet"> + registerAnimator("stateless_animator_preserves_effect_local_time", class { + animate(currentTime, effect) { + // The local time will be carried over to the new global scope. + effect.localTime = effect.localTime ? effect.localTime + 1 : 1; + } + }); +</script> + +<script id="stateless_animator_does_not_copy_effect_object" type="text/worklet"> + registerAnimator("stateless_animator_does_not_copy_effect_object", class { + animate(currentTime, effect) { + effect.localTime = effect.localTime ? effect.localTime + 1 : 1; + effect.foo = effect.foo ? effect.foo + 1 : 1; + // This condition becomes true once we switch global scope and only preserve local time + // otherwise these values keep increasing in lock step. + if (effect.localTime > effect.foo) { + // This works as long as we switch global scope before 10000 frames. + // which is a safe assumption. + effect.localTime = 10000; + } + } + }); +</script> + +<script id="state_function_returns_empty" type="text/worklet"> + registerAnimator("state_function_returns_empty", class { + constructor(options, state = { test_local_time: 0 }) { + this.test_local_time = state.test_local_time; + } + animate(currentTime, effect) { + effect.localTime = this.test_local_time++; + } + state() {} + }); +</script> + +<script id="state_function_returns_not_serializable" type="text/worklet"> + registerAnimator("state_function_returns_not_serializable", class { + constructor(options) { + this.test_local_time = 0; + } + animate(currentTime, effect) { + effect.localTime = this.test_local_time++; + } + state() { + return new Symbol('foo'); + } + }); +</script> + +<script> + const EXPECTED_FRAMES_TO_A_SCOPE_SWITCH = 15; + async function localTimeDoesNotUpdate(animation) { + // The local time stops increasing after the animator instance being dropped. + // e.g. 0, 1, 2, .., n, n, n, n, .. where n is the frame that the global + // scope switches at. + let last_local_time = animation.effect.getComputedTiming().localTime; + let frame_count = 0; + const FRAMES_WITHOUT_CHANGE = 10; + do { + await new Promise(window.requestAnimationFrame); + let current_local_time = animation.effect.getComputedTiming().localTime; + if (approxEquals(last_local_time, current_local_time)) + ++frame_count; + else + frame_count = 0; + last_local_time = current_local_time; + } while (frame_count < FRAMES_WITHOUT_CHANGE); + } + + async function localTimeResetsToZero(animation) { + // The local time is reset upon global scope switching. e.g. + // 0, 1, 2, .., 0, 1, 2, .., 0, 1, 2, .., 0, 1, 2, ... + let reset_count = 0; + const LOCAL_TIME_RESET_CHECK = 3; + do { + await new Promise(window.requestAnimationFrame); + if (approxEquals(0, animation.effect.getComputedTiming().localTime)) + ++reset_count; + } while (reset_count < LOCAL_TIME_RESET_CHECK); + } + + promise_test(async t => { + await runInAnimationWorklet(document.getElementById('stateful_animator_basic').textContent); + const target = document.getElementById('target'); + const effect = new KeyframeEffect(target, [{ opacity: 0 }], { duration: 1000 }); + const animation = new WorkletAnimation('stateful_animator_basic', effect); + animation.play(); + + // effect.localTime should be correctly increased upon global scope + // switches for stateful animators. + await waitForAnimationFrameWithCondition(_ => { + return approxEquals(animation.effect.getComputedTiming().localTime, + EXPECTED_FRAMES_TO_A_SCOPE_SWITCH); + }); + + animation.cancel(); + }, "Stateful animator can use its state to update the animation. Pass if test does not timeout"); + + promise_test(async t => { + await runInAnimationWorklet(document.getElementById('stateless_animator_basic').textContent); + const target = document.getElementById('target'); + const effect = new KeyframeEffect(target, [{ opacity: 0 }], { duration: 1000 }); + const animation = new WorkletAnimation('stateless_animator_basic', effect); + animation.play(); + + // The local time should be reset to 0 upon global scope switching for + // stateless animators. + await localTimeResetsToZero(animation); + + animation.cancel(); + }, "Stateless animator gets reecreated with 'undefined' state."); + + promise_test(async t => { + await runInAnimationWorklet(document.getElementById('stateless_animator_preserves_effect_local_time').textContent); + const target = document.getElementById('target'); + const effect = new KeyframeEffect(target, [{ opacity: 0 }], { duration: 1000 }); + const animation = new WorkletAnimation('stateless_animator_preserves_effect_local_time', effect); + animation.play(); + + await waitForAnimationFrameWithCondition(_ => { + return approxEquals(animation.effect.getComputedTiming().localTime, + EXPECTED_FRAMES_TO_A_SCOPE_SWITCH); + }); + + animation.cancel(); + }, "Stateless animator should preserve the local time of its effect."); + + promise_test(async t => { + await runInAnimationWorklet(document.getElementById('stateless_animator_does_not_copy_effect_object').textContent); + const target = document.getElementById('target'); + const effect = new KeyframeEffect(target, [{ opacity: 0 }], { duration: 1000 }); + const animation = new WorkletAnimation('stateless_animator_does_not_copy_effect_object', effect); + animation.play(); + + await waitForAnimationFrameWithCondition(_ => { + return approxEquals(animation.effect.getComputedTiming().localTime, 10000); + }); + + animation.cancel(); + }, "Stateless animator should not copy the effect object."); + + promise_test(async t => { + await runInAnimationWorklet(document.getElementById('state_function_returns_empty').textContent); + const target = document.getElementById('target'); + const effect = new KeyframeEffect(target, [{ opacity: 0 }], { duration: 1000 }); + const animation = new WorkletAnimation('state_function_returns_empty', effect); + animation.play(); + + // The local time should be reset to 0 upon global scope switching for + // stateless animators. + await localTimeResetsToZero(animation); + + animation.cancel(); + }, "Stateful animator gets recreated with 'undefined' state if state function returns undefined."); + + promise_test(async t => { + await runInAnimationWorklet(document.getElementById('state_function_returns_not_serializable').textContent); + const target = document.getElementById('target'); + const effect = new KeyframeEffect(target, [{ opacity: 0 }], { duration: 1000, iteration: Infinity }); + const animation = new WorkletAnimation('state_function_returns_not_serializable', effect); + animation.play(); + + // The local time of an animation increases until the registered animator + // gets removed. + await localTimeDoesNotUpdate(animation); + + animation.cancel(); + }, "Stateful Animator instance gets dropped (does not get migrated) if state function is not serializable."); +</script> diff --git a/testing/web-platform/tests/animation-worklet/worklet-animation-animator-name.https.html b/testing/web-platform/tests/animation-worklet/worklet-animation-animator-name.https.html new file mode 100644 index 0000000000..bd886ccd02 --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/worklet-animation-animator-name.https.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> + +<title>Worklet Animation's animator name should be accessible via animatorName property</title> +<link rel="help" href="https://drafts.css-houdini.org/css-animationworklet/"> + +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="common.js"></script> + +<script id="test_animator" type="text/worklet"> + class NoopAnimator { + animate(currentTime, effect) {} + } + registerAnimator('Tokyo', NoopAnimator); + registerAnimator('دزفول', NoopAnimator); +</script> + +<body></body> + +<script> +promise_test(async t => { + await runInAnimationWorklet(document.getElementById('test_animator').textContent); + + // An ascii name and a non-ascii one. + for (let name of ['Tokyo', 'دزفول']) { + const animation = new WorkletAnimation(name, new KeyframeEffect(document.body, {})); + assert_equals(name, animation.animatorName); + } +}, 'Verify that animatorName matches passed name'); +</script>
\ No newline at end of file diff --git a/testing/web-platform/tests/animation-worklet/worklet-animation-cancel.https.html b/testing/web-platform/tests/animation-worklet/worklet-animation-cancel.https.html new file mode 100644 index 0000000000..3b664ecddb --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/worklet-animation-cancel.https.html @@ -0,0 +1,45 @@ +<html class="reftest-wait"> +<title>Canceling a playing WorkletAnimation should remove the effect</title> +<link rel="help" href="https://drafts.css-houdini.org/css-animationworklet/"> +<meta name="assert" content="Canceling a playing animation should remove the effect"> +<link rel="match" href="references/not-translated-box-ref.html"> + +<script src="/web-animations/testcommon.js"></script> +<script src="/common/reftest-wait.js"></script> +<script src="common.js"></script> + +<style> + #box { + width: 100px; + height: 100px; + background-color: green; + } +</style> + +<div id="box"></div> + +<script> + registerConstantLocalTimeAnimator(500).then(_ => { + const box = document.getElementById('box'); + const effect = new KeyframeEffect(box, + [ + {transform: 'translateY(0)'}, + {transform: 'translateY(200px)'} + ], { + duration: 1000, + iterations: Infinity + } + ); + + const animation = new WorkletAnimation('constant_time', effect); + animation.play(); + + waitForAsyncAnimationFrames(1).then(_ => { + // Canceling a playing animation should remove the effect. + animation.cancel(); + waitForAsyncAnimationFrames(1).then(_ => { + takeScreenshot(); + }); + }); + }); +</script> diff --git a/testing/web-platform/tests/animation-worklet/worklet-animation-creation.https.html b/testing/web-platform/tests/animation-worklet/worklet-animation-creation.https.html new file mode 100644 index 0000000000..b7d1a43721 --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/worklet-animation-creation.https.html @@ -0,0 +1,141 @@ +<!DOCTYPE html> +<title>Verify that WorkletAnimation is correctly created</title> +<link rel="help" href="https://drafts.css-houdini.org/css-animationworklet/"> + +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="common.js"></script> + +<style> + .scroller { + height: 100px; + width: 100px; + overflow: scroll; + } + .content { + height: 500px; + width: 500px; + } +</style> + +<script> +function CreateKeyframeEffect(element) { + return new KeyframeEffect( + element, + [ + { transform: 'translateY(0%)' }, + { transform: 'translateY(100%)' } + ], + { duration: 3000, fill: 'forwards' } + ); +} +</script> +<script id="simple_animate" type="text/worklet"> + registerAnimator("test-animator", class { + animate(currentTime, effect) {} + }); +</script> + +<div id='element'></div> +<div id='element2'></div> +<div class='scroller'> + <div class='content'></div> +</div> + +<script> + promise_test(async t => { + await runInAnimationWorklet(document.getElementById('simple_animate').textContent); + let effect = CreateKeyframeEffect(document.querySelector('#element')); + let workletAnimation = new WorkletAnimation('test-animator', effect); + assert_equals(workletAnimation.playState, 'idle'); + assert_equals(workletAnimation.timeline, document.timeline); + }, 'WorkletAnimation creation without timeline should use default documentation timeline'); + + promise_test(async t => { + await runInAnimationWorklet(document.getElementById('simple_animate').textContent); + let effect = CreateKeyframeEffect(document.querySelector('#element')); + let workletAnimation = new WorkletAnimation('test-animator', effect); + assert_equals(workletAnimation.playState, 'idle'); + }, 'WorkletAnimation creation with timeline should work'); + + promise_test(async t => { + await runInAnimationWorklet(document.getElementById('simple_animate').textContent); + let iframe = document.createElement('iframe'); + iframe.src = 'resources/iframe.html'; + document.body.appendChild(iframe); + + await waitForAnimationFrameWithCondition(_ => { + return iframe.contentDocument.getElementById('iframe_box') != null; + }); + let iframe_document = iframe.contentDocument; + let effect = CreateKeyframeEffect(iframe_document.getElementById('iframe_box')); + + let animation_with_main_frame_timeline = + new WorkletAnimation('test-animator', effect, document.timeline); + assert_equals(animation_with_main_frame_timeline.timeline, document.timeline); + + let animation_with_iframe_timeline = + new WorkletAnimation('test-animator', effect, iframe_document.timeline); + assert_equals(animation_with_iframe_timeline.timeline, iframe_document.timeline); + + let animation_with_default_timeline = new WorkletAnimation('test-animator', effect); + // The spec says that the default timeline is taken from 'the Document that is + // associated with the window that is the current global object'. In this case + // that is the main document's timeline, not the iframe (despite the target + // being in the iframe). + assert_equals(animation_with_default_timeline.timeline, document.timeline); + + iframe.remove(); + }, 'WorkletAnimation creation should choose the correct timeline based on the current global object'); + + promise_test(async t => { + await runInAnimationWorklet(document.getElementById('simple_animate').textContent); + let effect = CreateKeyframeEffect(document.querySelector('#element')); + let options = { my_param: 'foo', my_other_param: true }; + let workletAnimation = new WorkletAnimation( + 'test-animator', effect, document.timeline, options); + assert_equals(workletAnimation.playState, 'idle'); + }, 'WorkletAnimation creation with timeline and options should work'); + + promise_test(async t => { + await runInAnimationWorklet(document.getElementById('simple_animate').textContent); + let effect = CreateKeyframeEffect(document.querySelector('#element')); + let scroller = document.querySelector('.scroller'); + let scrollTimeline = new ScrollTimeline( + { scrollSource: scroller, orientation: 'inline' }); + let workletAnimation = new WorkletAnimation( + 'test-animator', effect, scrollTimeline); + assert_equals(workletAnimation.playState, 'idle'); + }, 'ScrollTimeline is a valid timeline for a WorkletAnimation'); + + promise_test(async t => { + await runInAnimationWorklet(document.getElementById('simple_animate').textContent); + let constructorFunc = function() { new WorkletAnimation( + 'test-animator', []); }; + assert_throws_dom('NotSupportedError', constructorFunc); + }, 'If there are no effects specified, object construction should fail'); + + promise_test(async t => { + await runInAnimationWorklet(document.getElementById('simple_animate').textContent); + let effect = CreateKeyframeEffect(document.querySelector('#element')); + + let otherDoc = document.implementation.createHTMLDocument(); + let otherElement = otherDoc.createElement('div'); + otherDoc.body.appendChild(otherElement); + let otherEffect = CreateKeyframeEffect(otherElement); + + let workletAnimation = new WorkletAnimation( + 'test-animator', [ effect, otherEffect ]); + assert_equals(workletAnimation.playState, 'idle'); + }, 'Creating animation with effects from different documents is allowed'); + + promise_test(async t => { + await runInAnimationWorklet(document.getElementById('simple_animate').textContent); + let effect = CreateKeyframeEffect(document.querySelector('#element')); + let constructorFunc = function() { + new WorkletAnimation('unregistered-animator', effect); + }; + assert_throws_dom('InvalidStateError', constructorFunc); + }, 'Constructing worklet animation for unregisested animator should throw'); +</script> diff --git a/testing/web-platform/tests/animation-worklet/worklet-animation-duration.https.html b/testing/web-platform/tests/animation-worklet/worklet-animation-duration.https.html new file mode 100644 index 0000000000..1a8afc1e89 --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/worklet-animation-duration.https.html @@ -0,0 +1,39 @@ +<html> +<title>WorkletAnimation should continue to be in effect forever, even if its duration is passed</title> +<link rel="help" href="https://drafts.css-houdini.org/css-animationworklet/"> + +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="common.js"></script> + +<div id="box"></div> + +<script> + promise_test(async t => { + await registerConstantLocalTimeAnimator(0.5); + + const box = document.getElementById('box'); + const effect = new KeyframeEffect(box, + [ + {transform: 'translateY(0px)' }, + {transform: 'translateY(200px)' } + ], { + duration: 1, + } + ); + + const animation = new WorkletAnimation('constant_time', effect); + animation.play(); + + let expected_transform = "matrix(1, 0, 0, 1, 0, 100)"; + await waitForAnimationFrameWithCondition(_ => { + return getComputedStyle(box).transform == expected_transform; + }); + + // The animation is specified to last for 1 millisecond + await new Promise(resolve => step_timeout(resolve, 500)); + + assert_equals(getComputedStyle(document.getElementById("box")).transform, expected_transform); + }, "WorkletAnimation should continue to be in effect forever, even if its duration is passed"); +</script> diff --git a/testing/web-platform/tests/animation-worklet/worklet-animation-get-computed-timing-progress-on-worklet-thread.https.html b/testing/web-platform/tests/animation-worklet/worklet-animation-get-computed-timing-progress-on-worklet-thread.https.html new file mode 100644 index 0000000000..a66a4b9156 --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/worklet-animation-get-computed-timing-progress-on-worklet-thread.https.html @@ -0,0 +1,87 @@ +<html> +<title>Animation Worklet should update calculated timing whenever localTime changes</title> +<link rel="help" href="https://drafts.css-houdini.org/css-animationworklet/"> + +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="common.js"></script> + +<div id="box"></div> + +<script id="get_computed_timing_animator" type="text/worklet"> + registerAnimator('get_computed_timing', class { + constructor(options, state) { + this.step = state ? state.step : 0; + } + state() { + return { + step: 0 + } + } + animate(currentTime, effect){ + if (this.step === 0){ + // check calculated timing values before ever setting effect.localTime + effect.localTime = (effect.getComputedTiming().currentIteration * 100) + (effect.getComputedTiming().progress * 100); + this.step = 1; + } + else if (this.step === 1){ + // set effect.localTime, this should be the first time calculated timing values are computed + effect.localTime = 420; // 20% of the way through the last iteration + + // using the calculated timing of effect, set effect.localTime. + effect.localTime = (effect.getComputedTiming().currentIteration * 100) + (effect.getComputedTiming().progress * 100); + this.step = 2; + } + else if (this.step === 2){ + // set effect.localTime to null + effect.localTime = null; + effect.localTime = (effect.getComputedTiming().currentIteration * 100) + (effect.getComputedTiming().progress * 100); + this.step = 3; + } + else if (this.step === 3){ + // Check to make sure we can go from null to a valid localTime and that calculated timing values are computed + effect.localTime = 350; // 50% of the way through second iteration + effect.localTime = (effect.getComputedTiming().currentIteration * 100) + (effect.getComputedTiming().progress * 100); + this.step = 4; + } + } + }); +</script> + +<script> + promise_test(async t => { + await runInAnimationWorklet(document.getElementById('get_computed_timing_animator').textContent); + + const box = document.getElementById("box"); + const effect = new KeyframeEffect( + box, + [ + { opacity: 0 }, + { opacity: 1 } + ], { + delay: 200, + duration: 100, + iterations: 3 + } + ); + + const animation = new WorkletAnimation('get_computed_timing', effect); + animation.play(); + + // check calculated timing values before ever setting effect.localTime + await waitForAnimationFrameWithCondition(() => {return approxEquals(effect.getComputedTiming().localTime, 0)}); + + // Check to make sure initial values can be set for computed timing + await waitForAnimationFrameWithCondition(() => {return approxEquals(effect.getComputedTiming().localTime, 220)}); + + // Make sure setting effect.localTime to null causes calculated timing values to be computed + await waitForAnimationFrameWithCondition(() => {return approxEquals(effect.getComputedTiming().localTime, 0)}); + + // Make sure we can go from null to a valid localTime and that calculated timing values are computed + await waitForAnimationFrameWithCondition(() => {return approxEquals(effect.getComputedTiming().localTime, 150)}); + + // Passes if it doesn't timeout + animation.cancel(); + }, "WorkletAnimation effect should recompute its calculated timing if its local time changes"); +</script>
\ No newline at end of file diff --git a/testing/web-platform/tests/animation-worklet/worklet-animation-get-timing-on-worklet-thread.https.html b/testing/web-platform/tests/animation-worklet/worklet-animation-get-timing-on-worklet-thread.https.html new file mode 100644 index 0000000000..4ba68d79e4 --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/worklet-animation-get-timing-on-worklet-thread.https.html @@ -0,0 +1,54 @@ +<html class="reftest-wait"> +<title>Animation Worklet should have access to effect timing from within the worklet thread</title> +<link rel="help" href="https://drafts.css-houdini.org/css-animationworklet/"> +<meta name="assert" content="Animation Worklet should have access to effect timing from within the worklet thread"> +<link rel="match" href="references/translated-box-ref.html"> + +<script src="/web-animations/testcommon.js"></script> +<script src="/common/reftest-wait.js"></script> +<script src="common.js"></script> + +<style> + #box{ + height: 100px; + width: 100px; + background-color: green; + } +</style> + +<div id="box"></div> + +<script id="get_timing_animator" type="text/worklet"> + registerAnimator('get_timing', class { + animate(currentTime, effect){ + effect.localTime = effect.getTiming().delay + (effect.getTiming().duration / 2); + } + }); +</script> + +<script> + runInAnimationWorklet( + document.getElementById('get_timing_animator').textContent + ).then(() => { + const box = document.getElementById("box"); + const effect = new KeyframeEffect( + box, + [ + {transform: 'translateY(0)'}, + {transform: 'translateY(200px)'} + ], + { + delay: 2000, + duration: 1000 + } + ); + + const animation = new WorkletAnimation('get_timing', effect); + animation.play(); + + waitForAsyncAnimationFrames(1).then(_ => { + takeScreenshot(); + }); + }); +</script> +</html>
\ No newline at end of file diff --git a/testing/web-platform/tests/animation-worklet/worklet-animation-local-time-after-duration.https.html b/testing/web-platform/tests/animation-worklet/worklet-animation-local-time-after-duration.https.html new file mode 100644 index 0000000000..21293bc09b --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/worklet-animation-local-time-after-duration.https.html @@ -0,0 +1,41 @@ +<html class="reftest-wait"> +<title>Animation Worklet local time set after duration</title> +<link rel="help" href="https://drafts.css-houdini.org/css-animationworklet/"> +<meta name="assert" content="If an effect doesn't have fill-mode specified, setting its local time beyond its duration makes the animation inactive."> +<link rel="match" href="references/not-translated-box-ref.html"> + +<script src="/web-animations/testcommon.js"></script> +<script src="/common/reftest-wait.js"></script> +<script src="common.js"></script> + +<style> + #box { + width: 100px; + height: 100px; + background-color: green; + } +</style> + +<div id="box"></div> + +<script> + registerConstantLocalTimeAnimator(2000).then(() => { + const box = document.getElementById('box'); + const effect = new KeyframeEffect(box, + [ + { transform: 'translateY(100px)' }, + { transform: 'translateY(200px)' } + ], { + duration: 1000, + delay: 1000 + } + ); + + const animation = new WorkletAnimation('constant_time', effect); + animation.play(); + + waitForAsyncAnimationFrames(1).then(_ => { + takeScreenshot(); + }); + }); +</script>
\ No newline at end of file diff --git a/testing/web-platform/tests/animation-worklet/worklet-animation-local-time-before-start.https.html b/testing/web-platform/tests/animation-worklet/worklet-animation-local-time-before-start.https.html new file mode 100644 index 0000000000..a959b73c08 --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/worklet-animation-local-time-before-start.https.html @@ -0,0 +1,41 @@ +<html class="reftest-wait"> +<title>Animation Worklet local time set before start</title> +<link rel="help" href="https://drafts.css-houdini.org/css-animationworklet/"> +<meta name="assert" content="The local time should be trimmed by the duration, e.g. this is equivalent to effect.localTime = 0"> +<link rel="match" href="references/translated-box-ref.html"> + +<script src="/web-animations/testcommon.js"></script> +<script src="/common/reftest-wait.js"></script> +<script src="common.js"></script> + +<style> + #box { + width: 100px; + height: 100px; + background-color: green; + } +</style> + +<div id="box"></div> + +<script> + registerConstantLocalTimeAnimator(-500).then(() => { + const box = document.getElementById('box'); + const effect = new KeyframeEffect(box, + [ + { transform: 'translateY(100px)' }, + { transform: 'translateY(0px)' } + ], { + duration: 1000, + fill: 'backwards' + } + ); + + const animation = new WorkletAnimation('constant_time', effect); + animation.play(); + + waitForAsyncAnimationFrames(1).then(_ => { + takeScreenshot(); + }); + }); +</script>
\ No newline at end of file diff --git a/testing/web-platform/tests/animation-worklet/worklet-animation-local-time-null-1.https.html b/testing/web-platform/tests/animation-worklet/worklet-animation-local-time-null-1.https.html new file mode 100644 index 0000000000..52727fa6ea --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/worklet-animation-local-time-null-1.https.html @@ -0,0 +1,163 @@ +<!DOCTYPE html> +<title>Setting localTime to null means effect does not apply</title> +<link rel="help" href="https://drafts.css-houdini.org/css-animationworklet/"> + +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="common.js"></script> + +<style> +.box { + width: 100px; + height: 100px; + background-color: green; + display: inline-block; +} +</style> + +<div> +<div class="box" id="target1"></div> +<div class="box" id="target2"></div> +<div class="box" id="target3"></div> +<div class="box" id="target4"></div> +</div> + + +<script> +promise_test(async t => { + await runInAnimationWorklet(` + registerAnimator("blank_animator", class { + animate(currentTime, effect) { + // Unset effect.localTime is equivalent to 'null' + } + }); + `); + const target = document.getElementById('target1'); + + const animation = new WorkletAnimation('blank_animator', + new KeyframeEffect(target, + [ + { transform: 'translateY(100px)' }, + { transform: 'translateY(200px)' } + ], { + duration: 1000, + } + ) + ); + animation.play(); + await waitForAsyncAnimationFrames(1); + assert_equals(getComputedStyle(target).transform, "none"); +}, "A worklet which never sets localTime has no effect."); + +promise_test(async t => { + await runInAnimationWorklet(` + registerAnimator("null_animator", class { + animate(currentTime, effect) { + effect.localTime = null; + } + }); + `); + const target = document.getElementById('target2'); + + const animation = new WorkletAnimation('null_animator', + new KeyframeEffect(target, + [ + { transform: 'translateY(100px)' }, + { transform: 'translateY(200px)' } + ], { + duration: 1000, + } + ) + ); + animation.play(); + await waitForAsyncAnimationFrames(1); + assert_equals(getComputedStyle(target).transform, "none"); +}, "A worklet which sets localTime to null has no effect."); + +promise_test(async t => { + await runInAnimationWorklet(` + registerAnimator("drop_animator", class { + animate(currentTime, effect) { + if (currentTime < 500) + effect.localTime = 500; + else if (currentTime < 1000) + effect.localTime = 0; + else + effect.localTime = null; + } + }); + `); + const target = document.getElementById('target3'); + + const animation = new WorkletAnimation('drop_animator', + new KeyframeEffect(target, + [ + { transform: 'translateY(100px)' }, + { transform: 'translateY(200px)' } + ], { + duration: 1000, + } + ) + ); + animation.play(); + await waitForAsyncAnimationFrames(5); + assert_equals(getComputedStyle(target).transform, "matrix(1, 0, 0, 1, 0, 150)", + "The animation has an effect at first"); + + await waitForAnimationFrameWithCondition(() => animation.currentTime > 500); + await waitForAsyncAnimationFrames(1); + assert_equals(getComputedStyle(target).transform, "matrix(1, 0, 0, 1, 0, 100)", + "The effect correctly changes"); + + await waitForAnimationFrameWithCondition(() => animation.currentTime > 1000); + await waitForAsyncAnimationFrames(1); + assert_equals(getComputedStyle(target).transform, "none", + "The effect stops on nulling of localTime"); + +}, "A worklet which changes localTime to from a number to null has no effect on transform."); + +promise_test(async t => { + await runInAnimationWorklet(` + registerAnimator("drop2_animator", class { + animate(currentTime, effect) { + if (currentTime < 500) + effect.localTime = 500; + else if (currentTime < 1000) + effect.localTime = 0; + else + effect.localTime = null; + } + }); + `); + const target = document.getElementById('target4'); + + const animation = new WorkletAnimation('drop2_animator', + new KeyframeEffect(target, + [ + { backgroundColor: 'red' }, + { backgroundColor: 'blue' } + ], { + duration: 1000, + } + ) + ); + animation.play(); + await waitForAsyncAnimationFrames(5); + assert_equals(getComputedStyle(target).backgroundColor, "rgb(128, 0, 128)", + "The animation has an effect at first"); + + await waitForAnimationFrameWithCondition(() => animation.currentTime > 500); + await waitForAsyncAnimationFrames(1); + assert_equals(getComputedStyle(target).backgroundColor, "rgb(255, 0, 0)", + "The effect correctly changes"); + + await waitForAnimationFrameWithCondition(() => animation.currentTime > 1000); + await waitForAsyncAnimationFrames(1); + assert_equals(getComputedStyle(target).backgroundColor, "rgb(0, 128, 0)", + "The effect stops on nulling of localTime"); + +}, "A worklet which changes localTime to from a number to null has no effect on backgroundColor."); + + +</script> diff --git a/testing/web-platform/tests/animation-worklet/worklet-animation-local-time-null-2-ref.html b/testing/web-platform/tests/animation-worklet/worklet-animation-local-time-null-2-ref.html new file mode 100644 index 0000000000..3b7a2b9258 --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/worklet-animation-local-time-null-2-ref.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<title>Setting localTime to null means effect does not apply (reftest)</title> +<link rel="help" href="https://drafts.css-houdini.org/css-animationworklet/"> + +<style> +.box { + width: 100px; + height: 100px; + background-color: green; + display: inline-block; +} + +#control { + background-color: red; + transform: translateY(100px); +} +</style> + +<div> +<div class="box" id="target1"></div> +<div class="box" id="target2"></div> +<div class="box" id="target3"></div> +<div class="box" id="target4"></div> +<div class="box" id="control"></div> +</div> + +</script> diff --git a/testing/web-platform/tests/animation-worklet/worklet-animation-local-time-null-2.https.html b/testing/web-platform/tests/animation-worklet/worklet-animation-local-time-null-2.https.html new file mode 100644 index 0000000000..9c499bac0e --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/worklet-animation-local-time-null-2.https.html @@ -0,0 +1,110 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<title>Setting localTime to null means effect does not apply (reftest)</title> +<link rel="help" href="https://drafts.css-houdini.org/css-animationworklet/"> +<link rel="match" href="worklet-animation-local-time-null-2-ref.html"> +<meta name="timeout" content="long"> + +<script src="/common/reftest-wait.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="common.js"></script> + +<style> +.box { + width: 100px; + height: 100px; + background-color: green; + display: inline-block; +} +</style> + +<div> +<div class="box" id="target1"></div> +<div class="box" id="target2"></div> +<div class="box" id="target3"></div> +<div class="box" id="target4"></div> +<div class="box" id="control"></div> +</div> + + +<script> +runInAnimationWorklet(` + registerAnimator("blank_animator", class { + animate(currentTime, effect) { + // Unset effect.localTime is equivalent to 'null' + } + }); + + registerAnimator("null_animator", class { + animate(currentTime, effect) { + effect.localTime = null; + } + }); + + registerAnimator("drop_animator", class { + animate(currentTime, effect) { + if (currentTime < 500) + effect.localTime = 500; + else if (currentTime < 1000) + effect.localTime = 0; + else + effect.localTime = null; + } + }); + + registerAnimator("add_animator", class { + animate(currentTime, effect) { + if (currentTime < 1000) + effect.localTime = 500; + else + effect.localTime = 0; + } + }); +`).then(() => { + + const start_animation = (animator, targetId, keyframes) => { + const animation = new WorkletAnimation(animator, + new KeyframeEffect( + document.getElementById(targetId), + keyframes, + {duration: 1000} + ) + ); + animation.play(); + return animation; + }; + + start_animation('blank_animator','target1', [ + { transform: 'translateY(100px)' }, + { transform: 'translateY(200px)' } + ]); + + start_animation('null_animator','target2', [ + { transform: 'translateY(100px)' }, + { transform: 'translateY(200px)' } + ]); + + start_animation('drop_animator','target3', [ + { transform: 'translateY(100px)' }, + { transform: 'translateY(200px)' } + ]); + + start_animation('drop_animator','target4', [ + { backgroundColor: 'red' }, + { backgroundColor: 'blue' } + ]); + + // check that animation worklets are running to stop accidental pass + const control_anim = start_animation('add_animator','control', [ + { backgroundColor: 'red', transform: 'translateY(100px)' }, + { backgroundColor: 'blue', transform: 'translateY(200px)' } + ]); + + waitForAnimationFrameWithCondition(() => control_anim.currentTime > 1000) + // long timeout due to laggy compositor thread on debug build. + .then(() => waitForAsyncAnimationFrames(120)) + .then(takeScreenshot); +}); + + +</script> diff --git a/testing/web-platform/tests/animation-worklet/worklet-animation-pause-immediately.https.html b/testing/web-platform/tests/animation-worklet/worklet-animation-pause-immediately.https.html new file mode 100644 index 0000000000..f9dcf30bc9 --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/worklet-animation-pause-immediately.https.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<title>Verify that calling pause immediately after playing works as expected</title> +<link rel="help" href="https://drafts.css-houdini.org/css-animationworklet/"> +<link rel="match" href="references/translated-box-ref.html"> + +<script src="/common/reftest-wait.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="common.js"></script> +<style> +#box { + width: 100px; + height: 100px; + background-color: green; +} +</style> + +<div id="box"></div> + +<script> +registerPassthroughAnimator().then(async _ => { + const box = document.getElementById('box'); + const effect = new KeyframeEffect(box, + { transform: ['translateY(100px)', 'translateY(200px)'] }, + { duration: 100, iterations: 1 } + ); + + const animation = new WorkletAnimation('passthrough', effect); + animation.play(); + // Immediately pausing animation should freeze the current time at 0. + animation.pause(); + // Wait at least one frame to ensure a paused animation actually freezes. + await waitForAsyncAnimationFrames(1); + takeScreenshot(); +}); +</script> +</html> diff --git a/testing/web-platform/tests/animation-worklet/worklet-animation-pause-resume.https.html b/testing/web-platform/tests/animation-worklet/worklet-animation-pause-resume.https.html new file mode 100644 index 0000000000..f26a93468c --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/worklet-animation-pause-resume.https.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<title>Verify that calling pause immediately after playing works as expected</title> +<link rel="help" href="https://drafts.css-houdini.org/css-animationworklet/"> +<link rel="match" href="references/translated-box-ref.html"> + +<script src="/common/reftest-wait.js"></script> +<script src="common.js"></script> +<style> +#box { + width: 100px; + height: 100px; + background-color: green; +} +</style> + +<div id="box"></div> + +<script> +registerPassthroughAnimator().then(async _ => { + const duration = 18; // a bit longer than a frame + const box = document.getElementById('box'); + const effect = new KeyframeEffect(box, + { transform: ['translateY(0px)', 'translateY(100px)'] }, + { duration: duration, iterations: 1, fill: 'forwards'} + ); + + const animation = new WorkletAnimation('passthrough', effect); + // Immediately pausing animation should freeze the current time at 0. + animation.pause(); + // Playing should cause animation to resume. + animation.play(); + // Wait until we ensure animation has reached completion. + await waitForAnimationFrameWithCondition( _ => { + return animation.currentTime >= duration; + }); + takeScreenshot(); +}); +</script> +</html> diff --git a/testing/web-platform/tests/animation-worklet/worklet-animation-pause.https.html b/testing/web-platform/tests/animation-worklet/worklet-animation-pause.https.html new file mode 100644 index 0000000000..417db9e37a --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/worklet-animation-pause.https.html @@ -0,0 +1,60 @@ +<!DOCTYPE html> +<title>Verify that currentTime and playState are correct when animation is paused</title> +<link rel="help" href="https://drafts.css-houdini.org/css-animationworklet/"> + +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="common.js"></script> + +<div id="box"></div> + +<script> + +setup(setupAndRegisterTests, {explicit_done: true}); + +function createAnimation() { + const box = document.getElementById('box'); + const effect = new KeyframeEffect(box, + { transform: ['translateY(100px)', 'translateY(200px)'] }, + { duration: 100, iterations: 1 } + ); + + return new WorkletAnimation('passthrough', effect); +} + +async function setupAndRegisterTests() { + await registerPassthroughAnimator(); + + promise_test(async t => { + const animation = createAnimation(); + animation.play(); + // Immediately pausing animation should freeze the current time at 0. + animation.pause(); + assert_equals(animation.currentTime, 0); + assert_equals(animation.playState, "paused"); + // Wait some time to ensure a paused animation actually freezes. + await waitForNextFrame(); + assert_equals(animation.currentTime, 0); + assert_equals(animation.playState, "paused"); + }, 'pausing an animation freezes its current time'); + + promise_test(async t => { + const animation = createAnimation(); + animation.pause(); + animation.play(); + // Allow one async animation frame to pass so that animation is running. + await waitForAsyncAnimationFrames(1); + assert_equals(animation.playState, "running"); + // Allow time to advance so that we have a non-zero current time. + await waitForDocumentTimelineAdvance(); + const timelineTime = document.timeline.currentTime; + assert_greater_than(animation.currentTime, 0); + assert_times_equal(animation.currentTime, (timelineTime - animation.startTime)); + }, 'playing a paused animation should resume it'); + + done(); +} + +</script> + diff --git a/testing/web-platform/tests/animation-worklet/worklet-animation-play.https.html b/testing/web-platform/tests/animation-worklet/worklet-animation-play.https.html new file mode 100644 index 0000000000..038cd74aab --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/worklet-animation-play.https.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<title>Basic use of Worklet Animation</title> +<link rel="help" href="https://drafts.css-houdini.org/css-animationworklet/"> + +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="common.js"></script> + +<div id="target"></div> + +<script> + promise_test(async t => { + await registerConstantLocalTimeAnimator(500); + const effect = new KeyframeEffect(target, [{ opacity: 0 }], { duration: 1000 }); + const animation = new WorkletAnimation('constant_time', effect); + animation.play(); + + // wait until local times are synced back to the main thread. + await waitForAnimationFrameWithCondition(_ => { + return getComputedStyle(target).opacity != '1'; + }); + assert_equals(getComputedStyle(target).opacity, "0.5"); + + animation.cancel(); + }, "A running worklet animation should output values at specified local time."); + + promise_test(async t => { + await registerConstantLocalTimeAnimator(500); + const effect = new KeyframeEffect(target, [{ opacity: 0 }], { duration: 1000 }); + const animation = new WorkletAnimation('constant_time', effect); + animation.play(); + + await waitForAnimationFrameWithCondition(_=> { + return animation.playState == "running" + }); + + const prevCurrentTime = animation.currentTime; + animation.play(); + assert_equals(animation.playState, "running"); + assert_equals(animation.currentTime, prevCurrentTime) + + animation.cancel(); + }, "Playing a running animation should be a no-op."); +</script> diff --git a/testing/web-platform/tests/animation-worklet/worklet-animation-set-keyframes.https.html b/testing/web-platform/tests/animation-worklet/worklet-animation-set-keyframes.https.html new file mode 100644 index 0000000000..d3d02898db --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/worklet-animation-set-keyframes.https.html @@ -0,0 +1,44 @@ +<html class="reftest-wait"> +<title>Worklet Animation sets keyframes</title> +<link rel="help" href="https://drafts.css-houdini.org/css-animationworklet/"> +<meta name="assert" content="Can update the keyframes for an effect while the animation is running"> +<link rel="match" href="references/translated-box-ref.html"> + +<script src="/web-animations/testcommon.js"></script> +<script src="/common/reftest-wait.js"></script> +<script src="common.js"></script> + +<style> + #box { + width: 100px; + height: 100px; + background-color: green; + } +</style> + +<div id="box"></div> + +<script> + registerConstantLocalTimeAnimator(500).then(()=>{ + const keyframes_before = [ + { transform: 'translateX(0)' }, + { transform: 'translateX(200px)' } + ]; + const keyframes_after = [ + { transform: 'translateY(0)' }, + { transform: 'translateY(200px)' } + ]; + + const box = document.getElementById('box'); + const effect = new KeyframeEffect(box, keyframes_before, {duration: 1000}); + const animation = new WorkletAnimation('constant_time', effect); + animation.play(); + + waitForAsyncAnimationFrames(1).then(_ => { + effect.setKeyframes(keyframes_after); + waitForAsyncAnimationFrames(1).then(_ => { + takeScreenshot(); + }); + }); + }); +</script>
\ No newline at end of file diff --git a/testing/web-platform/tests/animation-worklet/worklet-animation-set-timing.https.html b/testing/web-platform/tests/animation-worklet/worklet-animation-set-timing.https.html new file mode 100644 index 0000000000..6c5cd51300 --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/worklet-animation-set-timing.https.html @@ -0,0 +1,46 @@ +<html class="reftest-wait"> +<title>Worklet Animation sets timing</title> +<link rel="help" href="https://drafts.css-houdini.org/css-animationworklet/"> +<meta name="assert" content="Can update the timing for an effect while the animation is running"> +<link rel="match" href="references/translated-box-ref.html"> + +<script src="/web-animations/testcommon.js"></script> +<script src="/common/reftest-wait.js"></script> +<script src="common.js"></script> + +<style> + #box { + width: 100px; + height: 100px; + background-color: green; + } +</style> + +<div id="box"></div> + +<script> + registerConstantLocalTimeAnimator(500).then(()=>{ + const keyframes = [ + { transform: 'translateY(0)' }, + { transform: 'translateY(400px)' } + ]; + const options_before = { + duration: 1000 + }; + const options_after = { + duration: 2000 + }; + + const box = document.getElementById('box'); + const effect = new KeyframeEffect(box, keyframes, options_before); + const animation = new WorkletAnimation('constant_time', effect); + animation.play(); + + waitForAsyncAnimationFrames(1).then(_ => { + effect.updateTiming(options_after); + waitForAsyncAnimationFrames(1).then(_ => { + takeScreenshot(); + }); + }); + }); +</script>
\ No newline at end of file diff --git a/testing/web-platform/tests/animation-worklet/worklet-animation-start-delay-ref.html b/testing/web-platform/tests/animation-worklet/worklet-animation-start-delay-ref.html new file mode 100644 index 0000000000..efef6f842b --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/worklet-animation-start-delay-ref.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<title>Reference for WorkletAnimation should respect delay given in options</title> +<style> +.box { + width: 100px; + height: 100px; + background-color: green; +} +</style> + +<div class="box"></div> +<div style="transform: translateX(100px);" class="box"></div> diff --git a/testing/web-platform/tests/animation-worklet/worklet-animation-start-delay.https.html b/testing/web-platform/tests/animation-worklet/worklet-animation-start-delay.https.html new file mode 100644 index 0000000000..c8683f7dac --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/worklet-animation-start-delay.https.html @@ -0,0 +1,64 @@ +<html class="reftest-wait"> +<title>WorkletAnimation should respect delay given in options</title> +<link rel="help" href="https://drafts.css-houdini.org/css-animationworklet/"> +<meta name="assert" content="Worklet Animation should respect delay given in options"> +<link rel="match" href="worklet-animation-start-delay-ref.html"> + +<script src="/web-animations/testcommon.js"></script> +<script src="/common/reftest-wait.js"></script> +<script src="common.js"></script> + +<style> + .box { + width: 100px; + height: 100px; + background-color: green; + } +</style> + +<div id="t0" class="box"></div> +<div id="t1" class="box"></div> +<div id="out"></div> +<script id="visual_update" type="text/worklet"> + registerAnimator("t0_animator", class { + animate(currentTime, effect) { + effect.localTime = 500; + } + }); + + registerAnimator("t1_animator", class { + animate(currentTime, effect) { + effect.localTime = 5500; + } + }); +</script> + +<script> + runInAnimationWorklet( + document.getElementById('visual_update').textContent + ).then(()=>{ + const keyframes = [ + {transform: 'translateX(0)' }, + {transform: 'translateX(200px)' } + ]; + const options = { + duration: 1000, + delay: 5000, + }; + + const $t0 = document.getElementById('t0'); + const $t0_effect = new KeyframeEffect($t0, keyframes, options); + const $t0_animation = new WorkletAnimation('t0_animator', $t0_effect); + + const $t1 = document.getElementById('t1'); + const $t1_effect = new KeyframeEffect($t1, keyframes, options); + const $t1_animation = new WorkletAnimation('t1_animator', $t1_effect); + + $t0_animation.play(); + $t1_animation.play(); + + waitForAsyncAnimationFrames(1).then(_ => { + takeScreenshot(); + }); + }); +</script> diff --git a/testing/web-platform/tests/animation-worklet/worklet-animation-with-effects-from-different-frames.https.html b/testing/web-platform/tests/animation-worklet/worklet-animation-with-effects-from-different-frames.https.html new file mode 100644 index 0000000000..152b13839c --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/worklet-animation-with-effects-from-different-frames.https.html @@ -0,0 +1,48 @@ +<!DOCTYPE html> +<title>Worklet animation can animate effects from different frames</title> +<link rel="help" href="https://drafts.css-houdini.org/css-animationworklet/"> + +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="common.js"></script> + +<div id="box"></div> + +<script id="simple_animate" type="text/worklet"> + registerAnimator("test_animator", class { + animate(currentTime, effect) { + let effects = effect.getChildren(); + effects[0].localTime = 500; + effects[1].localTime = 750; + } + }); +</script> + +<script> + promise_test(async t => { + await runInAnimationWorklet(document.getElementById('simple_animate').textContent); + const effect = new KeyframeEffect(box, [{ opacity: 0 }], { duration: 1000 }); + + let iframe = document.createElement('iframe'); + iframe.src = 'resources/iframe.html'; + document.body.appendChild(iframe); + + await waitForAnimationFrameWithCondition(_ => { + return iframe.contentDocument.getElementById('iframe_box') != null; + }); + let iframe_box = iframe.contentDocument.getElementById('iframe_box'); + let iframe_effect = new KeyframeEffect( + iframe_box, [{ opacity: 0 }], { duration: 1000 } + ); + + const animation = new WorkletAnimation('test_animator', [effect, iframe_effect]); + animation.play(); + + await waitForNotNullLocalTime(animation); + assert_equals(getComputedStyle(box).opacity, '0.5'); + assert_equals(getComputedStyle(iframe_box).opacity, '0.25'); + + iframe.remove(); + animation.cancel(); + }, "Effects from different documents can be animated within one worklet animation"); +</script> diff --git a/testing/web-platform/tests/animation-worklet/worklet-animation-with-fill-mode.https.html b/testing/web-platform/tests/animation-worklet/worklet-animation-with-fill-mode.https.html new file mode 100644 index 0000000000..725d10de43 --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/worklet-animation-with-fill-mode.https.html @@ -0,0 +1,147 @@ +<!DOCTYPE html> +<title>Test that worklet animation works with different fill modes</title> +<link rel="help" href="https://drafts.css-houdini.org/css-animationworklet/"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="common.js"></script> + +<style> +.target { + width: 100px; + height: 100px; + background-color: green; +} +</style> + +<div id="target" class='target'></div> + +<script> +setup(setupAndRegisterTests, {explicit_done: true}); + +function setupAndRegisterTests() { + registerConstantLocalTimeAnimator(2000).then(() => { + promise_test( + effect_with_fill_mode_forwards, + "Effect with fill mode forwards in after phase produces output that is equivalent to effect's end value."); + + promise_test( + effect_without_fill_mode_forwards, + 'Effect without fill mode forwards in after phase (local time beyond end) should deactivate the animation.'); + + promise_test( + effect_without_fill_forwards_at_end, + 'Effect without fill mode in after phase (local time at end) should deactivate the animation.'); + + promise_test( + effect_with_fill_backwards, + "Effect with fill mode backwards in before phase produces output that is equivalent to effect's start value."); + + promise_test( + effect_without_fill_backwards, + 'Effect without fill mode backwards in before phase (local time before start) should deactivate the animation.'); + + promise_test( + effect_without_fill_backwards_at_start, + 'Effect with local time at start point is in active phase.'); + + done(); + }); +} + +async function effect_with_fill_mode_forwards(t) { + const effect_with_fill_forwards = new KeyframeEffect( + target, + { opacity: [0.5, 0] }, + { duration: 1000, fill: 'forwards' }); + const animation = new WorkletAnimation( + 'constant_time', + effect_with_fill_forwards); + animation.play(); + await waitForNotNullLocalTime(animation); + + assert_equals(getComputedStyle(target).opacity, '0'); + + animation.cancel(); +} + +async function effect_without_fill_mode_forwards(t) { + const effect_without_fill_forwards = new KeyframeEffect( + target, + { opacity: [0.5, 0] }, + { duration: 1000 }); + const animation = new WorkletAnimation( + 'constant_time', + effect_without_fill_forwards); + animation.play(); + await waitForNotNullLocalTime(animation); + + assert_equals(getComputedStyle(target).opacity, '1'); + + animation.cancel(); +} + +async function effect_without_fill_forwards_at_end(t) { + const effect_without_fill_forwards_at_end = new KeyframeEffect( + target, + { opacity: [0.5, 0] }, + { duration: 2000 }); + const animation = new WorkletAnimation( + 'constant_time', + effect_without_fill_forwards_at_end); + animation.play(); + await waitForNotNullLocalTime(animation); + + assert_equals(getComputedStyle(target).opacity, '1'); + + animation.cancel(); +} + +async function effect_with_fill_backwards(t) { + const effect_with_fill_backwards = new KeyframeEffect( + target, + { opacity: [0.5, 0] }, + { duration: 1000, delay: 2001, fill: 'backwards' }); + const animation = new WorkletAnimation( + 'constant_time', + effect_with_fill_backwards); + animation.play(); + await waitForNotNullLocalTime(animation); + + assert_equals(getComputedStyle(target).opacity, '0.5'); + + animation.cancel(); +} + +async function effect_without_fill_backwards(t) { + const effect_without_fill_backwards = new KeyframeEffect( + target, + { opacity: [0.5, 0] }, + { duration: 1000, delay: 2001 }); + const animation = new WorkletAnimation( + 'constant_time', + effect_without_fill_backwards); + animation.play(); + waitForNotNullLocalTime(animation); + + assert_equals(getComputedStyle(target).opacity, '1'); + + animation.cancel(); +} + +async function effect_without_fill_backwards_at_start(t) { + const effect_without_fill_backwards_at_start = new KeyframeEffect( + target, + { opacity: [0.5, 0] }, + { duration: 1000, delay: 2000 }); + const animation = new WorkletAnimation( + 'constant_time', + effect_without_fill_backwards_at_start); + animation.play(); + await waitForNotNullLocalTime(animation); + + assert_equals(getComputedStyle(target).opacity, '0.5'); + + animation.cancel(); +} +</script> diff --git a/testing/web-platform/tests/animation-worklet/worklet-animation-with-invalid-effect.https.html b/testing/web-platform/tests/animation-worklet/worklet-animation-with-invalid-effect.https.html new file mode 100644 index 0000000000..75261d251c --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/worklet-animation-with-invalid-effect.https.html @@ -0,0 +1,36 @@ +<!DOCTYPE html> +<title>Test that worklet animation with invalid effect cannot be played</title> +<link rel="help" href="https://drafts.css-houdini.org/css-animationworklet/"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="common.js"></script> + +<style> +#target { + width: 100px; + height: 100px; +} +</style> + +<div id="target"></div> + +<script> +'use strict'; + +promise_test(async function() { + await registerPassthroughAnimator(); + let playFunc = function() { + let effect = new KeyframeEffect( + document.getElementById('target'), + [ + // No keyframe. + ], + { duration: 1000 } + ); + let animation = new WorkletAnimation('passthrough', effect); + animation.play(); + } + assert_throws_dom('InvalidStateError', playFunc); +}, 'Trying to play invalid worklet animation should throw an exception.'); +</script> diff --git a/testing/web-platform/tests/animation-worklet/worklet-animation-with-non-ascii-name-ref.html b/testing/web-platform/tests/animation-worklet/worklet-animation-with-non-ascii-name-ref.html new file mode 100644 index 0000000000..012f6f9d51 --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/worklet-animation-with-non-ascii-name-ref.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<title>Reference for WorkletAnimation name should accept non-ASCII characters</title> +<style> +.box { + width: 100px; + height: 100px; + background-color: green; +} +</style> + +<div style="transform: translateX(50px);" class="box"></div> +<div style="transform: translateX(150px);" class="box"></div> diff --git a/testing/web-platform/tests/animation-worklet/worklet-animation-with-non-ascii-name.https.html b/testing/web-platform/tests/animation-worklet/worklet-animation-with-non-ascii-name.https.html new file mode 100644 index 0000000000..d3a3f4ad35 --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/worklet-animation-with-non-ascii-name.https.html @@ -0,0 +1,59 @@ +<html class="reftest-wait"> +<title>WorkletAnimation name should accept non-ASCII characters</title> +<link rel="help" href="https://drafts.css-houdini.org/css-animationworklet/"> +<meta name="assert" content="Worklet Animation name should accept non-ASCII characters"> +<link rel="match" href="worklet-animation-with-non-ascii-name-ref.html"> + +<script src="/web-animations/testcommon.js"></script> +<script src="/common/reftest-wait.js"></script> +<script src="common.js"></script> + +<style> + .box { + width: 100px; + height: 100px; + background-color: green; + } +</style> + +<div id="t0" class="box"></div> +<div id="t1" class="box"></div> +<script id="visual_update" type="text/worklet"> + registerAnimator('bob', class { + animate(currentTime, effect) { + effect.localTime = 250; + } + }); + registerAnimator('東京', class { + animate(currentTime, effect) { + effect.localTime = 750; + } + }); +</script> +<script> + runInAnimationWorklet( + document.getElementById('visual_update').textContent + ).then(() => { + const keyframes = [ + {transform: 'translateX(0)' }, + {transform: 'translateX(200px)' } + ]; + const options = { + duration: 1000 + }; + const $t0 = document.getElementById('t0'); + const $t0_effect = new KeyframeEffect($t0, keyframes, options); + const $t0_animation = new WorkletAnimation('bob', $t0_effect); + + const $t1 = document.getElementById('t1'); + const $t1_effect = new KeyframeEffect($t1, keyframes, options); + const $t1_animation = new WorkletAnimation('東京', $t1_effect); + + $t0_animation.play(); + $t1_animation.play(); + + waitForAsyncAnimationFrames(1).then(_ => { + takeScreenshot(); + }); + }); +</script> diff --git a/testing/web-platform/tests/animation-worklet/worklet-animation-with-scroll-timeline-and-display-none.https.html b/testing/web-platform/tests/animation-worklet/worklet-animation-with-scroll-timeline-and-display-none.https.html new file mode 100644 index 0000000000..0bba0039da --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/worklet-animation-with-scroll-timeline-and-display-none.https.html @@ -0,0 +1,84 @@ +<html class="reftest-wait"> +<title>Scroll timeline with WorkletAnimation and transition from display:none to display:block</title> +<link rel="help" href="https://drafts.css-houdini.org/css-animationworklet/"> +<meta name="assert" content="Scroll timeline should properly handle going from display:none to display:block"> +<link rel="match" href="worklet-animation-with-scroll-timeline-ref.html"> + +<script src="/web-animations/testcommon.js"></script> +<script src="/common/reftest-wait.js"></script> +<script src="common.js"></script> + +<style> + #box { + width: 100px; + height: 100px; + background-color: green; + } + + #covered { + width: 100px; + height: 100px; + background-color: red; + } + + /* Hide scrollbars to avoid unnecessary visual issues related to them */ + #scroller::-webkit-scrollbar { + display: none; + } + + #scroller { + scrollbar-width: none; + overflow: auto; + height: 100px; + width: 100px; + will-change: transform; /* force compositing */ + } + + .removed { + display: none; + } + + #contents { + height: 1000px; + width: 100%; + } +</style> + +<div id="box"></div> +<div id="covered"></div> +<div id="scroller"> + <div id="contents"></div> +</div> + +<script> + registerPassthroughAnimator().then(()=>{ + const box = document.getElementById('box'); + const effect = new KeyframeEffect(box, + [ + { transform: 'translateY(0)', opacity: 1 }, + { transform: 'translateY(200px)', opacity: 0 } + ], { + duration: 1000, + } + ); + + const scroller = document.getElementById('scroller'); + scroller.classList.add('removed'); + const timeline = new ScrollTimeline({ scrollSource: scroller, orientation: 'block' }); + const animation = new WorkletAnimation('passthrough', effect, timeline); + animation.play(); + + // Ensure that the WorkletAnimation will have been started on the compositor. + waitForAsyncAnimationFrames(1).then(_ => { + // Now return the scroller to the world, which will cause it to be composited + // and the animation should update on the compositor side. + scroller.classList.remove('removed'); + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.5 * maxScroll; + + waitForAsyncAnimationFrames(1).then(_ => { + takeScreenshot(); + }); + }); + }); +</script>
\ No newline at end of file diff --git a/testing/web-platform/tests/animation-worklet/worklet-animation-with-scroll-timeline-and-overflow-hidden-ref.html b/testing/web-platform/tests/animation-worklet/worklet-animation-with-scroll-timeline-and-overflow-hidden-ref.html new file mode 100644 index 0000000000..c6d7314e39 --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/worklet-animation-with-scroll-timeline-and-overflow-hidden-ref.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<title>Scroll timeline with WorkletAnimation using a scroller with overflow hidden</title> +<style> + #box { + width: 100px; + height: 100px; + background-color: green; + transform: translate(0, 100px); + opacity: 0.5; + will-change: transform; /* force compositing */ + } + + #covered { + width: 100px; + height: 100px; + background-color: red; + } + + #scroller { + overflow: hidden; + height: 100px; + width: 100px; + will-change: transform; /* force compositing */ + } + + #contents { + height: 1000px; + width: 100%; + } +</style> + +<div id="box"></div> +<div id="covered"></div> +<div id="scroller"> + <div id="contents"></div> +</div> + +<script> + window.addEventListener('load', function() { + // Move the scroller to halfway. + const scroller = document.getElementById("scroller"); + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.5 * maxScroll; + }); +</script>
\ No newline at end of file diff --git a/testing/web-platform/tests/animation-worklet/worklet-animation-with-scroll-timeline-and-overflow-hidden.https.html b/testing/web-platform/tests/animation-worklet/worklet-animation-with-scroll-timeline-and-overflow-hidden.https.html new file mode 100644 index 0000000000..c2332bd6ce --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/worklet-animation-with-scroll-timeline-and-overflow-hidden.https.html @@ -0,0 +1,68 @@ +<html class="reftest-wait"> +<title>Scroll timeline with WorkletAnimation using a scroller with overflow hidden</title> +<link rel="help" href="https://drafts.css-houdini.org/css-animationworklet/"> +<meta name="assert" content="Worklet animation correctly updates values when using a overflow: hidden on the scroller being used as the source for the ScrollTimeline"> +<link rel="match" href="worklet-animation-with-scroll-timeline-and-overflow-hidden-ref.html"> + +<script src="/web-animations/testcommon.js"></script> +<script src="/common/reftest-wait.js"></script> +<script src="common.js"></script> + +<style> + #box { + width: 100px; + height: 100px; + background-color: green; + } + + #covered { + width: 100px; + height: 100px; + background-color: red; + } + + #scroller { + overflow: hidden; + height: 100px; + width: 100px; + } + + #contents { + height: 1000px; + width: 100%; + } +</style> + +<div id="box"></div> +<div id="covered"></div> +<div id="scroller"> + <div id="contents"></div> +</div> + +<script> + registerPassthroughAnimator().then(_ => { + const box = document.getElementById('box'); + const effect = new KeyframeEffect(box, + [ + {transform: 'translateY(0)', opacity: 1}, + {transform: 'translateY(200px)', opacity: 0} + ], { + duration: 1000, + } + ); + + const scroller = document.getElementById('scroller'); + const timeline = new ScrollTimeline({ scrollSource: scroller, orientation: 'block' }); + const animation = new WorkletAnimation('passthrough', effect, timeline); + animation.play(); + + // Move the scroller to the halfway point. + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.5 * maxScroll; + waitForAnimationFrameWithCondition(_ => { + return getComputedStyle(box).transform != 'matrix(1, 0, 0, 1, 0, 0)'; + }).then(_ => { + takeScreenshot(); + }); + }); +</script>
\ No newline at end of file diff --git a/testing/web-platform/tests/animation-worklet/worklet-animation-with-scroll-timeline-ref.html b/testing/web-platform/tests/animation-worklet/worklet-animation-with-scroll-timeline-ref.html new file mode 100644 index 0000000000..1316d69a42 --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/worklet-animation-with-scroll-timeline-ref.html @@ -0,0 +1,51 @@ +<!DOCTYPE html> +<title>Reference for Animation Worklet with scroll timeline tests</title> +<style> + #box { + width: 100px; + height: 100px; + background-color: green; + transform: translate(0, 100px); + opacity: 0.5; + will-change: transform; /* force compositing */ + } + + #covered { + width: 100px; + height: 100px; + background-color: red; + } + + /* Hide scrollbars to avoid unnecessary visual issues related to them */ + #scroller::-webkit-scrollbar { + display: none; + } + + #scroller { + scrollbar-width: none; + overflow: auto; + height: 100px; + width: 100px; + will-change: transform; /* force compositing */ + } + + #contents { + height: 1000px; + width: 100%; + } +</style> + +<div id="box"></div> +<div id="covered"></div> +<div id="scroller"> + <div id="contents"></div> +</div> + +<script> + window.addEventListener('load', function() { + // Move the scroller to halfway. + const scroller = document.getElementById("scroller"); + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.5 * maxScroll; + }); +</script>
\ No newline at end of file diff --git a/testing/web-platform/tests/animation-worklet/worklet-animation-with-scroll-timeline-root-scroller-ref.html b/testing/web-platform/tests/animation-worklet/worklet-animation-with-scroll-timeline-root-scroller-ref.html new file mode 100644 index 0000000000..917b044841 --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/worklet-animation-with-scroll-timeline-root-scroller-ref.html @@ -0,0 +1,43 @@ +<!DOCTYPE html> +<title>Reference for Scroll timeline with WorkletAnimation using the root scroller</title> +<style> + /* Hide scrollbars to avoid unnecessary visual issues related to them */ + html::-webkit-scrollbar { + display: none; + } + + html { + scrollbar-width: none; + min-height: 100%; + min-width: 100%; + padding-bottom: 100px; + padding-right: 100px; + } + + #box { + width: 100px; + height: 100px; + background-color: green; + transform: translate(0, 100px); + opacity: 0.5; + will-change: transform; /* force compositing */ + } + + #covered { + width: 100px; + height: 100px; + background-color: red; + } +</style> + +<div id="box"></div> +<div id="covered"></div> + +<script> + window.addEventListener('load', function() { + // Move the scroller to halfway. + const scroller = document.scrollingElement; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.5 * maxScroll; + }); +</script>
\ No newline at end of file diff --git a/testing/web-platform/tests/animation-worklet/worklet-animation-with-scroll-timeline-root-scroller.https.html b/testing/web-platform/tests/animation-worklet/worklet-animation-with-scroll-timeline-root-scroller.https.html new file mode 100644 index 0000000000..60560a938a --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/worklet-animation-with-scroll-timeline-root-scroller.https.html @@ -0,0 +1,69 @@ +<html class="reftest-wait"> +<title>Scroll timeline with WorkletAnimation using the root scroller</title> +<link rel="help" href="https://drafts.css-houdini.org/css-animationworklet/"> +<meta name="assert" + content="Worklet animation correctly updates values when using the root scroller as the source for the ScrollTimeline"> +<link rel="match" href="worklet-animation-with-scroll-timeline-root-scroller-ref.html"> + +<script src="/web-animations/testcommon.js"></script> +<script src="/common/reftest-wait.js"></script> +<script src="common.js"></script> + +<style> + /* Hide scrollbars to avoid unnecessary visual issues related to them */ + html::-webkit-scrollbar { + display: none; + } + + html { + scrollbar-width: none; + min-height: 100%; + min-width: 100%; + padding-bottom: 100px; + padding-right: 100px; + } + + #box { + width: 100px; + height: 100px; + background-color: green; + } + + #covered { + width: 100px; + height: 100px; + background-color: red; + } +</style> + +<div id="box"></div> +<div id="covered"></div> + +<script> + registerPassthroughAnimator().then(() => { + const box = document.getElementById('box'); + const effect = new KeyframeEffect(box, + [ + { transform: 'translateY(0)', opacity: 1 }, + { transform: 'translateY(200px)', opacity: 0 } + ], { + duration: 1000, + } + ); + + const scroller = document.scrollingElement; + const timeline = new ScrollTimeline({ scrollSource: scroller, orientation: 'block' }); + const animation = new WorkletAnimation('passthrough', effect, timeline); + animation.play(); + + // Move the scroller to the halfway point. + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.5 * maxScroll; + + waitForAnimationFrameWithCondition(_ => { + return getComputedStyle(box).transform != 'matrix(1, 0, 0, 1, 0, 0)'; + }).then(_ => { + takeScreenshot(); + }); + }); +</script>
\ No newline at end of file diff --git a/testing/web-platform/tests/animation-worklet/worklet-animation-with-scroll-timeline.https.html b/testing/web-platform/tests/animation-worklet/worklet-animation-with-scroll-timeline.https.html new file mode 100644 index 0000000000..99bd171d92 --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/worklet-animation-with-scroll-timeline.https.html @@ -0,0 +1,74 @@ +<html class="reftest-wait"> +<title>Basic use of scroll timeline with WorkletAnimation</title> +<link rel="help" href="https://drafts.css-houdini.org/css-animationworklet/"> +<meta name="assert" content="Should be able to use the scroll timeline to drive the worklet animation timing"> +<link rel="match" href="worklet-animation-with-scroll-timeline-ref.html"> + +<script src="/web-animations/testcommon.js"></script> +<script src="/common/reftest-wait.js"></script> +<script src="common.js"></script> + +<style> + #box { + width: 100px; + height: 100px; + background-color: green; + } + + #covered { + width: 100px; + height: 100px; + background-color: red; + } + + /* Hide scrollbars to avoid unnecessary visual issues related to them */ + #scroller::-webkit-scrollbar { + display: none; + } + + #scroller { + scrollbar-width: none; + overflow: auto; + height: 100px; + width: 100px; + will-change: transform; /* force compositing */ + } + + #contents { + height: 1000px; + width: 100%; + } +</style> + +<div id="box"></div> +<div id="covered"></div> +<div id="scroller"> + <div id="contents"></div> +</div> + +<script> + registerPassthroughAnimator().then(() => { + const box = document.getElementById('box'); + const effect = new KeyframeEffect(box, + [ + { transform: 'translateY(0)', opacity: 1 }, + { transform: 'translateY(200px)', opacity: 0 } + ], { + duration: 1000, + } + ); + + const scroller = document.getElementById('scroller'); + const timeline = new ScrollTimeline({ scrollSource: scroller, orientation: 'block' }); + const animation = new WorkletAnimation('passthrough', effect, timeline); + animation.play(); + + // Move the scroller to the halfway point. + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.5 * maxScroll; + + waitForAsyncAnimationFrames(1).then(_ => { + takeScreenshot(); + }); + }); +</script>
\ No newline at end of file diff --git a/testing/web-platform/tests/animation-worklet/worklet-animation-without-target.https.html b/testing/web-platform/tests/animation-worklet/worklet-animation-without-target.https.html new file mode 100644 index 0000000000..bfb6b4faee --- /dev/null +++ b/testing/web-platform/tests/animation-worklet/worklet-animation-without-target.https.html @@ -0,0 +1,76 @@ +<!DOCTYPE html> +<title>Verify that effect without target is supported</title> +<link rel="help" href="https://drafts.css-houdini.org/css-animationworklet/"> + +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="common.js"></script> + +<div id="box"></div> + +<script> + +setup(setupAndRegisterTests, {explicit_done: true}); + +async function setupAndRegisterTests() { + await registerPassthroughAnimator(); + + promise_test(async t => { + const effect = new KeyframeEffect(null, + { transform: ['translateY(100px)', 'translateY(200px)'] }, + { duration: Infinity, iterations: 1 } + ); + const animation = new WorkletAnimation('passthrough', effect); + animation.play(); + + // Allow one async animation frame to pass so that animation is running. + await waitForAsyncAnimationFrames(1); + assert_equals(animation.playState, "running"); + // Allow time to advance so that we have a non-zero current time. + await waitForDocumentTimelineAdvance(); + const t0 = document.timeline.currentTime; + assert_greater_than(animation.currentTime, 0); + assert_times_equal(animation.currentTime, (t0 - animation.startTime)); + assert_equals(animation.playState, "running"); + + animation.cancel(); + }, 'Animating effect with no target should work.'); + + promise_test(async t => { + const effect = new KeyframeEffect(document.getElementById('box'), + { transform: ['translateY(100px)', 'translateY(200px)'] }, + { duration: Infinity, iterations: 1 } + ); + + const animation = new WorkletAnimation('passthrough', effect); + animation.play(); + + // Allow one async animation frame to pass so that animation is running. + await waitForAsyncAnimationFrames(1); + assert_equals(animation.playState, "running"); + // Allow time to advance so that we have a non-zero current time. + await waitForDocumentTimelineAdvance(); + const t0 = document.timeline.currentTime; + assert_greater_than(animation.currentTime, 0); + assert_times_equal(animation.currentTime, (t0 - animation.startTime)); + assert_equals(animation.playState, "running"); + + await waitForDocumentTimelineAdvance(); + animation.effect.target = null; + const t1 = document.timeline.currentTime; + assert_times_equal(animation.currentTime, (t1 - animation.startTime)); + assert_equals(animation.playState, "running"); + + await waitForDocumentTimelineAdvance(); + animation.effect.target = document.getElementById('box'); + const t2 = document.timeline.currentTime; + assert_times_equal(animation.currentTime, (t2 - animation.startTime)); + assert_equals(animation.playState, "running"); + + animation.cancel(); + }, 'The existence of a target does not affect the animation.'); + + done(); +} +</script> |