summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/animation-worklet
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /testing/web-platform/tests/animation-worklet
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/animation-worklet')
-rw-r--r--testing/web-platform/tests/animation-worklet/META.yml4
-rw-r--r--testing/web-platform/tests/animation-worklet/animate-multiple-effects-on-different-targets-via-main-thread.https.html65
-rw-r--r--testing/web-platform/tests/animation-worklet/animate-non-accelerated-property.https.html44
-rw-r--r--testing/web-platform/tests/animation-worklet/animation-worklet-inside-iframe.https.html61
-rw-r--r--testing/web-platform/tests/animation-worklet/animator-with-options.https.html38
-rw-r--r--testing/web-platform/tests/animation-worklet/cancel-non-accelerated-property.https.html45
-rw-r--r--testing/web-platform/tests/animation-worklet/common.js61
-rw-r--r--testing/web-platform/tests/animation-worklet/current-time.https.html64
-rw-r--r--testing/web-platform/tests/animation-worklet/idlharness.any.js18
-rw-r--r--testing/web-platform/tests/animation-worklet/inactive-timeline.https.html139
-rw-r--r--testing/web-platform/tests/animation-worklet/multiple-effects-on-same-target-driven-by-individual-local-time.https.html65
-rw-r--r--testing/web-platform/tests/animation-worklet/playback-rate.https.html345
-rw-r--r--testing/web-platform/tests/animation-worklet/references/not-translated-box-ref.html12
-rw-r--r--testing/web-platform/tests/animation-worklet/references/translated-box-ref.html12
-rw-r--r--testing/web-platform/tests/animation-worklet/resources/animator-iframe.html44
-rw-r--r--testing/web-platform/tests/animation-worklet/resources/iframe.html9
-rw-r--r--testing/web-platform/tests/animation-worklet/scroll-timeline-writing-modes.https.html168
-rw-r--r--testing/web-platform/tests/animation-worklet/stateful-animator.https.html216
-rw-r--r--testing/web-platform/tests/animation-worklet/worklet-animation-animator-name.https.html30
-rw-r--r--testing/web-platform/tests/animation-worklet/worklet-animation-cancel.https.html45
-rw-r--r--testing/web-platform/tests/animation-worklet/worklet-animation-creation.https.html141
-rw-r--r--testing/web-platform/tests/animation-worklet/worklet-animation-duration.https.html39
-rw-r--r--testing/web-platform/tests/animation-worklet/worklet-animation-get-computed-timing-progress-on-worklet-thread.https.html87
-rw-r--r--testing/web-platform/tests/animation-worklet/worklet-animation-get-timing-on-worklet-thread.https.html54
-rw-r--r--testing/web-platform/tests/animation-worklet/worklet-animation-local-time-after-duration.https.html41
-rw-r--r--testing/web-platform/tests/animation-worklet/worklet-animation-local-time-before-start.https.html41
-rw-r--r--testing/web-platform/tests/animation-worklet/worklet-animation-local-time-null-1.https.html163
-rw-r--r--testing/web-platform/tests/animation-worklet/worklet-animation-local-time-null-2-ref.html27
-rw-r--r--testing/web-platform/tests/animation-worklet/worklet-animation-local-time-null-2.https.html110
-rw-r--r--testing/web-platform/tests/animation-worklet/worklet-animation-pause-immediately.https.html37
-rw-r--r--testing/web-platform/tests/animation-worklet/worklet-animation-pause-resume.https.html40
-rw-r--r--testing/web-platform/tests/animation-worklet/worklet-animation-pause.https.html60
-rw-r--r--testing/web-platform/tests/animation-worklet/worklet-animation-play.https.html45
-rw-r--r--testing/web-platform/tests/animation-worklet/worklet-animation-set-keyframes.https.html44
-rw-r--r--testing/web-platform/tests/animation-worklet/worklet-animation-set-timing.https.html46
-rw-r--r--testing/web-platform/tests/animation-worklet/worklet-animation-start-delay-ref.html12
-rw-r--r--testing/web-platform/tests/animation-worklet/worklet-animation-start-delay.https.html64
-rw-r--r--testing/web-platform/tests/animation-worklet/worklet-animation-with-effects-from-different-frames.https.html48
-rw-r--r--testing/web-platform/tests/animation-worklet/worklet-animation-with-fill-mode.https.html147
-rw-r--r--testing/web-platform/tests/animation-worklet/worklet-animation-with-invalid-effect.https.html36
-rw-r--r--testing/web-platform/tests/animation-worklet/worklet-animation-with-non-ascii-name-ref.html12
-rw-r--r--testing/web-platform/tests/animation-worklet/worklet-animation-with-non-ascii-name.https.html59
-rw-r--r--testing/web-platform/tests/animation-worklet/worklet-animation-with-scroll-timeline-and-display-none.https.html84
-rw-r--r--testing/web-platform/tests/animation-worklet/worklet-animation-with-scroll-timeline-and-overflow-hidden-ref.html45
-rw-r--r--testing/web-platform/tests/animation-worklet/worklet-animation-with-scroll-timeline-and-overflow-hidden.https.html68
-rw-r--r--testing/web-platform/tests/animation-worklet/worklet-animation-with-scroll-timeline-ref.html51
-rw-r--r--testing/web-platform/tests/animation-worklet/worklet-animation-with-scroll-timeline-root-scroller-ref.html43
-rw-r--r--testing/web-platform/tests/animation-worklet/worklet-animation-with-scroll-timeline-root-scroller.https.html69
-rw-r--r--testing/web-platform/tests/animation-worklet/worklet-animation-with-scroll-timeline.https.html74
-rw-r--r--testing/web-platform/tests/animation-worklet/worklet-animation-without-target.https.html76
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>