summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/scroll-animations/css
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
commit43a97878ce14b72f0981164f87f2e35e14151312 (patch)
tree620249daf56c0258faa40cbdcf9cfba06de2a846 /testing/web-platform/tests/scroll-animations/css
parentInitial commit. (diff)
downloadfirefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz
firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/scroll-animations/css')
-rw-r--r--testing/web-platform/tests/scroll-animations/css/animation-duration-auto.tentative.html56
-rw-r--r--testing/web-platform/tests/scroll-animations/css/animation-shorthand.html43
-rw-r--r--testing/web-platform/tests/scroll-animations/css/animation-timeline-computed.html76
-rw-r--r--testing/web-platform/tests/scroll-animations/css/animation-timeline-ignored.tentative.html145
-rw-r--r--testing/web-platform/tests/scroll-animations/css/animation-timeline-in-keyframe.html27
-rw-r--r--testing/web-platform/tests/scroll-animations/css/animation-timeline-multiple.html90
-rw-r--r--testing/web-platform/tests/scroll-animations/css/animation-timeline-named-scroll-progress-timeline.tentative.html662
-rw-r--r--testing/web-platform/tests/scroll-animations/css/animation-timeline-none.html41
-rw-r--r--testing/web-platform/tests/scroll-animations/css/animation-timeline-parsing.html88
-rw-r--r--testing/web-platform/tests/scroll-animations/css/animation-timeline-scroll-functional-notation.tentative.html148
-rw-r--r--testing/web-platform/tests/scroll-animations/css/animation-timeline-view-functional-notation.tentative.html263
-rw-r--r--testing/web-platform/tests/scroll-animations/css/get-animations-inactive-timeline.html87
-rw-r--r--testing/web-platform/tests/scroll-animations/css/named-range-keyframes-with-document-timeline.tentative.html54
-rw-r--r--testing/web-platform/tests/scroll-animations/css/printing/animation-timeline-none-with-progress-print.tentative.html56
-rw-r--r--testing/web-platform/tests/scroll-animations/css/printing/animation-timeline-none-with-progress-ref.html38
-rw-r--r--testing/web-platform/tests/scroll-animations/css/printing/scroll-timeline-default-iframe-print.html65
-rw-r--r--testing/web-platform/tests/scroll-animations/css/printing/scroll-timeline-default-print.tentative.html62
-rw-r--r--testing/web-platform/tests/scroll-animations/css/printing/scroll-timeline-specified-scroller-print.html58
-rw-r--r--testing/web-platform/tests/scroll-animations/css/printing/scroll-timeline-specified-scroller-ref.html38
-rw-r--r--testing/web-platform/tests/scroll-animations/css/progress-based-animation-animation-longhand-properties.tentative.html255
-rw-r--r--testing/web-platform/tests/scroll-animations/css/progress-based-animation-timeline.html52
-rw-r--r--testing/web-platform/tests/scroll-animations/css/scroll-timeline-axis-computed.tentative.html35
-rw-r--r--testing/web-platform/tests/scroll-animations/css/scroll-timeline-axis-parsing.tentative.html27
-rw-r--r--testing/web-platform/tests/scroll-animations/css/scroll-timeline-axis-writing-mode.html126
-rw-r--r--testing/web-platform/tests/scroll-animations/css/scroll-timeline-default-iframe-ref.html33
-rw-r--r--testing/web-platform/tests/scroll-animations/css/scroll-timeline-default-iframe.html73
-rw-r--r--testing/web-platform/tests/scroll-animations/css/scroll-timeline-default-quirks-mode.html63
-rw-r--r--testing/web-platform/tests/scroll-animations/css/scroll-timeline-default-ref.html31
-rw-r--r--testing/web-platform/tests/scroll-animations/css/scroll-timeline-default-writing-mode-rl-ref.html32
-rw-r--r--testing/web-platform/tests/scroll-animations/css/scroll-timeline-default-writing-mode-rl.html65
-rw-r--r--testing/web-platform/tests/scroll-animations/css/scroll-timeline-default.html63
-rw-r--r--testing/web-platform/tests/scroll-animations/css/scroll-timeline-document-scroller-quirks.html36
-rw-r--r--testing/web-platform/tests/scroll-animations/css/scroll-timeline-dynamic.tentative.html267
-rw-r--r--testing/web-platform/tests/scroll-animations/css/scroll-timeline-frame-size-changed-ref.html31
-rw-r--r--testing/web-platform/tests/scroll-animations/css/scroll-timeline-frame-size-changed.html67
-rw-r--r--testing/web-platform/tests/scroll-animations/css/scroll-timeline-in-container-query.html74
-rw-r--r--testing/web-platform/tests/scroll-animations/css/scroll-timeline-inactive.html51
-rw-r--r--testing/web-platform/tests/scroll-animations/css/scroll-timeline-inline-orientation-ref.html32
-rw-r--r--testing/web-platform/tests/scroll-animations/css/scroll-timeline-inline-orientation.html68
-rw-r--r--testing/web-platform/tests/scroll-animations/css/scroll-timeline-multi-pass.tentative.html109
-rw-r--r--testing/web-platform/tests/scroll-animations/css/scroll-timeline-name-computed.tentative.html34
-rw-r--r--testing/web-platform/tests/scroll-animations/css/scroll-timeline-name-parsing.tentative.html30
-rw-r--r--testing/web-platform/tests/scroll-animations/css/scroll-timeline-name-tree-scoped.html144
-rw-r--r--testing/web-platform/tests/scroll-animations/css/scroll-timeline-nearest-dirty.html42
-rw-r--r--testing/web-platform/tests/scroll-animations/css/scroll-timeline-paused-animations.html95
-rw-r--r--testing/web-platform/tests/scroll-animations/css/scroll-timeline-responsiveness-from-endpoint.html62
-rw-r--r--testing/web-platform/tests/scroll-animations/css/scroll-timeline-root-dirty.html35
-rw-r--r--testing/web-platform/tests/scroll-animations/css/scroll-timeline-sampling.html46
-rw-r--r--testing/web-platform/tests/scroll-animations/css/scroll-timeline-shorthand.tentative.html41
-rw-r--r--testing/web-platform/tests/scroll-animations/css/scroll-timeline-sibling-gcs.html52
-rw-r--r--testing/web-platform/tests/scroll-animations/css/support/testcommon.js19
-rw-r--r--testing/web-platform/tests/scroll-animations/css/timeline-range-name-offset-in-keyframes.tentative.html94
-rw-r--r--testing/web-platform/tests/scroll-animations/css/view-timeline-animation.html219
-rw-r--r--testing/web-platform/tests/scroll-animations/css/view-timeline-axis-computed.html37
-rw-r--r--testing/web-platform/tests/scroll-animations/css/view-timeline-axis-parsing.html29
-rw-r--r--testing/web-platform/tests/scroll-animations/css/view-timeline-delay-animation.html144
-rw-r--r--testing/web-platform/tests/scroll-animations/css/view-timeline-dynamic.html183
-rw-r--r--testing/web-platform/tests/scroll-animations/css/view-timeline-inset-animation.html768
-rw-r--r--testing/web-platform/tests/scroll-animations/css/view-timeline-inset-computed.html41
-rw-r--r--testing/web-platform/tests/scroll-animations/css/view-timeline-inset-parsing.html34
-rw-r--r--testing/web-platform/tests/scroll-animations/css/view-timeline-lookup.html301
-rw-r--r--testing/web-platform/tests/scroll-animations/css/view-timeline-name-computed.html36
-rw-r--r--testing/web-platform/tests/scroll-animations/css/view-timeline-name-parsing.html30
-rw-r--r--testing/web-platform/tests/scroll-animations/css/view-timeline-name-tree-scoped.html145
-rw-r--r--testing/web-platform/tests/scroll-animations/css/view-timeline-shorthand.tentative.html104
-rw-r--r--testing/web-platform/tests/scroll-animations/css/view-timeline-used-values.html104
66 files changed, 6556 insertions, 0 deletions
diff --git a/testing/web-platform/tests/scroll-animations/css/animation-duration-auto.tentative.html b/testing/web-platform/tests/scroll-animations/css/animation-duration-auto.tentative.html
new file mode 100644
index 0000000000..f44a682890
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/animation-duration-auto.tentative.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<title>animation-duration: auto</title>
+<link rel="help" src="https://github.com/w3c/csswg-drafts/issues/6530">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<script src="./support/testcommon.js"></script>
+<script src="/css/support/parsing-testcommon.js"></script>
+<script src="/css/support/computed-testcommon.js"></script>
+<style>
+ #scroller {
+ overflow: hidden;
+ width: 100px;
+ height: 100px;
+ }
+ #scroller > div {
+ height: 200px;
+ width: 200px;
+ }
+
+ @keyframes anim {
+ from { z-index: 0; }
+ to { z-index: 100; }
+ }
+
+ #scroller {
+ scroll-timeline: timeline;
+ }
+
+ #element {
+ z-index: -1;
+ animation-name: anim;
+ animation-duration: auto;
+ animation-timeline: timeline;
+ }
+</style>
+<main>
+ <div id=scroller>
+ <div></div>
+ </div>
+ <div id=element></div>
+</main>
+<script>
+ promise_test(async (t) => {
+ await waitForCSSScrollTimelineStyle();
+ assert_equals(getComputedStyle(element).zIndex, '0');
+ }, 'A value of auto can be specified for animation-duration');
+</script>
+
+<div id="target"></div>
+<script>
+ test_valid_value('animation-duration', 'auto');
+ test_computed_value('animation-duration', 'auto');
+ test_valid_value('animation', 'auto cubic-bezier(0, -2, 1, 3) -3s 4 reverse both paused anim');
+ test_computed_value('animation', 'auto cubic-bezier(0, -2, 1, 3) -3s 4 reverse both paused anim');
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/animation-shorthand.html b/testing/web-platform/tests/scroll-animations/css/animation-shorthand.html
new file mode 100644
index 0000000000..60d6c76646
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/animation-shorthand.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/css-animations-2/#animation-shorthand">
+<link rel="help" href="https://drafts.csswg.org/css-animations-2/#animation-timeline">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/computed-testcommon.js"></script>
+<script src="/css/support/parsing-testcommon.js"></script>
+<script src="/css/support/shorthand-testcommon.js"></script>
+<div id="target"></div>
+<script>
+test_valid_value('animation',
+ '1s linear 1s 2 reverse forwards paused anim');
+
+test_invalid_value('animation',
+ '1s linear 1s 2 reverse forwards paused anim initial');
+test_invalid_value('animation',
+ '1s linear 1s 2 reverse forwards paused anim 2000');
+test_invalid_value('animation',
+ '1s linear 1s 2 reverse forwards paused anim scroll()');
+test_invalid_value('animation',
+ '1s linear 1s 2 reverse forwards paused anim view()');
+test_invalid_value('animation',
+ '1s linear 1s 2 reverse forwards paused anim timeline');
+
+test_computed_value('animation',
+ '1s linear 1s 2 reverse forwards paused anim');
+
+test_shorthand_value('animation',
+ `1s linear 1s 2 reverse forwards paused anim1,
+ 1s linear 1s 2 reverse forwards paused anim2,
+ 1s linear 1s 2 reverse forwards paused anim3`,
+{
+ 'animation-duration': '1s, 1s, 1s',
+ 'animation-timing-function': 'linear, linear, linear',
+ 'animation-delay': '1s, 1s, 1s',
+ 'animation-iteration-count': '2, 2, 2',
+ 'animation-direction': 'reverse, reverse, reverse',
+ 'animation-fill-mode': 'forwards, forwards, forwards',
+ 'animation-play-state': 'paused, paused, paused',
+ 'animation-name': 'anim1, anim2, anim3',
+ 'animation-timeline': 'auto, auto, auto'
+});
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/animation-timeline-computed.html b/testing/web-platform/tests/scroll-animations/css/animation-timeline-computed.html
new file mode 100644
index 0000000000..9cabaab1fe
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/animation-timeline-computed.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#animation-timeline">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/computed-testcommon.js"></script>
+</head>
+<style>
+ #outer { animation-timeline: foo; }
+ #target { animation-timeline: bar; }
+</style>
+<div id="outer">
+ <div id="target"></div>
+</div>
+<script>
+test_computed_value('animation-timeline', 'initial', 'auto');
+test_computed_value('animation-timeline', 'inherit', 'foo');
+test_computed_value('animation-timeline', 'unset', 'auto');
+test_computed_value('animation-timeline', 'revert', 'auto');
+test_computed_value('animation-timeline', 'auto');
+test_computed_value('animation-timeline', 'none');
+test_computed_value('animation-timeline', 'auto, auto');
+test_computed_value('animation-timeline', 'none, none');
+test_computed_value('animation-timeline', 'auto, none');
+test_computed_value('animation-timeline', 'none, auto');
+test_computed_value('animation-timeline', '"test"', 'test');
+test_computed_value('animation-timeline', '"none"');
+test_computed_value('animation-timeline', '"auto"');
+test_computed_value('animation-timeline', '"initial"');
+test_computed_value('animation-timeline', '"inherit"');
+test_computed_value('animation-timeline', '"unset"');
+test_computed_value('animation-timeline', '"revert"');
+test_computed_value('animation-timeline', '"revert-layer"');
+test_computed_value('animation-timeline', 'test');
+test_computed_value('animation-timeline', 'test1, test2');
+test_computed_value('animation-timeline', 'test1, "test2", none, test3, auto', 'test1, test2, none, test3, auto');
+
+test(() => {
+ let style = getComputedStyle(document.getElementById('target'));
+ assert_not_equals(Array.from(style).indexOf('animation-timeline'), -1);
+}, 'The animation-timeline property shows up in CSSStyleDeclaration enumeration');
+
+test(() => {
+ let style = document.getElementById('target').style;
+ assert_not_equals(style.cssText.indexOf('animation-timeline'), -1);
+}, 'The animation-timeline property shows up in CSSStyleDeclaration.cssText');
+
+// https://drafts.csswg.org/scroll-animations-1/#scroll-notation
+//
+// animation-timeline: scroll(<axis>? <scroller>?);
+// <axis> = block | inline | vertical | horizontal
+// <scroller> = root | nearest
+test_computed_value('animation-timeline', 'scroll()');
+test_computed_value('animation-timeline', 'scroll(block)', 'scroll()');
+test_computed_value('animation-timeline', 'scroll(inline)');
+test_computed_value('animation-timeline', 'scroll(horizontal)');
+test_computed_value('animation-timeline', 'scroll(vertical)');
+test_computed_value('animation-timeline', 'scroll(root)');
+test_computed_value('animation-timeline', 'scroll(nearest)', 'scroll()');
+test_computed_value('animation-timeline', 'scroll(inline nearest)', 'scroll(inline)');
+test_computed_value('animation-timeline', 'scroll(vertical root)');
+
+// https://drafts.csswg.org/scroll-animations-1/#view-notation
+test_computed_value('animation-timeline', 'view()');
+test_computed_value('animation-timeline', 'view(block)', 'view()');
+test_computed_value('animation-timeline', 'view(inline)', 'view(inline)');
+test_computed_value('animation-timeline', 'view(horizontal)', 'view(horizontal)');
+test_computed_value('animation-timeline', 'view(vertical)', 'view(vertical)');
+test_computed_value('animation-timeline', 'view(vertical 1px)');
+test_computed_value('animation-timeline', 'view(1px auto)');
+test_computed_value('animation-timeline', 'view(auto 1px)');
+test_computed_value('animation-timeline', 'view(vertical 1px auto)');
+test_computed_value('animation-timeline', 'view(1px vertical)', 'view(vertical 1px)');
+test_computed_value('animation-timeline', 'view(vertical auto)', 'view(vertical)');
+test_computed_value('animation-timeline', 'view(vertical auto auto)', 'view(vertical)');
+
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/animation-timeline-ignored.tentative.html b/testing/web-platform/tests/scroll-animations/css/animation-timeline-ignored.tentative.html
new file mode 100644
index 0000000000..32cb89c4ef
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/animation-timeline-ignored.tentative.html
@@ -0,0 +1,145 @@
+<!DOCTYPE html>
+<link rel="help" src="https://github.com/w3c/csswg-drafts/pull/5666">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<style>
+ main {
+ overflow: hidden;
+ height: 0px;
+ }
+ main > div {
+ overflow: hidden;
+ width: 100px;
+ height: 100px;
+ }
+ main > div > div {
+ height: 200px;
+ }
+ @keyframes expand {
+ from { width: 100px; }
+ to { width: 200px; }
+ }
+ #scroller1 {
+ scroll-timeline: timeline1;
+ }
+ #scroller2 {
+ scroll-timeline: timeline2;
+ }
+ #scroller3 {
+ scroll-timeline: timeline3;
+ }
+ #element {
+ width: 0px;
+ height: 20px;
+ animation-name: expand;
+ animation-duration: 1000s;
+ animation-timing-function: linear;
+ animation-timeline: timeline1;
+ }
+ /* Ensure stable expectations if feature is not supported */
+ @supports not (animation-timeline:foo) {
+ #element { animation-play-state: paused; }
+ }
+</style>
+<main>
+ <div id=scroller1><div></div></div>
+ <div id=scroller2><div></div></div>
+ <div id=scroller3><div></div></div>
+ <div id=scroller4><div></div></div>
+ <div id=container></div>
+</main>
+<script>
+ // Force layout of scrollers.
+ scroller1.offsetTop;
+ scroller2.offsetTop;
+ scroller3.offsetTop;
+ scroller4.offsetTop;
+
+ scroller1.scrollTop = 20;
+ scroller2.scrollTop = 40;
+ scroller3.scrollTop = 60;
+ scroller4.scrollTop = 80;
+
+ // Create #element in #container, run |func|, then clean up afterwards.
+ function test_animation_timeline(func, description) {
+ promise_test(async () => {
+ try {
+ let element = document.createElement('element');
+ element.setAttribute('id', 'element');
+ container.append(element);
+ await func();
+ } finally {
+ while (container.firstChild)
+ container.firstChild.remove();
+ }
+ }, description);
+ }
+
+ test_animation_timeline(async () => {
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(element).width, '120px');
+ element.style = 'animation-timeline:timeline2';
+ assert_equals(getComputedStyle(element).width, '140px');
+ }, 'Changing animation-timeline changes the timeline (sanity check)');
+
+ test_animation_timeline(async () => {
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(element).width, '120px');
+
+ // Set a (non-CSS) ScrollTimeline on the CSSAnimation.
+ let timeline4 = new ScrollTimeline({
+ source: scroller4,
+ scrollOffsets: [CSS.px(0), CSS.px(100)]
+ });
+
+ element.getAnimations()[0].timeline = timeline4;
+ assert_equals(getComputedStyle(element).width, '180px');
+
+ // Changing the animation-timeline property should have no effect.
+ element.style = 'animation-timeline:timeline2';
+ assert_equals(getComputedStyle(element).width, '180px');
+ }, 'animation-timeline ignored after setting timeline with JS (ScrollTimeline from JS)');
+
+ test_animation_timeline(async () => {
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(element).width, '120px');
+ let animation = element.getAnimations()[0];
+ let timeline1 = animation.timeline;
+
+ element.style = 'animation-timeline:timeline2';
+ assert_equals(getComputedStyle(element).width, '140px');
+
+ animation.timeline = timeline1;
+ assert_equals(getComputedStyle(element).width, '120px');
+
+ // Should have no effect.
+ element.style = 'animation-timeline:timeline3';
+ assert_equals(getComputedStyle(element).width, '120px');
+ }, 'animation-timeline ignored after setting timeline with JS (ScrollTimeline from CSS)');
+
+ test_animation_timeline(async () => {
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(element).width, '120px');
+ element.getAnimations()[0].timeline = document.timeline;
+
+ // (The animation continues from where the previous timeline left it).
+ assert_equals(getComputedStyle(element).width, '120px');
+
+ // Changing the animation-timeline property should have no effect.
+ element.style = 'animation-timeline:timeline2';
+ assert_equals(getComputedStyle(element).width, '120px');
+ }, 'animation-timeline ignored after setting timeline with JS (document timeline)');
+
+ test_animation_timeline(async () => {
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(element).width, '120px');
+ element.getAnimations()[0].timeline = null;
+ assert_equals(getComputedStyle(element).width, '0px');
+
+ // Changing the animation-timeline property should have no effect.
+ element.style = 'animation-timeline:timeline2';
+ assert_equals(getComputedStyle(element).width, '0px');
+ }, 'animation-timeline ignored after setting timeline with JS (null)');
+
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/animation-timeline-in-keyframe.html b/testing/web-platform/tests/scroll-animations/css/animation-timeline-in-keyframe.html
new file mode 100644
index 0000000000..7548333139
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/animation-timeline-in-keyframe.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#animation-timeline">
+<link rel="help" href="https://drafts.csswg.org/css-animations-1/#keyframes">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<style>
+ @keyframes test {
+ from { width: 100px; animation-timeline: foo; }
+ to { width: 100px; animation-timeline: foo; }
+ }
+ #target {
+ width: 50px;
+ animation-name: test;
+ animation-duration: 1s;
+ animation-play-state: paused;
+ }
+</style>
+<div id="target"></div>
+<script>
+test(() => {
+ let style = getComputedStyle(document.getElementById('target'));
+ // Checking 'width' verifies that the animation is applied at all.
+ assert_equals(style.width, '100px');
+ assert_equals(style.animationTimeline, 'auto');
+}, 'The animation-timeline property may not be used in keyframes');
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/animation-timeline-multiple.html b/testing/web-platform/tests/scroll-animations/css/animation-timeline-multiple.html
new file mode 100644
index 0000000000..8ae37bf751
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/animation-timeline-multiple.html
@@ -0,0 +1,90 @@
+<!DOCTYPE html>
+<title>animation-timeline with multiple timelines</title>
+<link rel="help" src="https://drafts.csswg.org/css-animations-2/#animation-timeline">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<style>
+ main > div {
+ overflow: hidden;
+ width: 100px;
+ height: 100px;
+ }
+ main > div > div {
+ height: 200px;
+ width: 200px;
+ }
+
+ @keyframes top {
+ from { top: 100px; }
+ to { top: 200px; }
+ }
+ @keyframes bottom {
+ from { bottom: 100px; }
+ to { bottom: 200px; }
+ }
+ @keyframes left {
+ from { left: 100px; }
+ to { left: 200px; }
+ }
+ @keyframes right {
+ from { right: 100px; }
+ to { right: 200px; }
+ }
+
+ #top_scroller {
+ scroll-timeline: block top_timeline;
+ }
+ #bottom_scroller {
+ scroll-timeline: inline bottom_timeline;
+ }
+ #left_scroller {
+ scroll-timeline: block left_timeline;
+ }
+ #right_scroller {
+ scroll-timeline: inline right_timeline;
+ }
+
+ #element {
+ animation-name: top, bottom, left, right;
+ animation-duration: 10s;
+ animation-timing-function: linear;
+ animation-timeline: top_timeline, bottom_timeline, left_timeline, right_timeline;
+ }
+ /* Ensure stable expectations if feature is not supported */
+ @supports not (animation-timeline:foo) {
+ #element { animation-play-state: paused; }
+ }
+</style>
+<main>
+ <div id=top_scroller><div></div></div>
+ <div id=bottom_scroller><div></div></div>
+ <div id=left_scroller><div></div></div>
+ <div id=right_scroller><div></div></div>
+ <div id=element></div>
+</main>
+<script>
+ // Force layout of scrollers.
+ top_scroller.offsetTop;
+ bottom_scroller.offsetTop;
+ left_scroller.offsetTop;
+ right_scroller.offsetTop;
+
+ top_scroller.scrollTop = 20;
+ top_scroller.scrollLeft = 40;
+ bottom_scroller.scrollTop = 20;
+ bottom_scroller.scrollLeft = 40;
+ left_scroller.scrollTop = 60;
+ left_scroller.scrollLeft = 80;
+ right_scroller.scrollTop = 60;
+ right_scroller.scrollLeft = 80;
+
+ promise_test(async (t) => {
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(element).top, '120px');
+ assert_equals(getComputedStyle(element).bottom, '140px');
+ assert_equals(getComputedStyle(element).left, '160px');
+ assert_equals(getComputedStyle(element).right, '180px');
+ }, 'animation-timeline works with multiple timelines');
+
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/animation-timeline-named-scroll-progress-timeline.tentative.html b/testing/web-platform/tests/scroll-animations/css/animation-timeline-named-scroll-progress-timeline.tentative.html
new file mode 100644
index 0000000000..9f55e2d2c9
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/animation-timeline-named-scroll-progress-timeline.tentative.html
@@ -0,0 +1,662 @@
+<!DOCTYPE html>
+<title>The animation-timeline: scroll-timeline-name</title>
+<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1">
+<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/rewrite#scroll-timelines-named">
+<link rel="help" src="https://github.com/w3c/csswg-drafts/issues/6674">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<script src="support/testcommon.js"></script>
+<style>
+ @keyframes anim {
+ from { translate: 50px; }
+ to { translate: 150px; }
+ }
+ #target {
+ width: 100px;
+ height: 100px;
+ }
+ .square {
+ width: 100px;
+ height: 100px;
+ }
+ .square-container {
+ width: 300px;
+ height: 300px;
+ }
+ .scroller {
+ overflow: scroll;
+ }
+ .content {
+ inline-size: 100%;
+ block-size: 100%;
+ padding-inline-end: 100px;
+ padding-block-end: 100px;
+ }
+</style>
+<body>
+<div id="log"></div>
+<script>
+"use strict";
+
+setup(assert_implements_animation_timeline);
+
+function createScroller(t, scrollerSizeClass) {
+ let scroller = document.createElement('div');
+ let className = scrollerSizeClass || 'square';
+ scroller.className = `scroller ${className}`;
+ let content = document.createElement('div');
+ content.className = 'content';
+
+ scroller.appendChild(content);
+
+ t.add_cleanup(function() {
+ content.remove();
+ scroller.remove();
+ });
+
+ return scroller;
+}
+
+function createTarget(t) {
+ let target = document.createElement('div');
+ target.id = 'target';
+
+ t.add_cleanup(function() {
+ target.remove();
+ });
+
+ return target;
+}
+
+function createScrollerAndTarget(t, scrollerSizeClass) {
+ return [createScroller(t, scrollerSizeClass), createTarget(t)];
+}
+
+// -------------------------
+// Test scroll-timeline-name
+// -------------------------
+
+promise_test(async t => {
+ let target = document.createElement('div');
+ target.id = 'target';
+ target.className = 'scroller';
+ let content = document.createElement('div');
+ content.className = 'content';
+
+ // <div id='target' class='scroller'>
+ // <div id='content'></div>
+ // </div>
+ document.body.appendChild(target);
+ target.appendChild(content);
+
+ target.style.scrollTimelineName = 'timeline';
+ target.style.animation = "anim 10s linear";
+ target.style.animationTimeline = 'timeline';
+
+ target.scrollTop = 50; // 50%, in [0, 100].
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(target).translate, '100px');
+
+ content.remove();
+ target.remove();
+}, 'scroll-timeline-name is referenceable in animation-timeline on the ' +
+ 'declaring element itself');
+
+promise_test(async t => {
+ let [parent, target] = createScrollerAndTarget(t, 'square-container');
+
+ // <div id='parent' class='scroller'>
+ // <div id='target'></div>
+ // <div id='content'></div>
+ // </div>
+ document.body.appendChild(parent);
+ parent.insertBefore(target, parent.firstElementChild);
+
+ parent.style.scrollTimelineName = 'timeline';
+ target.style.animation = "anim 10s linear";
+ target.style.animationTimeline = 'timeline';
+
+ parent.scrollTop = 100; // 50%, in [0, 200].
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(target).translate, '100px');
+}, "scroll-timeline-name is referenceable in animation-timeline on that " +
+ "element's descendants");
+
+promise_test(async t => {
+ let [sibling, target] = createScrollerAndTarget(t);
+
+ // <div id='sibling' class='scroller'> ... </div>
+ // <div id='target'></div>
+ document.body.appendChild(sibling);
+ document.body.appendChild(target);
+
+ sibling.style.scrollTimelineName = 'timeline';
+ target.style.animation = "anim 10s linear";
+ target.style.animationTimeline = 'timeline';
+
+ sibling.scrollTop = 50; // 50%, in [0, 100].
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(target).translate, '100px');
+}, "scroll-timeline-name is referenceable in animation-timeline on that " +
+ "element's following siblings");
+
+promise_test(async t => {
+ let [sibling, target] = createScrollerAndTarget(t);
+ let parent = document.createElement('div');
+
+ // <div id='sibling' class='scroller'> ... </div>
+ // <div id='parent'>
+ // <div id='target'></div>
+ // </div>
+ document.body.appendChild(sibling);
+ document.body.appendChild(parent);
+ parent.appendChild(target);
+
+ sibling.style.scrollTimelineName = 'timeline';
+ target.style.animation = "anim 10s linear";
+ target.style.animationTimeline = 'timeline';
+
+ sibling.scrollTop = 50; // 50%, in [0, 100].
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(target).translate, '100px');
+
+ parent.remove();
+}, "scroll-timeline-name is referenceable in animation-timeline on that " +
+ "element's following siblings' descendants");
+
+// FIXME: We may use global scope for scroll-timeline-name.
+// See https://github.com/w3c/csswg-drafts/issues/7047
+promise_test(async t => {
+ let [sibling, target] = createScrollerAndTarget(t);
+
+ // <div id='target'></div>
+ // <div id='sibling' class='scroller'> ... </div>
+ document.body.appendChild(target);
+ document.body.appendChild(sibling);
+
+ sibling.style.scrollTimelineName = 'timeline';
+ target.style.animation = "anim 10s linear";
+ target.style.animationTimeline = 'timeline';
+
+ sibling.scrollTop = 50; // 50%, in [0, 100].
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(target).translate, '50px',
+ 'Animation with unknown timeline name holds current time at zero');
+}, "scroll-timeline-name is not referenceable in animation-timeline on that " +
+ "element's previous siblings");
+
+promise_test(async t => {
+ let [sibling, target] = createScrollerAndTarget(t);
+ let parent = document.createElement('div');
+ parent.className = 'scroller square-container';
+ let content = document.createElement('div');
+ content.className = 'content';
+
+ // <div id='parent' class='scroller'>
+ // <div id='sibling' class='scroller'> ... </div>
+ // <div id='target'></div>
+ // <div id='content'></div>
+ // </div>
+ document.body.appendChild(parent);
+ parent.appendChild(sibling);
+ parent.appendChild(target);
+ parent.appendChild(content);
+
+ parent.style.scrollTimelineName = 'timeline';
+ parent.style.scrollTimelineAxis = 'inline';
+ sibling.style.scrollTimelineName = 'timeline';
+ target.style.animation = "anim 10s linear";
+ target.style.animationTimeline = 'timeline';
+
+ parent.scrollTop = 50; // 25%, in [0, 200].
+ sibling.scrollTop = 50; // 50%, in [0, 100].
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(target).translate, '100px');
+
+ content.remove();
+ parent.remove();
+}, 'scroll-timeline-name is matched based on tree order, which considers ' +
+ 'siblings closer than parents');
+
+promise_test(async t => {
+ let sibling = document.createElement('div');
+ sibling.className = 'square';
+ sibling.style.overflowX = 'clip'; // This makes overflow-y be clip as well.
+ let target = document.createElement('div');
+ target.id = 'target';
+
+ // <div id='sibling' style='overflow-x: clip'></div>
+ // <div id='target'></div>
+ document.body.appendChild(sibling);
+ document.body.appendChild(target);
+
+ sibling.style.scrollTimelineName = 'timeline';
+ target.style.animation = "anim 10s linear";
+ target.style.animationTimeline = 'timeline';
+
+ sibling.scrollTop = 50; // 50%, in [0, 100].
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(target).translate, 'none',
+ 'Animation with an unresolved current time');
+
+ target.remove();
+ sibling.remove();
+}, 'scroll-timeline-name on an element which is not a scroll-container');
+
+promise_test(async t => {
+ let [sibling, target] = createScrollerAndTarget(t);
+ let main = document.createElement('div');
+ main.id = 'name';
+
+ // <div id='main'>
+ // <div id='sibling' class='scroller'> ... </div>
+ // <div id='target'></div>
+ // </div>
+ document.body.appendChild(main);
+ main.appendChild(sibling);
+ main.appendChild(target);
+
+ target.style.animation = 'anim 10s linear';
+ target.style.animationTimeline = 'timeline';
+ sibling.scrollTop = 50; // 50%, in [50, 150].
+ await waitForNextFrame();
+
+ // Unknown animation-timeline, current time held at zero.
+ assert_equals(getComputedStyle(target).translate, '50px');
+
+ // Ensure that #main (an ancestor of the scroller) needs style recalc.
+ main.style.background = 'lightgray';
+ sibling.style.scrollTimelineName = 'timeline';
+ await waitForCSSScrollTimelineStyle();
+ assert_equals(getComputedStyle(target).translate, '100px');
+
+ main.remove();
+}, 'scroll-timeline-name affects subsequent siblings when changed');
+
+promise_test(async t => {
+ let target = createTarget(t);
+
+ // <div id='target'></div>
+ document.body.appendChild(target);
+
+ target.style.animation = 'anim 10s linear';
+ target.style.animationTimeline = 'timeline';
+
+ // Unknown animation-timeline, current time held at zero.
+ assert_equals(getComputedStyle(target).translate, '50px');
+
+ let scroller = createScroller(t);
+ // <div class='scroller'> ... </div>
+ // <div id='target'></div>
+ document.body.insertBefore(scroller, target);
+ scroller.style.scrollTimelineName = 'timeline';
+
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(target).translate, '50px');
+
+ // Ensure that time is not just held at zero.
+ scroller.scrollTop = 50; // 50%, in [50, 150].
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(target).translate, '100px');
+}, 'scroll-timeline-name on inserted element affects subsequent siblings');
+
+promise_test(async t => {
+ let [scroller, target] = createScrollerAndTarget(t);
+
+ // <div class='scroller'> ... </div>
+ // <div id='target'></div>
+ document.body.appendChild(scroller);
+ document.body.appendChild(target);
+
+ scroller.scrollTop = 50; // 50%, in [50, 150].
+ await waitForNextFrame();
+
+ scroller.style.scrollTimelineName = 'timeline';
+ target.style.animation = 'anim 10s linear';
+ target.style.animationTimeline = 'timeline';
+ await waitForCSSScrollTimelineStyle();
+
+ assert_equals(getComputedStyle(target).translate, '100px');
+
+ // This effectively removes the CSS-created ScrollTimeline on this element,
+ // thus invoking "setting the timeline of an animation" [1] with a null-
+ // timeline on affected elements. This in turn causes the current time to
+ // become unresolved [2], ultimately resulting in no effect value.
+ //
+ // [1] https://drafts.csswg.org/web-animations-1/#setting-the-timeline
+ // [2] https://drafts.csswg.org/web-animations-1/#the-current-time-of-an-animation
+ scroller.remove();
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(target).translate, 'none');
+}, 'scroll-timeline-name on removed element affects subsequent siblings');
+
+promise_test(async t => {
+ let [scroller, target] = createScrollerAndTarget(t);
+
+ // <div class='scroller' style='display:none'> ... </div>
+ // <div id='target'></div>
+ scroller.style.display = 'none';
+ document.body.appendChild(scroller);
+ document.body.appendChild(target);
+
+ scroller.style.scrollTimelineName = 'timeline';
+ target.style.animation = 'anim 10s linear';
+ target.style.animationTimeline = 'timeline';
+
+ // Unknown animation-timeline, current time held at zero.
+ assert_equals(getComputedStyle(target).translate, '50px');
+
+ scroller.style.display = 'block';
+ scroller.scrollTop = 50; // 50%, in [50, 150].
+ await waitForNextFrame();
+
+ assert_equals(getComputedStyle(target).translate, '100px');
+}, 'scroll-timeline-name on element leaving display:none affects subsequent siblings');
+
+promise_test(async t => {
+ let [scroller, target] = createScrollerAndTarget(t);
+
+ // <div class='scroller'> ... </div>
+ // <div id='target'></div>
+ document.body.appendChild(scroller);
+ document.body.appendChild(target);
+
+ scroller.scrollTop = 50; // 50%, in [50, 150].
+ await waitForNextFrame();
+
+ scroller.style.scrollTimelineName = 'timeline';
+ target.style.animation = 'anim 10s linear';
+ target.style.animationTimeline = 'timeline';
+ await waitForCSSScrollTimelineStyle();
+
+ assert_equals(getComputedStyle(target).translate, '100px');
+
+ // See comment in the test "scroll-timeline-name on removed element ..." for
+ // an explantation of this result. (Setting display:none is similar to
+ // removing the element).
+ scroller.style.display = 'none';
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(target).translate, 'none');
+}, 'scroll-timeline-name on element becoming display:none affects subsequent siblings');
+
+promise_test(async t => {
+ let [scroller, target] = createScrollerAndTarget(t);
+
+ // <div id='scroller' class='scroller'> ... </div>
+ // <div id='target'></div>
+
+ document.body.appendChild(scroller);
+ document.body.appendChild(target);
+
+ scroller.style.scrollTimelineName = 'timeline';
+ scroller.style.display = 'none';
+ target.style.animation = "anim 10s linear";
+ target.style.animationTimeline = 'timeline';
+
+ await waitForNextFrame();
+
+ const anim = target.getAnimations()[0];
+ assert_true(!!anim, 'Failed to create animation');
+ assert_equals(anim.timeline, null);
+ // Hold time of animation is zero.
+ assert_equals(getComputedStyle(target).translate, '50px');
+
+ scroller.style.display = 'block';
+ scroller.scrollTop = 50;
+ await waitForNextFrame();
+
+ assert_true(!!anim.timeline, 'Failed to create timeline');
+ assert_equals(getComputedStyle(target).translate, '100px');
+
+}, 'scroll-timeline-name on element not resolved until element becomes visible');
+
+promise_test(async t => {
+ let [scroller, target] = createScrollerAndTarget(t);
+
+ // <div id='scroller' class='scroller'> ... </div>
+ // <div id='target'></div>
+
+ document.body.appendChild(scroller);
+ document.body.appendChild(target);
+
+ scroller.style.scrollTimelineName = 'timeline-A';
+ scroller.scrollTop = 50;
+ target.style.animation = "anim 10s linear";
+ target.style.animationTimeline = 'timeline-B';
+
+ await waitForNextFrame();
+
+ const anim = target.getAnimations()[0];
+ assert_true(!!anim, 'Failed to create animation');
+ assert_equals(anim.timeline, null);
+ // Hold time of animation is zero.
+ assert_equals(getComputedStyle(target).translate, '50px');
+
+ scroller.style.scrollTimelineName = 'timeline-B';
+ await waitForNextFrame();
+
+ assert_true(!!anim.timeline, 'Failed to create timeline');
+ assert_equals(getComputedStyle(target).translate, '100px');
+
+}, 'Change in scroll-timeline-name to match animation timeline updates animation.');
+
+promise_test(async t => {
+ let [scroller, target] = createScrollerAndTarget(t);
+
+ // <div id='scroller' class='scroller'> ... </div>
+ // <div id='target'></div>
+
+ document.body.appendChild(scroller);
+ document.body.appendChild(target);
+
+ scroller.style.scrollTimelineName = 'timeline-A';
+ scroller.scrollTop = 50;
+ target.style.animation = "anim 10s linear";
+ target.style.animationTimeline = 'timeline-A';
+
+ await waitForNextFrame();
+
+ const anim = target.getAnimations()[0];
+ assert_true(!!anim, 'Failed to create animation');
+ assert_true(!!anim.timeline, 'Failed to create timeline');
+ assert_equals(getComputedStyle(target).translate, '100px');
+
+ scroller.style.scrollTimelineName = 'timeline-B';
+ await waitForNextFrame();
+
+ assert_equals(anim.timeline, null, 'Failed to remove timeline');
+ assert_equals(getComputedStyle(target).translate, 'none');
+
+}, 'Change in scroll-timeline-name to no longer match animation timeline updates animation.');
+
+promise_test(async t => {
+ let target = createTarget(t);
+ let scroller1 = createScroller(t);
+ let scroller2 = createScroller(t);
+
+ target.style.animation = 'anim 10s linear';
+ target.style.animationTimeline = 'timeline';
+ scroller1.style.scrollTimelineName = 'timeline';
+ scroller2.style.scrollTimelineName = 'timeline';
+ scroller1.id = 'A';
+ scroller2.id = 'B';
+
+ // <div class='scroller' id='A'> ... </div> (scroller1)
+ // <div class='scroller' id="B"> ... </div> (scroller2)
+ // <div id='target'></div>
+ document.body.appendChild(scroller1);
+ document.body.appendChild(scroller2);
+ document.body.append(target);
+
+ scroller1.scrollTop = 10; // 10%, in [50, 150].
+ scroller2.scrollTop = 50; // 50%, in [50, 150].
+ await waitForNextFrame();
+
+
+ // The named timeline lookup should select scroller2.
+ let anim = target.getAnimations()[0];
+ assert_true(!!anim, 'Failed to fetch animation');
+ assert_equals(anim.timeline.source.id, 'B');
+ assert_equals(getComputedStyle(target).translate, '100px');
+
+ scroller2.remove();
+
+ // Now it should select scroller1.
+ anim = target.getAnimations()[0];
+ assert_true(!!anim, 'Failed to fetch animation after update');
+ assert_true(!!anim.timeline, 'Animation no longer has a timeline');
+ assert_equals(anim.timeline.source.id, 'A', 'Timeline not updated');
+ assert_equals(getComputedStyle(target).translate, '60px');
+}, 'Timeline lookup finds next candidate when element is removed');
+
+promise_test(async t => {
+ let target = createTarget(t);
+ let scroller1 = createScroller(t);
+
+ target.style.animation = 'anim 10s linear';
+ target.style.animationTimeline = 'timeline';
+ scroller1.style.scrollTimelineName = 'timeline';
+ scroller1.id = 'A';
+
+ // <div class='scroller' id='A'> ... </div> (scroller1)
+ // <div id='target'></div>
+ document.body.appendChild(scroller1);
+ document.body.append(target);
+
+ scroller1.scrollTop = 10; // 10%, in [50, 150].
+
+ await waitForNextFrame();
+
+ const anim = target.getAnimations()[0];
+
+ assert_true(!!anim.timeline, 'Failed to retrieve animation');
+ assert_equals(anim.timeline.source.id, 'A');
+ assert_equals(getComputedStyle(target).translate, '60px');
+
+ await waitForNextFrame();
+
+ let scroller2 = createScroller(t);
+ scroller2.style.scrollTimelineName = 'timeline';
+ scroller2.id = 'B';
+
+ // <div class='scroller' id="A"> ... </div> (scroller1)
+ // <div class='scroller' id="B"> ... </div> (scroller2)
+ // <div id='target'></div>
+ document.body.insertBefore(scroller2, target);
+
+ scroller2.scrollTop = 50; // 50%, in [50, 150].
+
+ await waitForNextFrame();
+
+ // The timeline should be updated to scroller2.
+ assert_true(!!anim.timeline, 'Animation no longer has a timeline');
+ assert_equals(anim.timeline.source.id, 'B', 'Timeline not updated');
+ assert_equals(getComputedStyle(target).translate, '100px');
+}, 'Timeline lookup updates candidate when closer match available.');
+
+promise_test(async t => {
+ let target = createTarget(t);
+
+ // <div id='target'></div>
+ document.body.append(target);
+ target.style.animation = "anim 10s linear";
+ target.style.animationTimeline = 'timeline';
+
+ await waitForNextFrame();
+
+ // Timeline initially cannot be resolved, resulting in a null
+ // timeline. The animation's hold time is zero.
+ let anim = document.getAnimations()[0];
+ assert_equals(getComputedStyle(target).translate, '50px');
+
+ await waitForNextFrame();
+
+ let scroller = createScroller(t);
+ scroller.style.scrollTimelineName = 'timeline';
+
+ // <div class='scroller'> ... </div> (scroller1)
+ // <div id='target'></div>
+ document.body.insertBefore(scroller, target);
+
+ scroller.scrollTop = 50; // 50%, in [50, 150].
+
+ await waitForNextFrame();
+
+ // The timeline should be updated to scroller.
+ assert_equals(getComputedStyle(target).translate, '100px');
+}, 'Timeline lookup updates candidate when match becomes available.');
+
+// -------------------------
+// Test scroll-timeline-axis
+// -------------------------
+
+promise_test(async t => {
+ let [scroller, target] = createScrollerAndTarget(t);
+ scroller.style.writingMode = 'vertical-lr';
+
+ document.body.appendChild(scroller);
+ document.body.appendChild(target);
+
+ scroller.style.scrollTimeline = 'timeline block';
+ target.style.animation = "anim 10s linear";
+ target.style.animationTimeline = 'timeline';
+
+ scroller.scrollLeft = 50;
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(target).translate, '100px');
+}, 'scroll-timeline-axis is block');
+
+promise_test(async t => {
+ let [scroller, target] = createScrollerAndTarget(t);
+ scroller.style.writingMode = 'vertical-lr';
+
+ document.body.appendChild(scroller);
+ document.body.appendChild(target);
+
+ scroller.style.scrollTimeline = 'timeline inline';
+ target.style.animation = "anim 10s linear";
+ target.style.animationTimeline = 'timeline';
+
+ scroller.scrollTop = 50;
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(target).translate, '100px');
+}, 'scroll-timeline-axis is inline');
+
+promise_test(async t => {
+ let [scroller, target] = createScrollerAndTarget(t);
+ scroller.style.writingMode = 'vertical-lr';
+
+ document.body.appendChild(scroller);
+ document.body.appendChild(target);
+
+ scroller.style.scrollTimeline = 'timeline horizontal';
+ target.style.animation = "anim 10s linear";
+ target.style.animationTimeline = 'timeline';
+
+ scroller.scrollLeft = 50;
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(target).translate, '100px');
+}, 'scroll-timeline-axis is horizontal');
+
+promise_test(async t => {
+ let [scroller, target] = createScrollerAndTarget(t);
+ scroller.style.writingMode = 'vertical-lr';
+
+ document.body.appendChild(scroller);
+ document.body.appendChild(target);
+
+ scroller.style.scrollTimeline = 'timeline vertical';
+ target.style.animation = "anim 10s linear";
+ target.style.animationTimeline = 'timeline';
+
+ scroller.scrollTop = 50;
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(target).translate, '100px');
+}, 'scroll-timeline-axis is vertical');
+
+// TODO: Add more tests which change scroll-timeline-axis property.
+// Those animations which use this timeline should be restyled properly.
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/scroll-animations/css/animation-timeline-none.html b/testing/web-platform/tests/scroll-animations/css/animation-timeline-none.html
new file mode 100644
index 0000000000..a8e07a44d6
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/animation-timeline-none.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<link rel="help" src="https://drafts.csswg.org/css-animations-2/#animation-timeline">
+<link rel="help" src="https://drafts.csswg.org/web-animations/#playing-an-animation-section">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<style>
+ @keyframes expand {
+ from { width: 100px; }
+ to { width: 200px; }
+ }
+
+ .test {
+ width: 0px;
+ animation-name: expand;
+ animation-duration: 1s;
+ }
+
+ #element_timeline_none {
+ animation-timeline: none;
+ }
+ #element_unknown_timeline {
+ animation-timeline: unknown_timeline;
+ }
+
+</style>
+<div class=test id=element_timeline_none></div>
+<div class=test id=element_unknown_timeline></div>
+<script>
+ promise_test(async (t) => {
+ assert_equals(getComputedStyle(element_timeline_none).width, '100px');
+ await waitForAnimationFrames(3);
+ assert_equals(getComputedStyle(element_timeline_none).width, '100px');
+ }, 'Animation with animation-timeline:none holds current time at zero');
+
+ promise_test(async (t) => {
+ assert_equals(getComputedStyle(element_unknown_timeline).width, '100px');
+ await waitForAnimationFrames(3);
+ assert_equals(getComputedStyle(element_unknown_timeline).width, '100px');
+ }, 'Animation with unknown timeline name holds current time at zero');
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/animation-timeline-parsing.html b/testing/web-platform/tests/scroll-animations/css/animation-timeline-parsing.html
new file mode 100644
index 0000000000..1a30f11f85
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/animation-timeline-parsing.html
@@ -0,0 +1,88 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#animation-timeline">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/parsing-testcommon.js"></script>
+</head>
+<div id="target"></div>
+<script>
+test_valid_value('animation-timeline', 'initial');
+test_valid_value('animation-timeline', 'inherit');
+test_valid_value('animation-timeline', 'unset');
+test_valid_value('animation-timeline', 'revert');
+test_valid_value('animation-timeline', 'auto');
+test_valid_value('animation-timeline', 'none');
+test_valid_value('animation-timeline', 'auto, auto');
+test_valid_value('animation-timeline', 'none, none');
+test_valid_value('animation-timeline', 'auto, none');
+test_valid_value('animation-timeline', 'none, auto');
+test_valid_value('animation-timeline', '"test"', ["test", '"test"']);
+test_valid_value('animation-timeline', '"none"');
+test_valid_value('animation-timeline', '"auto"');
+test_valid_value('animation-timeline', '"initial"');
+test_valid_value('animation-timeline', '"inherit"');
+test_valid_value('animation-timeline', '"unset"');
+test_valid_value('animation-timeline', '"revert"');
+test_valid_value('animation-timeline', 'test');
+test_valid_value('animation-timeline', 'test1, test2');
+test_valid_value('animation-timeline', 'test1, "test2", none, test3, auto', ["test1, test2, none, test3, auto", 'test1, "test2", none, test3, auto']);
+
+test_invalid_value('animation-timeline', '10px');
+test_invalid_value('animation-timeline', 'auto auto');
+test_invalid_value('animation-timeline', 'none none');
+test_invalid_value('animation-timeline', 'foo bar');
+test_invalid_value('animation-timeline', '"foo" "bar"');
+test_invalid_value('animation-timeline', 'rgb(1, 2, 3)');
+test_invalid_value('animation-timeline', '#fefefe');
+
+// https://drafts.csswg.org/scroll-animations-1/#scroll-notation
+//
+// animation-timeline: scroll(<axis>? <scroller>?);
+// <axis> = block | inline | vertical | horizontal
+// <scroller> = root | nearest
+test_valid_value('animation-timeline', 'scroll()');
+test_valid_value('animation-timeline', 'scroll(block)', 'scroll()');
+test_valid_value('animation-timeline', 'scroll(inline)');
+test_valid_value('animation-timeline', 'scroll(horizontal)');
+test_valid_value('animation-timeline', 'scroll(vertical)');
+test_valid_value('animation-timeline', 'scroll(root)');
+test_valid_value('animation-timeline', 'scroll(nearest)', 'scroll()');
+test_valid_value('animation-timeline', 'scroll(inline nearest)', 'scroll(inline)');
+test_valid_value('animation-timeline', 'scroll(vertical root)');
+
+test_invalid_value('animation-timeline', 'scroll(root block)');
+test_invalid_value('animation-timeline', 'scroll(abc root)');
+test_invalid_value('animation-timeline', 'scroll(abc)');
+test_invalid_value('animation-timeline', 'scroll(vertical abc)');
+test_invalid_value('animation-timeline', 'scroll("string")');
+
+// https://drafts.csswg.org/scroll-animations-1/#view-notation
+test_valid_value('animation-timeline', 'view()');
+test_valid_value('animation-timeline', 'view(block)', 'view()');
+test_valid_value('animation-timeline', 'view(inline)');
+test_valid_value('animation-timeline', 'view(horizontal)');
+test_valid_value('animation-timeline', 'view(vertical)');
+test_valid_value('animation-timeline', 'view(vertical 1px 2px)');
+test_valid_value('animation-timeline', 'view(vertical 1px)');
+test_valid_value('animation-timeline', 'view(vertical auto)', 'view(vertical)');
+test_valid_value('animation-timeline', 'view(vertical auto auto)', 'view(vertical)');
+test_valid_value('animation-timeline', 'view(vertical auto 1px)');
+test_valid_value('animation-timeline', 'view(1px 2px vertical)', 'view(vertical 1px 2px)');
+test_valid_value('animation-timeline', 'view(1px vertical)', 'view(vertical 1px)');
+test_valid_value('animation-timeline', 'view(auto horizontal)', 'view(horizontal)');
+test_valid_value('animation-timeline', 'view(1px 2px)');
+test_valid_value('animation-timeline', 'view(1px)');
+test_valid_value('animation-timeline', 'view(1px 1px)', 'view(1px)');
+test_valid_value('animation-timeline', 'view(1px auto)');
+test_valid_value('animation-timeline', 'view(auto calc(1% + 1px))');
+test_valid_value('animation-timeline', 'view(auto)', 'view()');
+test_valid_value('animation-timeline', 'view(auto auto)', 'view()');
+
+test_invalid_value('animation-timeline', 'view(vertical 1px 2px 3px)');
+test_invalid_value('animation-timeline', 'view(1px vertical 3px)');
+test_invalid_value('animation-timeline', 'view(1px 2px 3px)');
+test_invalid_value('animation-timeline', 'view(abc block)');
+test_invalid_value('animation-timeline', 'view(abc)');
+test_invalid_value('animation-timeline', 'view(vertical abc)');
+test_invalid_value('animation-timeline', 'view("string")');
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/animation-timeline-scroll-functional-notation.tentative.html b/testing/web-platform/tests/scroll-animations/css/animation-timeline-scroll-functional-notation.tentative.html
new file mode 100644
index 0000000000..e28e85245e
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/animation-timeline-scroll-functional-notation.tentative.html
@@ -0,0 +1,148 @@
+<!DOCTYPE html>
+<title>The animation-timeline: scroll() notation</title>
+<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1">
+<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/rewrite#scroll-notation">
+<link rel="help" src="https://github.com/w3c/csswg-drafts/issues/6674">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<script src="support/testcommon.js"></script>
+<style>
+ @keyframes anim {
+ from { translate: 50px; }
+ to { translate: 150px; }
+ }
+ html {
+ min-height: 100vh;
+ /* This makes the max scrollable ragne be 100px in root element */
+ padding-bottom: 100px;
+ }
+ #container {
+ width: 300px;
+ height: 300px;
+ overflow: scroll;
+ }
+ #target {
+ width: 100px;
+ /* This makes the max scrollable ragne be 100px in the block direction */
+ height: 100px;
+ }
+ /* large block content */
+ .block-content {
+ block-size: 100%;
+ }
+ /* large inline content */
+ .inline-content {
+ inline-size: 100%;
+ block-size: 5px;
+ /* This makes the max scrollable ragne be 100px in the inline direction */
+ padding-inline-end: 100px;
+ }
+</style>
+<body>
+<div id="log"></div>
+<script>
+"use strict";
+
+setup(assert_implements_animation_timeline);
+
+const root = document.scrollingElement;
+const createTargetWithStuff = function(t, contentClass) {
+ let container = document.createElement('div');
+ container.id = 'container';
+ let target = document.createElement('div');
+ target.id = 'target';
+ let content = document.createElement('div');
+ content.className = contentClass;
+
+ // <div id='container'>
+ // <div id='target'></div>
+ // <div class=contentClass></div>
+ // </div>
+ document.body.appendChild(container);
+ container.appendChild(target);
+ container.appendChild(content);
+
+ if (t && typeof t.add_cleanup === 'function') {
+ t.add_cleanup(() => {
+ content.remove();
+ target.remove();
+ container.remove();
+ });
+ }
+
+ return [container, target];
+};
+
+async function scrollLeft(element, value) {
+ element.scrollLeft = value;
+ await waitForNextFrame();
+}
+
+async function scrollTop(element, value) {
+ element.scrollTop = value;
+ await waitForNextFrame();
+}
+
+promise_test(async t => {
+ let [container, div] = createTargetWithStuff(t, 'block-content');
+ div.style.animation = "anim 10s linear";
+ div.style.animationTimeline = "scroll(nearest)";
+
+ await scrollTop(root, 50);
+ assert_equals(getComputedStyle(div).translate, '50px');
+
+ await scrollTop(container, 50);
+ assert_equals(getComputedStyle(div).translate, '100px');
+
+ await scrollTop(root, 0);
+}, 'animation-timeline: scroll(nearest)');
+
+promise_test(async t => {
+ let [container, div] = createTargetWithStuff(t, 'block-content');
+ div.style.animation = "anim 10s linear";
+ div.style.animationTimeline = "scroll(root)";
+
+ await scrollTop(container, 50);
+ assert_equals(getComputedStyle(div).translate, '50px');
+
+ await scrollTop(root, 50);
+ assert_equals(getComputedStyle(div).translate, '100px');
+
+ await scrollTop(root, 0);
+}, 'animation-timeline: scroll(root)');
+
+promise_test(async t => {
+ let [container, div] = createTargetWithStuff(t, 'inline-content');
+ div.style.animation = "anim 10s linear";
+ div.style.animationTimeline = "scroll(inline)";
+
+ await scrollLeft(container, 50);
+ assert_equals(getComputedStyle(div).translate, '100px');
+}, 'animation-timeline: scroll(inline)');
+
+promise_test(async t => {
+ let [container, div] = createTargetWithStuff(t, 'block-content');
+ container.style.writingMode = 'vertical-lr';
+ div.style.animation = "anim 10s linear";
+ div.style.animationTimeline = "scroll(horizontal)";
+
+ await scrollLeft(container, 50);
+ assert_equals(getComputedStyle(div).translate, '100px');
+}, 'animation-timeline: scroll(horizontal)');
+
+promise_test(async t => {
+ let [container, div] = createTargetWithStuff(t, 'inline-content');
+ container.style.writingMode = 'vertical-lr';
+ div.style.animation = "anim 10s linear";
+ div.style.animationTimeline = "scroll(vertical)";
+
+ await scrollTop(container, 50);
+ assert_equals(getComputedStyle(div).translate, '100px');
+}, 'animation-timeline: scroll(vertical)');
+
+// TODO: Add more tests which change the overflow property of the container for
+// scroll(nearest)
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/scroll-animations/css/animation-timeline-view-functional-notation.tentative.html b/testing/web-platform/tests/scroll-animations/css/animation-timeline-view-functional-notation.tentative.html
new file mode 100644
index 0000000000..d93306819a
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/animation-timeline-view-functional-notation.tentative.html
@@ -0,0 +1,263 @@
+<!DOCTYPE html>
+<title>The animation-timeline: view() notation</title>
+<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1">
+<link rel="help" src="https://w3c.github.io/csswg-drafts/scroll-animations-1/#view-notation">
+<link rel="help" src="https://github.com/w3c/csswg-drafts/issues/7587">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<script src="support/testcommon.js"></script>
+<style>
+ @keyframes fade-in-out {
+ enter 0% { opacity: 0; }
+ enter 100% { opacity: 1; }
+ exit 0% { opacity: 1; }
+ exit 100% { opacity: 0; }
+ }
+ @keyframes fade-out {
+ exit 0% { opacity: 1; }
+ exit 100% { opacity: 0; }
+ }
+ @keyframes change-font-size {
+ exit 0% { font-size: 10px; }
+ exit 100% { font-size: 20px; }
+ }
+ #container {
+ width: 200px;
+ height: 200px;
+ overflow-y: scroll;
+ overflow-x: hidden;
+ }
+ .target {
+ width: 100px;
+ height: 100px;
+ background-color: red;
+ }
+ .content {
+ width: 400px;
+ height: 400px;
+ background-color: blue;
+ }
+</style>
+
+<body>
+<script>
+"use strict";
+
+setup(assert_implements_animation_timeline);
+
+const createTargetWithStuff = function(t, divClasses) {
+ let container = document.createElement('div');
+ container.id = 'container';
+ document.body.appendChild(container);
+
+ // *** When testing inset
+ // <div id='container'>
+ // <div class='content'></div>
+ // <div class='target'></div>
+ // <div class='content'></div>
+ // </div>
+ // *** When testing axis
+ // <div id='container'>
+ // <div class='target'></div>
+ // <div class='content'></div>
+ // </div>
+
+ let divs = [];
+ let target;
+ for(let className of divClasses) {
+ let div = document.createElement('div');
+ div.className = className;
+ container.appendChild(div);
+
+ divs.push(div);
+ if(className === 'target')
+ target = div;
+ }
+
+ if (t && typeof t.add_cleanup === 'function') {
+ t.add_cleanup(() => {
+ for(let div of divs)
+ div.remove();
+ container.remove();
+ });
+ }
+
+ return [container, target];
+};
+
+async function scrollLeft(element, value) {
+ element.scrollLeft = value;
+ await waitForNextFrame();
+}
+
+async function scrollTop(element, value) {
+ element.scrollTop = value;
+ await waitForNextFrame();
+}
+
+promise_test(async t => {
+ let [container, div] = createTargetWithStuff(t, ['content', 'target', 'content']);
+ div.style.animation = "fade-in-out 1s linear";
+ div.style.animationTimeline = "view()";
+
+ await scrollTop(container, 200);
+ assert_equals(getComputedStyle(div).opacity, '0', 'At enter 0%');
+ await scrollTop(container, 250);
+ assert_equals(getComputedStyle(div).opacity, '0.5', 'At enter 50%');
+ await scrollTop(container, 300);
+ assert_equals(getComputedStyle(div).opacity, '1', 'At enter 100%');
+
+ await scrollTop(container, 400);
+ assert_equals(getComputedStyle(div).opacity, '1', 'At exit 0%');
+ await scrollTop(container, 450);
+ assert_equals(getComputedStyle(div).opacity, '0.5', 'At exit 50%');
+ await scrollTop(container, 500);
+ assert_equals(getComputedStyle(div).opacity, '0', 'At exit 100%');
+}, 'animation-timeline: view()');
+
+promise_test(async t => {
+ let [container, div] = createTargetWithStuff(t, ['content', 'target', 'content']);
+ div.style.animation = "fade-in-out 1s linear";
+ div.style.animationTimeline = "view(50px)";
+
+ await scrollTop(container, 250);
+ assert_equals(getComputedStyle(div).opacity, '0', 'At enter 0%');
+ await scrollTop(container, 300);
+ assert_equals(getComputedStyle(div).opacity, '0.5', 'At enter 50%');
+
+ await scrollTop(container, 350);
+ assert_equals(getComputedStyle(div).opacity, '1', 'At enter 100% & exit 0%');
+
+ await scrollTop(container, 400);
+ assert_equals(getComputedStyle(div).opacity, '0.5', 'At exit 50%');
+ await scrollTop(container, 450);
+ assert_equals(getComputedStyle(div).opacity, '0', 'At exit 100%');
+}, 'animation-timeline: view(50px)');
+
+promise_test(async t => {
+ let [container, div] = createTargetWithStuff(t, ['content', 'target', 'content']);
+ div.style.animation = "fade-in-out 1s linear";
+ div.style.animationTimeline = "view(auto 50px)";
+
+ await scrollTop(container, 250);
+ assert_equals(getComputedStyle(div).opacity, '0', 'At enter 0%');
+ await scrollTop(container, 300);
+ assert_equals(getComputedStyle(div).opacity, '0.5', 'At enter 50%');
+ await scrollTop(container, 350);
+ assert_equals(getComputedStyle(div).opacity, '1', 'At enter 100%');
+
+ await scrollTop(container, 400);
+ assert_equals(getComputedStyle(div).opacity, '1', 'At exit 0%');
+ await scrollTop(container, 450);
+ assert_equals(getComputedStyle(div).opacity, '0.5', 'At exit 50%');
+ await scrollTop(container, 500);
+ assert_equals(getComputedStyle(div).opacity, '0', 'At exit 100%');
+}, 'animation-timeline: view(auto 50px)');
+
+promise_test(async t => {
+ let [container, div] = createTargetWithStuff(t, ['target', 'content']);
+ container.style.overflow = 'scroll';
+ div.style.animation = "fade-out 1s linear";
+ div.style.animationTimeline = "view(inline)";
+
+ await scrollLeft(container, 0);
+ assert_equals(getComputedStyle(div).opacity, '1', 'At exit 0%');
+ await scrollLeft(container, 50);
+ assert_equals(getComputedStyle(div).opacity, '0.5', 'At exit 50%');
+ await scrollLeft(container, 100);
+ assert_equals(getComputedStyle(div).opacity, '0', 'At exit 100%');
+}, 'animation-timeline: view(inline)');
+
+promise_test(async t => {
+ let [container, div] = createTargetWithStuff(t, ['target', 'content']);
+ container.style.overflow = 'scroll';
+ div.style.animation = "fade-out 1s linear";
+ div.style.animationTimeline = "view(horizontal)";
+
+ await scrollLeft(container, 0);
+ assert_equals(getComputedStyle(div).opacity, '1', 'At exit 0%');
+ await scrollLeft(container, 50);
+ assert_equals(getComputedStyle(div).opacity, '0.5', 'At exit 50%');
+ await scrollLeft(container, 100);
+ assert_equals(getComputedStyle(div).opacity, '0', 'At exit 100%');
+}, 'animation-timeline: view(horizontal)');
+
+promise_test(async t => {
+ let [container, div] = createTargetWithStuff(t, ['target', 'content']);
+ container.style.overflow = 'scroll';
+ div.style.animation = "fade-out 1s linear";
+ div.style.animationTimeline = "view(vertical)";
+
+ await scrollTop(container, 0);
+ assert_equals(getComputedStyle(div).opacity, '1', 'At exit 0%');
+ await scrollTop(container, 50);
+ assert_equals(getComputedStyle(div).opacity, '0.5', 'At exit 50%');
+ await scrollTop(container, 100);
+ assert_equals(getComputedStyle(div).opacity, '0', 'At exit 100%');
+}, 'animation-timeline: view(vertical)');
+
+promise_test(async t => {
+ let [container, div] = createTargetWithStuff(t, ['target', 'content']);
+ container.style.overflowY = 'hidden';
+ container.style.overflowX = 'scroll';
+ div.style.animation = "fade-out 1s linear";
+ div.style.animationTimeline = "view(horizontal 50px)";
+
+ await scrollLeft(container, 0);
+ assert_equals(getComputedStyle(div).opacity, '0.5', 'At exit 50%');
+ await scrollLeft(container, 50);
+ assert_equals(getComputedStyle(div).opacity, '0', 'At exit 100%');
+}, 'animation-timeline: view(horizontal 50px)');
+
+promise_test(async t => {
+ let [container, div] = createTargetWithStuff(t, ['target', 'content']);
+ container.style.overflow = 'scroll';
+ div.style.animation = "fade-out 1s linear, change-font-size 1s linear";
+ div.style.animationTimeline = "view(), view(inline)";
+
+ await scrollLeft(container, 0);
+ assert_equals(getComputedStyle(div).fontSize, '10px', 'At exit 0% inline');
+ await scrollLeft(container, 50);
+ assert_equals(getComputedStyle(div).fontSize, '15px', 'At exit 50% inline');
+ await scrollLeft(container, 100);
+ assert_equals(getComputedStyle(div).fontSize, '20px', 'At exit 100% inline');
+
+ await scrollLeft(container, 0);
+
+ await scrollTop(container, 0);
+ assert_equals(getComputedStyle(div).opacity, '1', 'At exit 0% block');
+ await scrollTop(container, 50);
+ assert_equals(getComputedStyle(div).opacity, '0.5', 'At exit 50% block');
+ await scrollTop(container, 100);
+ assert_equals(getComputedStyle(div).opacity, '0', 'At exit 100% block');
+
+ await scrollLeft(container, 50);
+ await scrollTop(container, 50);
+ assert_equals(getComputedStyle(div).fontSize, '15px', 'At exit 50% inline');
+ assert_equals(getComputedStyle(div).opacity, '0.5', 'At exit 50% block');
+}, 'animation-timeline: view(), view(inline)');
+
+promise_test(async t => {
+ let [container, div] = createTargetWithStuff(t, ['target', 'content']);
+ container.style.overflowY = 'hidden';
+ container.style.overflowX = 'scroll';
+ div.style.animation = "fade-out 1s linear";
+
+ div.style.animationTimeline = "view(inline)";
+ await scrollLeft(container, 0);
+ assert_equals(getComputedStyle(div).opacity, '1', 'At exit 0%');
+ await scrollLeft(container, 50);
+ assert_equals(getComputedStyle(div).opacity, '0.5', 'At exit 50%');
+ await scrollLeft(container, 100);
+ assert_equals(getComputedStyle(div).opacity, '0', 'At exit 100%');
+
+ div.style.animationTimeline = "view(inline 50px)";
+ await scrollLeft(container, 0);
+ assert_equals(getComputedStyle(div).opacity, '0.5', 'At exit 50%');
+ await scrollLeft(container, 50);
+ assert_equals(getComputedStyle(div).opacity, '0', 'At exit 100%');
+}, 'animation-timeline: view(inline) changes to view(inline 50px)');
+
+</script>
+</body> \ No newline at end of file
diff --git a/testing/web-platform/tests/scroll-animations/css/get-animations-inactive-timeline.html b/testing/web-platform/tests/scroll-animations/css/get-animations-inactive-timeline.html
new file mode 100644
index 0000000000..10bf00fbbc
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/get-animations-inactive-timeline.html
@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<html>
+<meta charset="utf-8">
+<title>getAnimations for scroll-linked animations</title>
+<link rel="help"
+ href="https://www.w3.org/TR/web-animations-1/#animation-effect-phases-and-states">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="support/testcommon.js"></script>
+<style>
+ @keyframes slide {
+ from { transform: translateX(100px); }
+ to { transform: translateX(100px); }
+ }
+
+ #container {
+ border: 10px solid lightgray;
+ overflow-x: scroll;
+ height: 200px;
+ width: 200px;
+ scroll-timeline-name: timeline;
+ }
+ #spacer {
+ height: 200vh;
+ }
+ #target {
+ background-color: green;
+ height: 100px;
+ width: 100px;
+ animation: slide 1s linear;
+ animation-timeline: timeline;
+ }
+</style>
+<body>
+ <div id="container">
+ <div id="spacer"></div>
+ <div id="target"></div>
+ </div>
+</body>
+<script type="text/javascript">
+ setup(assert_implements_animation_timeline);
+
+ promise_test(async t => {
+ // Newly created timeline is inactive,
+ let animations = document.getAnimations();
+ assert_equals(animations.length, 1,
+ 'Single running animation');
+ assert_true(animations[0].timeline instanceof ScrollTimeline,
+ 'Animation associated with a scroll timeline');
+ assert_equals(animations[0].timeline.currentTime, null,
+ 'Timeline is initially inactive');
+
+ // Canceled animation is no longer current.
+ const anim = animations[0];
+ animations[0].cancel();
+
+ assert_equals(
+ document.getAnimations().length, 0,
+ 'A canceled animation is no longer returned by getAnimations');
+
+ // Replaying an animation makes it current.
+ anim.play();
+ assert_equals(
+ document.getAnimations().length, 1,
+ 'A play-pending animation is return by getAnimations');
+
+ // Animation effect is still current even if the timeline's source element
+ // cannot be scrolled.
+ spacer.style = 'display: none';
+ t.add_cleanup(() => {
+ spacer.style = '';
+ });
+
+ animations = document.getAnimations();
+ assert_equals(
+ animations.length, 1,
+ 'Running animation is included in getAnimations list even if ' +
+ 'currentTime is null');
+ assert_true(animations[0].timeline instanceof ScrollTimeline,
+ 'Animation has timeline associated with an element that ' +
+ 'cannot be scrolled');
+ assert_equals(animations[0].timeline.currentTime, null,
+ 'Inactive timeline when timeline\'s source element cannot ' +
+ 'be scrolled');
+ }, 'getAnimations includes inactive scroll-linked animations that have not ' +
+ 'been canceled');
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/named-range-keyframes-with-document-timeline.tentative.html b/testing/web-platform/tests/scroll-animations/css/named-range-keyframes-with-document-timeline.tentative.html
new file mode 100644
index 0000000000..a0094d3220
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/named-range-keyframes-with-document-timeline.tentative.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<html>
+<meta charset="utf-8">
+<title>Named range keyframe offset when you have a document timeline</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<script src="support/testcommon.js"></script>
+<style>
+ @keyframes fade-in-animation {
+ from { opacity: 0 }
+
+ enter 0% { opacity: 0 }
+ enter 100% { opacity: 1 }
+ exit 0% { opacity: 1 }
+ exit 100% { opacity: 0 }
+
+ to { opacity: 1 }
+ }
+
+ #subject {
+ background-color: blue;
+ height: 200px;
+ width: 200px;
+ animation: linear both fade-in-animation;
+ animation-duration: 0.1s;
+ animation-play-state: paused;
+ }
+</style>
+<body onload="runTests()">
+ <div id="subject"></div>
+</body>
+
+<script type="text/javascript">
+ setup(assert_implements_animation_timeline);
+
+ function runTests() {
+ promise_test(async t => {
+ const anim = subject.getAnimations()[0];
+ anim.currentTime = -1;
+ assert_equals(getComputedStyle(subject).opacity, "0",
+ 'unexpected value in the before phase');
+
+ anim.currentTime = 50;
+ assert_equals(getComputedStyle(subject).opacity, "0.5",
+ 'unexpected value in the middle of the animation');
+
+ anim.currentTime = 100;
+ assert_equals(getComputedStyle(subject).opacity, "1",
+ 'unexpected value in the after phase');
+ });
+ }
+</script>
+</html>
diff --git a/testing/web-platform/tests/scroll-animations/css/printing/animation-timeline-none-with-progress-print.tentative.html b/testing/web-platform/tests/scroll-animations/css/printing/animation-timeline-none-with-progress-print.tentative.html
new file mode 100644
index 0000000000..3939a1df48
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/printing/animation-timeline-none-with-progress-print.tentative.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<title>The animation-timeline:none with preserved progress for print</title>
+<link rel="help" href="https://drafts.csswg.org/css-animations-2/#animation-timeline">
+<meta name="assert" content="print correctly for an animation with animation-timeline:none with preserved progress">
+<link rel="match" href="animation-timeline-none-with-progress-ref.html">
+
+<style>
+ @keyframes anim {
+ from { transform: translateX(0px); }
+ to { transform: translateX(100px); }
+ }
+
+ #scroller {
+ scroll-timeline: timeline;
+ overflow: scroll;
+ width: 100px;
+ height: 100px;
+ scrollbar-width: none;
+ }
+
+ #contents {
+ height: 200px;
+ }
+
+ #box {
+ width: 100px;
+ height: 100px;
+ background-color: green;
+ animation: anim 1s linear timeline;
+ }
+</style>
+
+<div id="scroller">
+ <div id="contents"></div>
+</div>
+<div id="box"></div>
+
+<script>
+ window.addEventListener('load', function() {
+ const scroller = document.getElementById("scroller");
+ // Move the scroller to the halfway point.
+ const maxScroll = scroller.scrollHeight - scroller.clientHeight;
+ scroller.scrollTop = 0.5 * maxScroll;
+
+ window.requestAnimationFrame(() => {
+ let box = document.getElementById("box");
+ box.style.animationTimeline = "none";
+ getComputedStyle(box).marginLeft;
+
+ window.requestAnimationFrame(() => {
+ document.documentElement.classList.remove("reftest-wait");
+ });
+ });
+ });
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/printing/animation-timeline-none-with-progress-ref.html b/testing/web-platform/tests/scroll-animations/css/printing/animation-timeline-none-with-progress-ref.html
new file mode 100644
index 0000000000..09bcba2fd4
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/printing/animation-timeline-none-with-progress-ref.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<title>Reference for none animation-timeline</title>
+<style>
+ #scroller {
+ overflow: scroll;
+ width: 100px;
+ height: 100px;
+ scrollbar-width: none;
+ }
+
+ #contents {
+ height: 200px;
+ }
+
+ #box {
+ width: 100px;
+ height: 100px;
+ background-color: green;
+ transform: translateX(50px);
+ }
+</style>
+
+<div id="scroller">
+ <div id="contents"></div>
+</div>
+<div id="box"></div>
+
+<script>
+ window.addEventListener('load', function() {
+ const scroller = document.getElementById("scroller");
+ // Move the scroller to the halfway point.
+ const maxScroll = scroller.scrollHeight - scroller.clientHeight;
+ scroller.scrollTop = 0.5 * maxScroll;
+
+ document.documentElement.classList.remove("reftest-wait");
+ });
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/printing/scroll-timeline-default-iframe-print.html b/testing/web-platform/tests/scroll-animations/css/printing/scroll-timeline-default-iframe-print.html
new file mode 100644
index 0000000000..d732ca141a
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/printing/scroll-timeline-default-iframe-print.html
@@ -0,0 +1,65 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<title>The default scroll() timeline in the iframe for print</title>
+<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#scroll-notation">
+<link rel="help" href="https://drafts.csswg.org/css-animations-2/#animation-timeline">
+<meta name="assert" content="CSS animation correctly updates values when using the default scroll() timeline">
+<link rel="match" href="../scroll-timeline-default-iframe-ref.html">
+<meta name="fuzzy" content="25;100">
+
+<iframe id="target" width="400" height="400" srcdoc='
+ <html>
+ <style>
+ @keyframes update {
+ from { transform: translateY(0px); }
+ to { transform: translateY(200px); }
+ }
+ html {
+ min-height: 100%;
+ padding-bottom: 100px;
+ }
+ #box {
+ width: 100px;
+ height: 100px;
+ background-color: green;
+ animation: update 1s linear;
+ animation-timeline: scroll();
+ }
+ #covered {
+ width: 100px;
+ height: 100px;
+ background-color: red;
+ }
+
+ * {
+ margin-top: 0px;
+ margin-bottom: 0px;
+ }
+ </style>
+ <script>
+ window.addEventListener("load", function() {
+ const scroller = document.scrollingElement;
+
+ // Move the scroller to the halfway point.
+ const maxScroll = scroller.scrollHeight - scroller.clientHeight;
+ scroller.scrollTop = 0.5 * maxScroll;
+
+ window.requestAnimationFrame(() => {
+ window.parent.postMessage("ready", "*");
+ });
+ });
+ </script>
+ <body>
+ <div id="box"></div>
+ <div id="covered"></div>
+ </body>
+ </html>
+'></iframe>
+
+<script>
+ window.addEventListener("message", event => {
+ if (event.data == "ready") {
+ document.documentElement.classList.remove("reftest-wait");
+ }
+ }, false);
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/printing/scroll-timeline-default-print.tentative.html b/testing/web-platform/tests/scroll-animations/css/printing/scroll-timeline-default-print.tentative.html
new file mode 100644
index 0000000000..975a59812e
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/printing/scroll-timeline-default-print.tentative.html
@@ -0,0 +1,62 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<title>The default scroll() timeline for print</title>
+<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#scroll-notation">
+<link rel="help" href="https://drafts.csswg.org/css-animations-2/#animation-timeline">
+<meta name="assert" content="CSS animation correctly updates values when using the default scroll() timeline">
+<link rel="match" href="../scroll-timeline-default-ref.html">
+
+<style>
+ @keyframes update {
+ from { transform: translateY(0px); }
+ to { transform: translateY(200px); }
+ }
+
+ html {
+ min-height: 100%;
+ padding-bottom: 100px;
+ }
+
+ #box {
+ width: 100px;
+ height: 100px;
+ background-color: green;
+ animation: update 1s linear;
+ animation-timeline: scroll();
+ }
+
+ #covered {
+ width: 100px;
+ height: 100px;
+ background-color: red;
+ }
+
+ * {
+ margin-top: 0px;
+ margin-bottom: 0px;
+ }
+</style>
+
+<div id="box"></div>
+<div id="covered"></div>
+<script src="/web-animations/testcommon.js"></script>
+<script>
+ document.documentElement.addEventListener('TestRendered', async () => {
+ runTest();
+ }, { once: true });
+
+ async function runTest() {
+ const scroller = document.scrollingElement;
+
+ await waitForCompositorReady();
+
+ // Move the scroller to the halfway point.
+ const maxScroll = scroller.scrollHeight - scroller.clientHeight;
+ scroller.scrollTop = 0.5 * maxScroll;
+
+ await waitForNextFrame();
+ await waitForNextFrame();
+
+ document.documentElement.classList.remove("reftest-wait");
+ }
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/printing/scroll-timeline-specified-scroller-print.html b/testing/web-platform/tests/scroll-animations/css/printing/scroll-timeline-specified-scroller-print.html
new file mode 100644
index 0000000000..7e5b6607b6
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/printing/scroll-timeline-specified-scroller-print.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<title>A scroll timeline with a specified scroller for print</title>
+<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#scroll-timelines">
+<link rel="help" href="https://drafts.csswg.org/css-animations-2/#animation-timeline">
+<meta name="assert" content="CSS animation correctly updates values when using a specified scroller">
+<link rel="match" href="scroll-timeline-specified-scroller-ref.html">
+
+<style>
+ @keyframes anim {
+ from { transform: translateX(0px); }
+ to { transform: translateX(100px); }
+ }
+
+ #scroller {
+ scroll-timeline: timeline;
+ overflow: scroll;
+ width: 100px;
+ height: 100px;
+ scrollbar-width: none;
+ }
+
+ #contents {
+ height: 200px;
+ }
+
+ #box {
+ width: 100px;
+ height: 100px;
+ background-color: green;
+ animation: anim 1s linear;
+ animation-timeline: timeline;
+ }
+
+ @supports not (animation-timeline:timeline) {
+ #box {
+ animation-play-state: paused;
+ }
+ }
+</style>
+
+<div id="scroller">
+ <div id="contents"></div>
+</div>
+<div id="box"></div>
+
+<script>
+ window.addEventListener('load', function() {
+ const scroller = document.getElementById("scroller");
+ // Move the scroller to the halfway point.
+ const maxScroll = scroller.scrollHeight - scroller.clientHeight;
+ scroller.scrollTop = 0.5 * maxScroll;
+
+ window.requestAnimationFrame(() => {
+ document.documentElement.classList.remove("reftest-wait");
+ });
+ });
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/printing/scroll-timeline-specified-scroller-ref.html b/testing/web-platform/tests/scroll-animations/css/printing/scroll-timeline-specified-scroller-ref.html
new file mode 100644
index 0000000000..9074f2454b
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/printing/scroll-timeline-specified-scroller-ref.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<title>Reference for scroll timeline with a specified scroller</title>
+<style>
+ #scroller {
+ overflow: scroll;
+ width: 100px;
+ height: 100px;
+ scrollbar-width: none;
+ }
+
+ #contents {
+ height: 200px;
+ }
+
+ #box {
+ width: 100px;
+ height: 100px;
+ background-color: green;
+ transform: translateX(50px);
+ }
+</style>
+
+<div id="scroller">
+ <div id="contents"></div>
+</div>
+<div id="box"></div>
+
+<script>
+ window.addEventListener('load', function() {
+ const scroller = document.getElementById("scroller");
+ // Move the scroller to the halfway point.
+ const maxScroll = scroller.scrollHeight - scroller.clientHeight;
+ scroller.scrollTop = 0.5 * maxScroll;
+
+ document.documentElement.classList.remove("reftest-wait");
+ });
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/progress-based-animation-animation-longhand-properties.tentative.html b/testing/web-platform/tests/scroll-animations/css/progress-based-animation-animation-longhand-properties.tentative.html
new file mode 100644
index 0000000000..f4f9a669f3
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/progress-based-animation-animation-longhand-properties.tentative.html
@@ -0,0 +1,255 @@
+<!DOCTYPE html>
+<title>The various animation longhands with progress based animations</title>
+<link rel="help" src="https://drafts.csswg.org/css-animations-2">
+<link rel="help" src="https://github.com/w3c/csswg-drafts/issues/4862">
+<link rel="help" src="https://github.com/w3c/csswg-drafts/issues/6674">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<script src="support/testcommon.js"></script>
+<style>
+ @keyframes anim {
+ from { translate: 0px; }
+ to { translate: 100px; }
+ }
+ #container {
+ width: 300px;
+ height: 300px;
+ overflow: scroll;
+ }
+ #target {
+ width: 100px;
+ height: 100px;
+ translate: none;
+ }
+</style>
+<body>
+<div id="log"></div>
+<script>
+"use strict";
+
+setup(assert_implements_animation_timeline);
+
+const createTargetAndScroller = function(t) {
+ let container = document.createElement('div');
+ container.id = 'container';
+ let target = document.createElement('div');
+ target.id = 'target';
+ let content = document.createElement('div');
+ content.style.blockSize = '100%';
+
+ // The height of target is 100px and the content is 100%, so the scroll range
+ // is [0, 100].
+
+ // <div id='container'>
+ // <div id='target'></div>
+ // <div style='block-size: 100%;'></div>
+ // </div>
+ document.body.appendChild(container);
+ container.appendChild(target);
+ container.appendChild(content);
+
+ if (t && typeof t.add_cleanup === 'function') {
+ t.add_cleanup(() => {
+ content.remove();
+ target.remove();
+ container.remove();
+ });
+ }
+
+ return [target, container];
+};
+
+async function scrollTop(element, value) {
+ element.scrollTop = value;
+ await waitForNextFrame();
+}
+
+// ------------------------------
+// Test animation-duration
+// ------------------------------
+
+promise_test(async t => {
+ let [target, scroller] = createTargetAndScroller(t);
+ target.style.animation = '10s linear anim';
+ target.style.animationTimeline = 'scroll(nearest)';
+
+ await scrollTop(scroller, 25); // [0, 100].
+ assert_equals(getComputedStyle(target).translate, '25px');
+}, 'animation-duration');
+
+promise_test(async t => {
+ let [target, scroller] = createTargetAndScroller(t);
+ target.style.animation = '0s linear anim forwards';
+ target.style.animationTimeline = 'scroll(nearest)';
+
+ await scrollTop(scroller, 25); // [0, 100].
+ assert_equals(getComputedStyle(target).translate, '100px');
+}, 'animation-duration: 0s');
+
+
+// ------------------------------
+// Test animation-iteration-count
+// ------------------------------
+
+promise_test(async t => {
+ let [target, scroller] = createTargetAndScroller(t);
+ target.style.animation = '10s linear anim';
+ target.style.animationTimeline = 'scroll(nearest)';
+
+ await scrollTop(scroller, 25); // [0, 100].
+ assert_equals(getComputedStyle(target).translate, '25px');
+
+ // Let animation become 50% in the 1st iteration.
+ target.style.animationIterationCount = '2';
+ await waitForCSSScrollTimelineStyle();
+ assert_equals(getComputedStyle(target).translate, '50px');
+
+ // Let animation become 0% in the 2nd iteration.
+ target.style.animationIterationCount = '4';
+ await waitForCSSScrollTimelineStyle();
+ assert_equals(getComputedStyle(target).translate, '0px');
+}, 'animation-iteration-count');
+
+promise_test(async t => {
+ let [target, scroller] = createTargetAndScroller(t);
+ target.style.animation = '10s linear anim forwards';
+ target.style.animationTimeline = 'scroll(nearest)';
+ target.style.animationIterationCount = '0';
+
+ await scrollTop(scroller, 25); // [0, 100].
+ assert_equals(getComputedStyle(target).translate, '0px');
+}, 'animation-iteration-count: 0');
+
+promise_test(async t => {
+ let [target, scroller] = createTargetAndScroller(t);
+ target.style.animation = '10s linear anim forwards';
+ target.style.animationTimeline = 'scroll(nearest)';
+ target.style.animationIterationCount = 'infinite';
+
+ await scrollTop(scroller, 25); // [0, 100].
+ assert_equals(getComputedStyle(target).translate, '100px');
+}, 'animation-iteration-count: infinite');
+
+
+// ------------------------------
+// Test animation-direction
+// ------------------------------
+
+promise_test(async t => {
+ let [target, scroller] = createTargetAndScroller(t);
+ target.style.animation = '10s linear anim';
+ target.style.animationTimeline = 'scroll(nearest)';
+
+ await scrollTop(scroller, 25) // [0, 100].
+ assert_equals(getComputedStyle(target).translate, '25px');
+}, 'animation-direction: normal');
+
+promise_test(async t => {
+ let [target, scroller] = createTargetAndScroller(t);
+ target.style.animation = '10s linear anim';
+ target.style.animationTimeline = 'scroll(nearest)';
+ target.style.animationDirection = 'reverse';
+
+ await scrollTop(scroller, 25); // 25% in the reversing direction.
+ assert_equals(getComputedStyle(target).translate, '75px');
+}, 'animation-direction: reverse');
+
+promise_test(async t => {
+ let [target, scroller] = createTargetAndScroller(t);
+ target.style.animation = '10s linear anim';
+ target.style.animationTimeline = 'scroll(nearest)';
+ target.style.animationIterationCount = '2';
+ target.style.animationDirection = 'alternate';
+
+ await scrollTop(scroller, 10); // 20% in the 1st iteration.
+ assert_equals(getComputedStyle(target).translate, '20px');
+
+ await scrollTop(scroller, 60); // 20% in the 2nd iteration (reversing direction).
+ assert_equals(getComputedStyle(target).translate, '80px');
+}, 'animation-direction: alternate');
+
+promise_test(async t => {
+ let [target, scroller] = createTargetAndScroller(t);
+ target.style.animation = '10s linear anim';
+ target.style.animationTimeline = 'scroll(nearest)';
+ target.style.animationIterationCount = '2';
+ target.style.animationDirection = 'alternate-reverse';
+
+ await scrollTop(scroller, 10); // 20% in the 1st iteration (reversing direction).
+ assert_equals(getComputedStyle(target).translate, '80px');
+
+ await scrollTop(scroller, 60); // 20% in the 2nd iteration.
+ assert_equals(getComputedStyle(target).translate, '20px');
+}, 'animation-direction: alternate-reverse');
+
+
+// ------------------------------
+// Test animation-delay
+// ------------------------------
+
+promise_test(async t => {
+ let [target, scroller] = createTargetAndScroller(t);
+ target.style.animation = '10s linear anim';
+ target.style.animationTimeline = 'scroll(nearest)';
+
+ await scrollTop(scroller, 25); // [0, 100].
+ assert_equals(getComputedStyle(target).translate, '25px');
+
+ // (start delay: 10s) (duration: 10s)
+ // before active
+ // |--------------------|--------------------|
+ // 0px 50px 100px (The scroller)
+ // 0% 100% (The iteration progress)
+
+ // Let animation be in before phase.
+ target.style.animationDelay = '10s';
+ target.style.animationDelayStart = '10s'; // crbug.com/1375994
+ assert_equals(getComputedStyle(target).translate, 'none');
+
+ await scrollTop(scroller, 50); // The animation enters active phase.
+ assert_equals(getComputedStyle(target).translate, '0px');
+
+ await scrollTop(scroller, 75); // The ieration progress is 50%.
+ assert_equals(getComputedStyle(target).translate, '50px');
+}, 'animation-delay with a positive value');
+
+promise_test(async t => {
+ let [target, scroller] = createTargetAndScroller(t);
+ target.style.animation = '10s linear anim';
+ target.style.animationTimeline = 'scroll(nearest)';
+
+ // active
+ // |--------------------|
+ // 0px 100px (The scroller)
+ // 50% 100% (The iteration progress)
+
+ await scrollTop(scroller, 20); // [0, 100].
+ target.style.animationDelay = '-5s';
+ target.style.animationDelayStart = '-5s'; // crbug.com/1375994
+ await waitForCSSScrollTimelineStyle();
+ assert_equals(getComputedStyle(target).translate, '60px');
+}, 'animation-delay with a negative value');
+
+
+// ------------------------------
+// Test animation-fill-mode
+// ------------------------------
+
+promise_test(async t => {
+ let [target, scroller] = createTargetAndScroller(t);
+ target.style.animation = '10s linear anim';
+ target.style.animationTimeline = 'scroll(nearest)';
+ target.style.animationDelay = '10s';
+ target.style.animationDelayStart = '10s'; // crbug.com/1375994
+
+ await scrollTop(scroller, 25);
+ assert_equals(getComputedStyle(target).translate, 'none');
+
+ target.style.animationFillMode = 'backwards';
+ await waitForCSSScrollTimelineStyle();
+ assert_equals(getComputedStyle(target).translate, '0px');
+}, 'animation-fill-mode');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/scroll-animations/css/progress-based-animation-timeline.html b/testing/web-platform/tests/scroll-animations/css/progress-based-animation-timeline.html
new file mode 100644
index 0000000000..2e910cbe51
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/progress-based-animation-timeline.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<title>CSS Animation using progress based timeline</title>
+<link rel="help" src="https://drafts.csswg.org/css-animations-2/#animation-timeline">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<style>
+ main > div {
+ overflow: hidden;
+ width: 100px;
+ height: 100px;
+ }
+ main > div > div {
+ height: 200px;
+ }
+
+ @keyframes top {
+ from { top: 100px; }
+ to { top: 200px; }
+ }
+
+ #scroller1 {
+ scroll-timeline: top_timeline;
+ }
+
+ #element {
+ animation-name: top;
+ animation-duration: 10s;
+ animation-timing-function: linear;
+ animation-timeline: top_timeline;
+ }
+ /* Ensure stable expectations if feature is not supported */
+ @supports not (animation-timeline:foo) {
+ #element { animation-play-state: paused; }
+ }
+</style>
+<main>
+ <div id=scroller1><div></div></div>
+ <div id=element></div>
+</main>
+<script>
+ window.onload = async () => {
+ promise_test(async (t) => {
+ await waitForNextFrame();
+ const anim = document.getAnimations()[0];
+ await anim.ready;
+ scroller1.scrollTop = 20;
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(element).top, '120px');
+ }, 'progress based animation timeline works');
+ };
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-axis-computed.tentative.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-axis-computed.tentative.html
new file mode 100644
index 0000000000..8e23968b8e
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-axis-computed.tentative.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/6674">
+<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/rewrite#scroll-timeline-axis">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/computed-testcommon.js"></script>
+</head>
+<style>
+ #outer { scroll-timeline-axis: inline; }
+ #target { scroll-timeline-axis: vertical; }
+</style>
+<div id="outer">
+ <div id="target"></div>
+</div>
+<script>
+test_computed_value('scroll-timeline-axis', 'initial', 'block');
+test_computed_value('scroll-timeline-axis', 'inherit', 'inline');
+test_computed_value('scroll-timeline-axis', 'unset', 'block');
+test_computed_value('scroll-timeline-axis', 'revert', 'block');
+test_computed_value('scroll-timeline-axis', 'block');
+test_computed_value('scroll-timeline-axis', 'inline');
+test_computed_value('scroll-timeline-axis', 'vertical');
+test_computed_value('scroll-timeline-axis', 'horizontal');
+
+test(() => {
+ let style = getComputedStyle(document.getElementById('target'));
+ assert_not_equals(Array.from(style).indexOf('scroll-timeline-axis'), -1);
+}, 'The scroll-timeline-axis property shows up in CSSStyleDeclaration enumeration');
+
+test(() => {
+ let style = document.getElementById('target').style;
+ assert_not_equals(style.cssText.indexOf('scroll-timeline-axis'), -1);
+}, 'The scroll-timeline-axis property shows up in CSSStyleDeclaration.cssText');
+
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-axis-parsing.tentative.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-axis-parsing.tentative.html
new file mode 100644
index 0000000000..81be07e38f
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-axis-parsing.tentative.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/6674">
+<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/rewrite#scroll-timeline-axis">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/parsing-testcommon.js"></script>
+</head>
+<div id="target"></div>
+
+<script>
+
+test_valid_value('scroll-timeline-axis', 'initial');
+test_valid_value('scroll-timeline-axis', 'inherit');
+test_valid_value('scroll-timeline-axis', 'unset');
+test_valid_value('scroll-timeline-axis', 'revert');
+
+test_valid_value('scroll-timeline-axis', 'block');
+test_valid_value('scroll-timeline-axis', 'inline');
+test_valid_value('scroll-timeline-axis', 'vertical');
+test_valid_value('scroll-timeline-axis', 'horizontal');
+
+test_invalid_value('scroll-timeline-axis', 'abc');
+test_invalid_value('scroll-timeline-axis', '10px');
+test_invalid_value('scroll-timeline-axis', 'auto');
+test_invalid_value('scroll-timeline-axis', 'none');
+
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-axis-writing-mode.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-axis-writing-mode.html
new file mode 100644
index 0000000000..90912bfc22
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-axis-writing-mode.html
@@ -0,0 +1,126 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1">
+<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#scroll-timeline-axis">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<style>
+ .scroller {
+ overflow: hidden;
+ width: 100px;
+ height: 100px;
+ }
+ .contents {
+ height: 200px;
+ width: 200px;
+ }
+ @keyframes expand {
+ from { width: 100px; }
+ to { width: 200px; }
+ }
+ #timeline_initial_axis {
+ scroll-timeline: timeline_initial_axis;
+ }
+ #timeline_vertical {
+ scroll-timeline: vertical timeline_vertical;
+ }
+ #timeline_horizontal {
+ scroll-timeline: horizontal timeline_horizontal;
+ }
+ #timeline_block_in_horizontal {
+ scroll-timeline: block timeline_block_in_horizontal;
+ }
+ #timeline_inline_in_horizontal {
+ scroll-timeline: inline timeline_inline_in_horizontal;
+ }
+ #timeline_block_in_vertical {
+ scroll-timeline: block timeline_block_in_vertical;
+ writing-mode: vertical-lr;
+ }
+ #timeline_inline_in_vertical {
+ scroll-timeline: inline timeline_inline_in_vertical;
+ writing-mode: vertical-lr;
+ }
+ #container > div {
+ width: 0px;
+ animation-name: expand;
+ animation-duration: 10s;
+ animation-timing-function: linear;
+ }
+ /* Ensure stable expectations if feature is not supported */
+ @supports not (animation-timeline:foo) {
+ #container > div { animation-play-state: paused; }
+ }
+ #element_initial_axis { animation-timeline: timeline_initial_axis; }
+ #element_vertical { animation-timeline: timeline_vertical; }
+ #element_horizontal { animation-timeline: timeline_horizontal; }
+ #element_block_in_horizontal { animation-timeline: timeline_block_in_horizontal; }
+ #element_inline_in_horizontal { animation-timeline: timeline_inline_in_horizontal; }
+ #element_block_in_vertical { animation-timeline: timeline_block_in_vertical; }
+ #element_inline_in_vertical { animation-timeline: timeline_inline_in_vertical; }
+</style>
+<div class=scroller id=timeline_initial_axis><div class=contents></div></div>
+<div class=scroller id=timeline_vertical><div class=contents></div></div>
+<div class=scroller id=timeline_horizontal><div class=contents></div></div>
+<div class=scroller id=timeline_block_in_horizontal><div class=contents></div></div>
+<div class=scroller id=timeline_inline_in_horizontal><div class=contents></div></div>
+<div class=scroller id=timeline_block_in_vertical><div class=contents></div></div>
+<div class=scroller id=timeline_inline_in_vertical><div class=contents></div></div>
+<div id=container>
+ <div id=element_initial_axis></div>
+ <div id=element_vertical></div>
+ <div id=element_horizontal></div>
+ <div id=element_block_in_horizontal></div>
+ <div id=element_inline_in_horizontal></div>
+ <div id=element_block_in_vertical></div>
+ <div id=element_inline_in_vertical></div>
+</div>
+<script>
+ // Animations linked to vertical scroll-timelines are at 75% progress.
+ timeline_initial_axis.scrollTop = 75;
+ timeline_vertical.scrollTop = 75;
+ timeline_block_in_horizontal.scrollTop = 75;
+ timeline_inline_in_vertical.scrollTop = 75;
+ // Animations linked to horizontal scroll-timelines are at 25% progress.
+ timeline_horizontal.scrollLeft = 25;
+ timeline_block_in_vertical.scrollLeft = 25;
+ timeline_inline_in_horizontal.scrollLeft = 25;
+
+ promise_test(async (t) => {
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(element_initial_axis).width, '175px');
+ }, 'Initial axis');
+
+ promise_test(async (t) => {
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(element_vertical).width, '175px');
+ }, 'Vertical axis');
+
+ promise_test(async (t) => {
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(element_horizontal).width, '125px');
+ }, 'Horizontal axis');
+
+ promise_test(async (t) => {
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(element_block_in_horizontal).width, '175px');
+ }, 'Block axis in horizontal writing-mode');
+
+ promise_test(async (t) => {
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(element_inline_in_horizontal).width, '125px');
+ }, 'Inline axis in horizontal writing-mode');
+
+ promise_test(async (t) => {
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(timeline_block_in_vertical).writingMode, 'vertical-lr');
+ assert_equals(getComputedStyle(element_block_in_vertical).width, '125px');
+ }, 'Block axis in vertical writing-mode');
+
+ promise_test(async (t) => {
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(timeline_inline_in_vertical).writingMode, 'vertical-lr');
+ assert_equals(getComputedStyle(element_inline_in_vertical).width, '175px');
+ }, 'Inline axis in vertical writing-mode');
+
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-default-iframe-ref.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-default-iframe-ref.html
new file mode 100644
index 0000000000..1ab5646c8b
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-default-iframe-ref.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<title>Reference for default scroll() timeline</title>
+<iframe width="400" height="400" srcdoc='
+ <html>
+ <style>
+ html {
+ min-height: 100%;
+ padding-bottom: 100px;
+ }
+
+ #box {
+ width: 100px;
+ height: 100px;
+ background-color: green;
+ transform: translateY(100px);
+ }
+
+ * {
+ margin-top: 0px;
+ margin-bottom: 0px;
+ }
+ </style>
+ <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>
+ <div id="box"></div>
+ </html>
+'></iframe>
diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-default-iframe.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-default-iframe.html
new file mode 100644
index 0000000000..dbcf5941a8
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-default-iframe.html
@@ -0,0 +1,73 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<title>The default scroll() timeline in the iframe</title>
+<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#scroll-notation">
+<link rel="help" href="https://drafts.csswg.org/css-animations-2/#animation-timeline">
+<meta name="assert" content="CSS animation correctly updates values when using the default scroll() timeline">
+<link rel="match" href="scroll-timeline-default-iframe-ref.html">
+
+<iframe id="target" width="400" height="400" srcdoc='
+ <html>
+ <style>
+ @keyframes update {
+ from { transform: translateY(0px); }
+ to { transform: translateY(200px); }
+ }
+ html {
+ min-height: 100%;
+ padding-bottom: 100px;
+ }
+ #box {
+ width: 100px;
+ height: 100px;
+ background-color: green;
+ animation: update 1s linear;
+ animation-timeline: scroll();
+ }
+ #covered {
+ width: 100px;
+ height: 100px;
+ background-color: red;
+ }
+
+ * {
+ margin-top: 0px;
+ margin-bottom: 0px;
+ }
+ </style>
+ <script src="/web-animations/testcommon.js"></script>
+ <script>
+ window.addEventListener("load", async function() {
+ const scroller = document.scrollingElement;
+
+ // Move the scroller to the halfway point.
+ const maxScroll = scroller.scrollHeight - scroller.clientHeight;
+ scroller.scrollTop = 0.5 * maxScroll;
+
+ await waitForCompositorReady();
+ await waitForNextFrame();
+ await waitForNextFrame();
+
+ window.parent.postMessage("success", "*");
+ });
+ </script>
+ <body>
+ <div id="box"></div>
+ <div id="covered"></div>
+ </body>
+ </html>
+'></iframe>
+<script src="/web-animations/testcommon.js"></script>
+<script>
+ async function finishTest() {
+ await waitForCompositorReady();
+ await waitForNextFrame();
+ await waitForNextFrame();
+ document.documentElement.classList.remove("reftest-wait");
+ }
+ window.addEventListener("message", event => {
+ if (event.data == "success") {
+ finishTest();
+ }
+ }, false);
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-default-quirks-mode.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-default-quirks-mode.html
new file mode 100644
index 0000000000..d2c28d86b6
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-default-quirks-mode.html
@@ -0,0 +1,63 @@
+<html class="reftest-wait">
+<title>The default scroll() timeline in quirks mode</title>
+<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#scroll-notation">
+<link rel="help" href="https://drafts.csswg.org/css-animations-2/#animation-timeline">
+<meta name="assert" content="CSS animation correctly updates values when using the default scroll() timeline">
+<link rel="match" href="scroll-timeline-default-ref.html">
+
+<style>
+ @keyframes update {
+ from { transform: translateY(0px); }
+ to { transform: translateY(200px); }
+ }
+
+ html {
+ min-height: 100%;
+ padding-bottom: 100px;
+ }
+
+ #box {
+ width: 100px;
+ height: 100px;
+ background-color: green;
+ animation: update 1s linear;
+ animation-timeline: scroll();
+ }
+
+ #covered {
+ width: 100px;
+ height: 100px;
+ background-color: red;
+ }
+
+ * {
+ margin-top: 0px;
+ margin-bottom: 0px;
+ }
+</style>
+
+<div id="box"></div>
+<div id="covered"></div>
+<script src="/web-animations/testcommon.js"></script>
+<script>
+document.documentElement.addEventListener('TestRendered', async () => {
+ runTest();
+}, { once: true });
+
+async function runTest() {
+ const scroller = document.scrollingElement;
+
+ await waitForCompositorReady();
+
+ // Move the scroller to the halfway point. Then advance to the next frame
+ // to pick up the new timeline time.
+ const maxScroll = scroller.scrollHeight - scroller.clientHeight;
+ scroller.scrollTop = 0.5 * maxScroll;
+
+ await waitForNextFrame();
+ await waitForNextFrame();
+
+ document.documentElement.classList.remove("reftest-wait");
+}
+
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-default-ref.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-default-ref.html
new file mode 100644
index 0000000000..cb3b60e4bd
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-default-ref.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<title>Reference for default scroll() timeline</title>
+<style>
+ html {
+ min-height: 100%;
+ padding-bottom: 100px;
+ }
+
+ #box {
+ width: 100px;
+ height: 100px;
+ background-color: green;
+ transform: translateY(100px);
+ }
+
+ * {
+ margin-top: 0px;
+ margin-bottom: 0px;
+ }
+</style>
+
+<div id="box"></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>
diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-default-writing-mode-rl-ref.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-default-writing-mode-rl-ref.html
new file mode 100644
index 0000000000..3c072829e6
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-default-writing-mode-rl-ref.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<title>Reference for default scroll() timeline with vertical-rl</title>
+<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1">
+<style>
+ html {
+ min-block-size: 100%;
+ padding-block-end: 100px;
+ writing-mode: vertical-rl
+ }
+
+ #box {
+ width: 100px;
+ height: 100px;
+ background-color: green;
+ transform: translateX(-100px);
+ }
+
+ * {
+ margin-block: 0px;
+ }
+</style>
+
+<div id="box"></div>
+
+<script>
+ window.addEventListener('load', function() {
+ // Move the scroller to halfway.
+ const scroller = document.scrollingElement;
+ const maxScroll = scroller.scrollWidth - scroller.clientWidth;
+ scroller.scrollLeft = -0.5 * maxScroll;
+ });
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-default-writing-mode-rl.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-default-writing-mode-rl.html
new file mode 100644
index 0000000000..27e6ec196b
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-default-writing-mode-rl.html
@@ -0,0 +1,65 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<title>The default scroll() timeline with writing-mode:vertical-rl</title>
+<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1">
+<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#scroll-notation">
+<link rel="help" href="https://drafts.csswg.org/css-animations-2/#animation-timeline">
+<meta name="assert" content="CSS animation correctly updates values when using
+ the default scroll() timeline with writing-mode:vertical-rl">
+<link rel="match" href="scroll-timeline-default-writing-mode-rl-ref.html">
+
+<style>
+ @keyframes update {
+ from { transform: translateX(0px); }
+ to { transform: translateX(-200px); }
+ }
+
+ html {
+ min-block-size: 100%;
+ padding-block-end: 100px;
+ writing-mode: vertical-rl;
+ }
+
+ #box {
+ width: 100px;
+ height: 100px;
+ background-color: green;
+ animation: update 1s linear;
+ animation-timeline: scroll();
+ }
+
+ #covered {
+ width: 100px;
+ height: 100px;
+ background-color: red;
+ }
+
+ * {
+ margin-block: 0px;
+ }
+</style>
+
+<div id="box"></div>
+<div id="covered"></div>
+
+<script src="/web-animations/testcommon.js"></script>
+<script>
+ document.documentElement.addEventListener('TestRendered', async () => {
+ runTest();
+ }, { once: true });
+
+ async function runTest() {
+ const scroller = document.scrollingElement;
+
+ await waitForCompositorReady();
+
+ // Move the scroller to the halfway point.
+ const maxScroll = scroller.scrollWidth - scroller.clientWidth;
+ scroller.scrollLeft = -0.5 * maxScroll;
+
+ await waitForNextFrame();
+ await waitForNextFrame();
+
+ document.documentElement.classList.remove("reftest-wait");
+ }
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-default.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-default.html
new file mode 100644
index 0000000000..07eda33fd0
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-default.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<title>The default scroll() timeline</title>
+<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#scroll-notation">
+<link rel="help" href="https://drafts.csswg.org/css-animations-2/#animation-timeline">
+<meta name="assert" content="CSS animation correctly updates values when using the default scroll() timeline">
+<link rel="match" href="scroll-timeline-default-ref.html">
+
+<style>
+ @keyframes update {
+ from { transform: translateY(0px); }
+ to { transform: translateY(200px); }
+ }
+
+ html {
+ min-height: 100%;
+ padding-bottom: 100px;
+ }
+
+ #box {
+ width: 100px;
+ height: 100px;
+ background-color: green;
+ animation: update 1s linear;
+ animation-timeline: scroll();
+ }
+
+ #covered {
+ width: 100px;
+ height: 100px;
+ background-color: red;
+ }
+
+ * {
+ margin-top: 0px;
+ margin-bottom: 0px;
+ }
+</style>
+
+<div id="box"></div>
+<div id="covered"></div>
+
+<script src="/web-animations/testcommon.js"></script>
+<script>
+ document.documentElement.addEventListener('TestRendered', async () => {
+ runTest();
+ }, { once: true });
+
+ async function runTest() {
+ const scroller = document.scrollingElement;
+
+ await waitForCompositorReady();
+
+ // Move the scroller to the halfway point.
+ const maxScroll = scroller.scrollHeight - scroller.clientHeight;
+ scroller.scrollTop = 0.5 * maxScroll;
+
+ await waitForNextFrame();
+ await waitForNextFrame();
+
+ document.documentElement.classList.remove("reftest-wait");
+ }
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-document-scroller-quirks.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-document-scroller-quirks.html
new file mode 100644
index 0000000000..809a658a15
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-document-scroller-quirks.html
@@ -0,0 +1,36 @@
+<!-- Quirks mode -->
+<title>Tests the document scroller in quirks mode</title>
+<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#scroll-notation">
+<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=1180575">
+<link rel="author" href="mailto:andruud@chromium.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/scroll-animations/scroll-timelines/testcommon.js"></script>
+<script src="/css/css-animations/support/testcommon.js"></script>
+<script src="support/testcommon.js"></script>
+<style>
+ @keyframes anim {
+ from { z-index: 100; }
+ to { z-index: 100; }
+ }
+ #element {
+ animation: anim forwards;
+ animation-timeline: scroll(root);
+ }
+ #spacer {
+ height: 200vh;
+ }
+</style>
+<div id=element></div>
+<div id=spacer></div>
+
+<script>
+'use strict';
+
+setup(assert_implements_animation_timeline);
+
+promise_test(async () => {
+ await waitForCSSScrollTimelineStyle();
+ assert_equals(getComputedStyle(element).zIndex, "100");
+});
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-dynamic.tentative.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-dynamic.tentative.html
new file mode 100644
index 0000000000..b0880a7cc5
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-dynamic.tentative.html
@@ -0,0 +1,267 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1">
+<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#scroll-timelines">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<script src="support/testcommon.js"></script>
+<style>
+ main > div {
+ overflow: hidden;
+ width: 100px;
+ height: 100px;
+ }
+ main > div > div {
+ height: 200px;
+ }
+ @keyframes expand {
+ from { width: 100px; }
+ to { width: 200px; }
+ }
+ #element {
+ width: 0px;
+ height: 20px;
+ animation-name: expand;
+ /* Some of the tests in this file assume animations attached to the
+ DocumentTimeline are "stopped" without actually being paused.
+ Using 600s + steps(10, end) achieves this for one minute.*/
+ animation-duration: 600s;
+ animation-timing-function: steps(10, end);
+ }
+</style>
+<main>
+ <div id=scroller1><div></div></div>
+ <div id=scroller2><div></div></div>
+ <div id=container></div>
+</main>
+<script>
+ // Force layout of scrollers.
+ scroller1.offsetTop;
+ scroller2.offsetTop;
+
+ // Note the steps(10, end) timing function and height:100px. (10px scroll
+ // resolution).
+ scroller1.scrollTop = 20;
+ scroller2.scrollTop = 40;
+
+ function insertElement() {
+ let element = document.createElement('div');
+ element.id = 'element';
+ container.append(element);
+ return element;
+ }
+
+ // Runs a test with dynamically added/removed elements or CSS rules.
+ // Each test is instantiated twice: once for the initial style resolve where
+ // the DOM change was effectuated, and once after scrolling.
+ function dynamic_rule_test(func, description) {
+ // assert_width is an async function which verifies that the computed value
+ // of 'width' is as expected.
+ const instantiate = (assert_width, description) => {
+ promise_test(async (t) => {
+ try {
+ await func(t, assert_width);
+ } finally {
+ while (container.firstChild)
+ container.firstChild.remove();
+ scroller1.style = '';
+ scroller2.style = '';
+ }
+ }, description);
+ };
+
+ // Verify that the computed style is as expected after a full frame update
+ // following the rule change took place.
+ instantiate(async (element, expected) => {
+ await waitForCSSScrollTimelineStyle();
+ assert_equals(getComputedStyle(element).width, expected);
+ }, description + ' [immediate]');
+
+ // Verify that the computed style after scrolling a bit.
+ instantiate(async (element, expected) => {
+ scroller1.scrollTop = scroller1.scrollTop + 10;
+ scroller2.scrollTop = scroller2.scrollTop + 10;
+ await waitForNextFrame();
+ scroller1.scrollTop = scroller1.scrollTop - 10;
+ scroller2.scrollTop = scroller2.scrollTop - 10;
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(element).width, expected);
+ }, description + ' [scroll]');
+ }
+
+ dynamic_rule_test(async (t, assert_width) => {
+ let element = insertElement();
+
+ // This element initially has a DocumentTimeline.
+ await assert_width(element, '100px');
+
+ // Switch to scroll timeline.
+ scroller1.style.scrollTimelineName = 'timeline';
+ element.style.animationTimeline = 'timeline';
+ await assert_width(element, '120px');
+
+ // Switching from ScrollTimeline -> DocumentTimeline should preserve
+ // current time.
+ scroller1.style = '';
+ element.style = '';
+ await assert_width(element, '120px');
+ }, 'Switching between document and scroll timelines');
+
+ dynamic_rule_test(async (t, assert_width) => {
+ let element = insertElement();
+
+ // Flush style and create the animation with play pending.
+ getComputedStyle(element).animation;
+
+ let anim = element.getAnimations()[0];
+ assert_true(anim.pending, "The animation is in play pending");
+
+ // Switch to scroll timeline for a pending animation.
+ scroller1.style.scrollTimelineName = 'timeline';
+ element.style.animationTimeline = 'timeline';
+
+ await anim.ready;
+ assert_false(anim.pending, "The animation is not pending");
+
+ await assert_width(element, '120px');
+ }, 'Switching pending animation from document to scroll timelines');
+
+ dynamic_rule_test(async (t, assert_width) => {
+ let element = insertElement();
+
+ // Note: #scroller1 is at 20%, and #scroller2 is at 40%.
+ scroller1.style.scrollTimelineName = 'timeline1';
+ scroller2.style.scrollTimelineName = 'timeline2';
+
+ await assert_width(element, '100px');
+
+ element.style.animationTimeline = 'timeline1';
+ await assert_width(element, '120px');
+
+ element.style.animationTimeline = 'timeline2';
+ await assert_width(element, '140px');
+
+ element.style.animationTimeline = 'timeline1';
+ await assert_width(element, '120px');
+
+ // Switching from ScrollTimeline -> DocumentTimeline should preserve
+ // current time.
+ element.style.animationTimeline = '';
+ await assert_width(element, '120px');
+
+ }, 'Changing computed value of animation-timeline changes effective timeline');
+
+ dynamic_rule_test(async (t, assert_width) => {
+ let element = insertElement();
+
+ scroller1.style.scrollTimelineName = 'timeline';
+
+ // DocumentTimeline applies by default.
+ await assert_width(element, '100px');
+
+ // DocumentTimeline -> none
+ element.style.animationTimeline = 'none';
+ await assert_width(element, '0px');
+
+ // none -> DocumentTimeline
+ element.style.animationTimeline = '';
+ await assert_width(element, '100px');
+
+ // DocumentTimeline -> ScrollTimeline
+ element.style.animationTimeline = 'timeline';
+ await assert_width(element, '120px');
+
+ // ScrollTimeline -> none
+ element.style.animationTimeline = 'none';
+ await assert_width(element, '120px');
+
+ // none -> ScrollTimeline
+ element.style.animationTimeline = 'timeline';
+ await assert_width(element, '120px');
+ }, 'Changing to/from animation-timeline:none');
+
+
+ dynamic_rule_test(async (t, assert_width) => {
+ let element = insertElement();
+
+ element.style.animationTimeline = 'timeline';
+
+ // Unknown animation-timeline, current time held at zero.
+ await assert_width(element, '100px');
+
+ scroller1.style.scrollTimelineName = 'timeline';
+ await assert_width(element, '120px');
+
+ scroller2.style.scrollTimelineName = 'timeline';
+ await assert_width(element, '140px');
+ }, 'Changing scroll-timeline on preceding elements affects target element');
+
+
+ dynamic_rule_test(async (t, assert_width) => {
+ let element = insertElement();
+
+ element.style.animationDirection = 'reverse';
+ element.style.animationTimeline = 'timeline';
+
+ // Unknown animation-timeline, current time held at zero.
+ await assert_width(element, '200px');
+
+ // Note: #scroller1 is at 20%.
+ scroller1.style.scrollTimelineName = 'timeline';
+ await assert_width(element, '180px');
+
+ // Note: #scroller2 is at 40%.
+ scroller2.style.scrollTimelineName = 'timeline';
+ await assert_width(element, '160px');
+
+ element.style.animationDirection = '';
+ await assert_width(element, '140px');
+ }, 'Reverse animation direction');
+
+ dynamic_rule_test(async (t, assert_width) => {
+ let element = insertElement();
+ element.style.animationTimeline = 'timeline';
+
+ // Unknown animation-timeline, current time held at zero.
+ await assert_width(element, '100px');
+
+ // Note: #scroller1 is at 20%.
+ scroller1.style.scrollTimelineName = 'timeline';
+ await assert_width(element, '120px');
+
+ element.style.animationPlayState = 'paused';
+
+ // We should still be at the same position after pausing.
+ await assert_width(element, '120px');
+
+ // Note: #scroller2 is at 40%.
+ scroller2.style.scrollTimelineName = 'timeline';
+
+ // Even when switching timelines, we should be at the same position until
+ // we unpause.
+ await assert_width(element, '120px');
+
+ // Unpausing should synchronize to the scroll position.
+ element.style.animationPlayState = '';
+ await assert_width(element, '140px');
+ }, 'Switching timelines while paused');
+
+ dynamic_rule_test(async (t, assert_width) => {
+ let element = insertElement();
+
+ // Note: #scroller1 is at 20%.
+ scroller1.style.scrollTimelineName = 'timeline';
+
+ await assert_width(element, '100px');
+
+ element.style.animationTimeline = 'timeline';
+ element.style.animationPlayState = 'paused';
+
+ // Pausing should happen before the timeline is modified. (Tentative).
+ // https://github.com/w3c/csswg-drafts/issues/5653
+ await assert_width(element, '100px');
+
+ element.style.animationPlayState = 'running';
+ await assert_width(element, '120px');
+ }, 'Switching timelines and pausing at the same time');
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-frame-size-changed-ref.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-frame-size-changed-ref.html
new file mode 100644
index 0000000000..ea7628ac72
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-frame-size-changed-ref.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<title>Reference for the default scroll() timeline</title>
+<style>
+ html {
+ min-height: 100%;
+ padding-bottom: 50px;
+ }
+
+ #box {
+ width: 100px;
+ height: 100px;
+ background-color: green;
+ transform: translateY(100px);
+ }
+
+ * {
+ margin-top: 0px;
+ margin-bottom: 0px;
+ }
+</style>
+
+<div id="box"></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>
diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-frame-size-changed.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-frame-size-changed.html
new file mode 100644
index 0000000000..fb0eb8aa17
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-frame-size-changed.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<title>The default scroll() timeline when the frame size changed</title>
+<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#scroll-notation">
+<link rel="help" href="https://drafts.csswg.org/css-animations-2/#animation-timeline">
+<meta name="assert" content="CSS animation correctly updates values when using
+ the default scroll() timeline and update the
+ frame size">
+<link rel="match" href="scroll-timeline-frame-size-changed-ref.html">
+
+<style>
+ @keyframes update {
+ from { transform: translateY(0px); }
+ to { transform: translateY(200px); }
+ }
+
+ html {
+ min-height: 100%;
+ padding-bottom: 100px;
+ }
+
+ #box {
+ width: 100px;
+ height: 100px;
+ background-color: green;
+ animation: update 1s linear;
+ animation-timeline: scroll();
+ }
+
+ #covered {
+ width: 100px;
+ height: 100px;
+ background-color: red;
+ }
+
+ * {
+ margin-top: 0px;
+ margin-bottom: 0px;
+ }
+</style>
+
+<div id="box"></div>
+<div id="covered"></div>
+
+<script src="/web-animations/testcommon.js"></script>
+<script>
+ document.documentElement.addEventListener('TestRendered', async () => {
+ runTest();
+ }, { once: true });
+
+ async function runTest() {
+ const scroller = document.scrollingElement;
+
+ await waitForCompositorReady();
+
+ // Move the scroller to the 25% point.
+ const maxScroll = scroller.scrollHeight - scroller.clientHeight;
+ scroller.scrollTop = 0.25 * maxScroll;
+ await waitForNextFrame();
+
+ // Update scroll range to make the current position become 50% point.
+ scroller.style.paddingBottom = "50px";
+ await waitForNextFrame();
+
+ document.documentElement.classList.remove("reftest-wait");
+ }
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-in-container-query.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-in-container-query.html
new file mode 100644
index 0000000000..d6a8ba8e60
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-in-container-query.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<title>scroll-timeline and container queries</title>
+<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#scroll-timeline-shorthand">
+<link rel="help" src="https://drafts.csswg.org/css-contain-3/#container-queries">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<script src="support/testcommon.js"></script>
+<style>
+ #outer {
+ height: 100px;
+ width: 150px;
+ }
+
+ #container {
+ container-type: size;
+ }
+
+ #scroller {
+ overflow: auto;
+ width: auto;
+ height: 100px;
+ }
+ #scroller > div {
+ height: 200px;
+ }
+
+ /* This does not apply initially. */
+ @container (width > 200px) {
+ #scroller {
+ scroll-timeline: timeline;
+ }
+ }
+
+ @keyframes expand {
+ from { background-color: rgb(100, 100, 100); }
+ to { background-color: rgb(200, 200, 200); }
+ }
+ #element {
+ height: 10px;
+ width: 10px;
+ animation: expand 10s linear;
+ animation-timeline: timeline;
+ background-color: rgb(0, 0, 0);
+ }
+</style>
+<div id=outer>
+ <div id=container>
+ <div id=scroller>
+ <div></div>
+ </div>
+ <div id=element>
+ </div>
+ </div>
+</div>
+<script>
+ setup(assert_implements_animation_timeline);
+
+ promise_test(async (t) => {
+ element.offsetTop;
+ scroller.scrollTop = 50;
+ await waitForNextFrame();
+ // Unknown timeline, time held at zero.
+ assert_equals(getComputedStyle(element).backgroundColor, 'rgb(100, 100, 100)');
+ // This causes the timeline to be created.
+ outer.style.width = '250px';
+ // Check value with getComputedStyle immediately, which is the unanimated
+ // value since the scroll timeline is inactive before the next frame.
+ assert_equals(getComputedStyle(element).backgroundColor, 'rgb(0, 0, 0)');
+ // Also check value after one frame.
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(element).backgroundColor, 'rgb(150, 150, 150)');
+ }, 'Timeline appearing via container queries');
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-inactive.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-inactive.html
new file mode 100644
index 0000000000..92165d7046
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-inactive.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#scroll-timelines">
+<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#avoiding-cycles">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<style>
+ #scroller {
+ overflow: scroll;
+ width: 100px;
+ height: 100px;
+ }
+ #contents {
+ height: 200px;
+ }
+ @keyframes expand {
+ from { width: 100px; }
+ to { width: 200px; }
+ }
+ #element {
+ width: 0px;
+ animation: expand 10s linear paused;
+ animation-timeline: timeline;
+ }
+</style>
+<div id="container">
+ <div id=element></div>
+</div>
+<script>
+
+promise_test(async (t) => {
+ let div = document.createElement('div');
+ div.setAttribute('id', 'scroller');
+ div.style.scrollTimeline = 'timeline';
+ div.innerHTML = '<div id=contents></div>';
+ try {
+ container.insertBefore(div, element);
+
+ // The source has no layout box at the time the scroll timeline is created.
+ assert_equals(getComputedStyle(element).width, '0px');
+ scroller.offsetTop; // Ensure a layout box for the scroller.
+ // Wait for an update to the timeline state:
+ await waitForNextFrame();
+ // The timeline should now be active, and the animation should apply:
+ assert_equals(getComputedStyle(element).width, '100px');
+ } finally {
+ div.remove();
+ }
+}, 'Animation does not apply when timeline is initially inactive');
+
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-inline-orientation-ref.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-inline-orientation-ref.html
new file mode 100644
index 0000000000..7b87b1db39
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-inline-orientation-ref.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<title>Reference for scroll timeline with inline orientation and root scroller</title>
+<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1">
+<style>
+ html {
+ min-width: 100%;
+ padding-right: 100px;
+ }
+
+ #box {
+ width: 100px;
+ height: 100px;
+ background-color: green;
+ transform: translateX(100px);
+ }
+
+ * {
+ margin-left: 0px;
+ margin-right: 0px;
+ }
+</style>
+
+<div id="box"></div>
+
+<script>
+ window.addEventListener('load', function() {
+ // Move the scroller to halfway.
+ const scroller = document.scrollingElement;
+ const maxScroll = scroller.scrollWidth - scroller.clientWidth;
+ scroller.scrollLeft = 0.5 * maxScroll;
+ });
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-inline-orientation.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-inline-orientation.html
new file mode 100644
index 0000000000..52b7427f2d
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-inline-orientation.html
@@ -0,0 +1,68 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<title>Scroll timeline with inline orientation and root scroller</title>
+<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1">
+<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#scroll-notation">
+<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#descdef-scroll-timeline-orientation">
+<link rel="help" href="https://drafts.csswg.org/css-animations-2/#animation-timeline">
+<meta name="assert" content="CSS animation correctly updates values when using the inline orientation">
+<link rel="match" href="scroll-timeline-inline-orientation-ref.html">
+
+<style>
+ @keyframes update {
+ from { transform: translateX(0px); }
+ to { transform: translateX(200px); }
+ }
+
+ html {
+ min-width: 100%;
+ padding-right: 100px;
+ font-size: 0;
+ }
+
+ #box {
+ width: 100px;
+ height: 100px;
+ background-color: green;
+ animation: update 1s linear;
+ animation-timeline: scroll(inline root);
+ display: inline-block;
+ }
+
+ #covered {
+ width: 100px;
+ height: 100px;
+ background-color: red;
+ display: inline-block;
+ }
+
+ * {
+ margin-left: 0px;
+ margin-right: 0px;
+ }
+</style>
+
+<div id="box"></div>
+<div id="covered"></div>
+
+<script src="/web-animations/testcommon.js"></script>
+<script>
+ document.documentElement.addEventListener('TestRendered', async () => {
+ runTest();
+ }, { once: true });
+
+ async function runTest() {
+ const scroller = document.scrollingElement;
+
+ await waitForCompositorReady();
+
+ // Move the scroller to the halfway point.
+ const maxScroll = scroller.scrollWidth - scroller.clientWidth;
+ scroller.scrollLeft = 0.5 * maxScroll;
+
+ await waitForNextFrame();
+ await waitForNextFrame();
+
+ document.documentElement.classList.remove("reftest-wait");
+ }
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-multi-pass.tentative.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-multi-pass.tentative.html
new file mode 100644
index 0000000000..91668ada5b
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-multi-pass.tentative.html
@@ -0,0 +1,109 @@
+<!DOCTYPE html>
+<title>ScrollTimelines may trigger multiple style/layout passes</title>
+<link rel="help" src="https://github.com/w3c/csswg-drafts/issues/5261">
+<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#avoiding-cycles">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<script src="support/testcommon.js"></script>
+<style>
+ @keyframes expand_width {
+ from { width: 100px; }
+ to { width: 100px; }
+ }
+ @keyframes expand_height {
+ from { height: 100px; }
+ to { height: 100px; }
+ }
+ main {
+ height: 0px;
+ overflow: hidden;
+ }
+ .scroller {
+ height: 100px;
+ overflow: scroll;
+ }
+ .scroller > div {
+ height: 200px;
+ }
+ #element1 {
+ width: 1px;
+ animation: expand_width 10s;
+ animation-timeline: timeline1;
+ }
+ #element2 {
+ height: 1px;
+ animation: expand_height 10s;
+ animation-timeline: timeline2;
+ }
+</style>
+<main id=main>
+ <div id=element1></div>
+ <div>
+ <div id=element2></div>
+ </div>
+</main>
+<script>
+ setup(assert_implements_animation_timeline);
+
+ function insertScroller(timeline_name) {
+ let scroller = document.createElement('div');
+ scroller.classList.add('scroller');
+ scroller.style.scrollTimeline = timeline_name;
+ scroller.append(document.createElement('div'));
+ main.insertBefore(scroller, element1);
+ }
+
+ promise_test(async () => {
+ await waitForNextFrame();
+
+ let events1 = [];
+ let events2 = [];
+
+ insertScroller('timeline1');
+ // Even though the scroller was just inserted into the DOM, |timeline1|
+ // remains inactive until the next frame.
+ //
+ // https://drafts.csswg.org/scroll-animations-1/#avoiding-cycles
+ assert_equals(getComputedStyle(element1).width, '1px');
+ (new ResizeObserver(entries => {
+ events1.push(entries);
+ insertScroller('timeline2');
+ assert_equals(getComputedStyle(element2).height, '1px');
+ })).observe(element1);
+
+ (new ResizeObserver(entries => {
+ events2.push(entries);
+ })).observe(element2);
+
+ await waitForNextFrame();
+
+ // According to the basic rules of the spec [1], the timeline is
+ // inactive at the time the resize observer event was delivered, because
+ // #scroller1 did not have a layout box at the time style recalc for
+ // #element1 happened.
+ //
+ // However, an additional style/layout pass should take place
+ // (before resize observer deliveries) if we detect new ScrollTimelines
+ // in this situation, hence we ultimately do expect the animation to
+ // apply [2].
+ //
+ // [1] https://drafts.csswg.org/scroll-animations-1/#avoiding-cycles
+ // [2] https://github.com/w3c/csswg-drafts/issues/5261
+ assert_equals(events1.length, 1);
+ assert_equals(events1[0].length, 1);
+ assert_equals(events1[0][0].contentBoxSize.length, 1);
+ assert_equals(events1[0][0].contentBoxSize[0].inlineSize, 100);
+
+ // ScrollTimelines created during the ResizeObserver should remain
+ // inactive during the frame they're created, so the ResizeObserver
+ // event should not reflect the animated value.
+ assert_equals(events2.length, 1);
+ assert_equals(events2[0].length, 1);
+ assert_equals(events2[0][0].contentBoxSize.length, 1);
+ assert_equals(events2[0][0].contentBoxSize[0].blockSize, 1);
+
+ assert_equals(getComputedStyle(element1).width, '100px');
+ assert_equals(getComputedStyle(element2).height, '100px');
+ }, 'Multiple style/layout passes occur when necessary');
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-name-computed.tentative.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-name-computed.tentative.html
new file mode 100644
index 0000000000..1acb964ef7
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-name-computed.tentative.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/6674">
+<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/rewrite#scroll-timeline-name">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/computed-testcommon.js"></script>
+</head>
+<style>
+ #outer { scroll-timeline-name: foo; }
+ #target { scroll-timeline-name: bar; }
+</style>
+<div id="outer">
+ <div id="target"></div>
+</div>
+<script>
+test_computed_value('scroll-timeline-name', 'initial', 'none');
+test_computed_value('scroll-timeline-name', 'inherit', 'foo');
+test_computed_value('scroll-timeline-name', 'unset', 'none');
+test_computed_value('scroll-timeline-name', 'revert', 'none');
+test_computed_value('scroll-timeline-name', 'none');
+test_computed_value('scroll-timeline-name', 'test');
+test_computed_value('scroll-timeline-name', 'tEst');
+
+test(() => {
+ let style = getComputedStyle(document.getElementById('target'));
+ assert_not_equals(Array.from(style).indexOf('scroll-timeline-name'), -1);
+}, 'The scroll-timeline-name property shows up in CSSStyleDeclaration enumeration');
+
+test(() => {
+ let style = document.getElementById('target').style;
+ assert_not_equals(style.cssText.indexOf('scroll-timeline-name'), -1);
+}, 'The scroll-timeline-name property shows up in CSSStyleDeclaration.cssText');
+
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-name-parsing.tentative.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-name-parsing.tentative.html
new file mode 100644
index 0000000000..a8f2e31650
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-name-parsing.tentative.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/6674">
+<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/rewrite#scroll-timeline-name">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/parsing-testcommon.js"></script>
+</head>
+<div id="target"></div>
+
+<script>
+
+test_valid_value('scroll-timeline-name', 'initial');
+test_valid_value('scroll-timeline-name', 'inherit');
+test_valid_value('scroll-timeline-name', 'unset');
+test_valid_value('scroll-timeline-name', 'revert');
+
+test_valid_value('scroll-timeline-name', 'none');
+test_valid_value('scroll-timeline-name', 'abc');
+test_valid_value('scroll-timeline-name', ' abc', 'abc');
+test_valid_value('scroll-timeline-name', 'aBc');
+test_valid_value('scroll-timeline-name', 'auto');
+
+test_invalid_value('scroll-timeline-name', 'default');
+test_invalid_value('scroll-timeline-name', '10px');
+test_invalid_value('scroll-timeline-name', 'foo bar');
+test_invalid_value('scroll-timeline-name', '"foo" "bar"');
+test_invalid_value('scroll-timeline-name', 'rgb(1, 2, 3)');
+test_invalid_value('scroll-timeline-name', '#fefefe');
+
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-name-tree-scoped.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-name-tree-scoped.html
new file mode 100644
index 0000000000..622a582cce
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-name-tree-scoped.html
@@ -0,0 +1,144 @@
+<!DOCTYPE html>
+<title>scroll-timelime-name and tree-scoped references</title>
+<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#scroll-timelines-named">
+<link rel="help" src="https://drafts.csswg.org/css-scoping-1/#shadow-names">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<script src="/resources/declarative-shadow-dom-polyfill.js"></script>
+
+<main id=main></main>
+<script>
+ function inflate(t, template) {
+ t.add_cleanup(() => main.replaceChildren());
+ main.append(template.content.cloneNode(true));
+ main.offsetTop;
+ }
+
+ setup(() => {
+ polyfill_declarative_shadow_dom(document);
+ });
+</script>
+<style>
+ @keyframes anim {
+ from { z-index: 100; }
+ to { z-index: 100; }
+ }
+</style>
+
+<template id=scroll_timeline_host>
+ <style>
+ .target {
+ animation: anim 10s linear;
+ animation-timeline: timeline;
+ }
+ main > .scroller {
+ scroll-timeline: timeline horizontal;
+ }
+ </style>
+ <div class=scroller>
+ <div class=scroller>
+ <template shadowroot=open>
+ <style>
+ :host {
+ scroll-timeline: timeline vertical;
+ }
+ </style>
+ <slot></slot>
+ </template>
+ <div class=target></div>
+ </div>
+ </div>
+ <style>
+ </style>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, scroll_timeline_host);
+ let target = main.querySelector('.target');
+ assert_equals(target.getAnimations().length, 1);
+ let anim = target.getAnimations()[0];
+ assert_not_equals(anim.timeline, null);
+ assert_equals(anim.timeline.axis, 'horizontal');
+ }, 'Outer animation can not see scroll timeline defined by :host');
+</script>
+
+
+<template id=scroll_timeline_slotted>
+ <style>
+ .target {
+ animation: anim 10s linear;
+ animation-timeline: timeline;
+ }
+ .host {
+ scroll-timeline: timeline horizontal;
+ }
+ </style>
+ <div class=host>
+ <template shadowroot=open>
+ <style>
+ ::slotted(.scroller) {
+ scroll-timeline: timeline vertical;
+ }
+ </style>
+ <slot></slot>
+ </template>
+ <div class=scroller>
+ <div class=target></div>
+ </div>
+ </div>
+ <style>
+ </style>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, scroll_timeline_slotted);
+ let target = main.querySelector('.target');
+ assert_equals(target.getAnimations().length, 1);
+ let anim = target.getAnimations()[0];
+ assert_not_equals(anim.timeline, null);
+ assert_equals(anim.timeline.axis, 'horizontal');
+ }, 'Outer animation can not see scroll timeline defined by ::slotted');
+</script>
+
+
+<template id=scroll_timeline_part>
+ <style>
+ .host {
+ scroll-timeline: timeline vertical;
+ }
+ .host::part(foo) {
+ scroll-timeline: timeline horizontal;
+ }
+ </style>
+ <div class=host>
+ <template shadowroot=open>
+ <style>
+ /* Not using 'anim' at document scope, due to https://crbug.com/1334534 */
+ @keyframes anim2 {
+ from { z-index: 100; background-color: green; }
+ to { z-index: 100; background-color: green; }
+ }
+ .target {
+ animation: anim2 10s linear;
+ animation-timeline: timeline;
+ }
+ </style>
+ <div part=foo>
+ <div class=target></div>
+ </div>
+ </template>
+ </div>
+ <style>
+ </style>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, scroll_timeline_part);
+ let target = main.querySelector('.host').shadowRoot.querySelector('.target');
+ assert_equals(target.getAnimations().length, 1);
+ let anim = target.getAnimations()[0];
+ assert_not_equals(anim.timeline, null);
+ assert_equals(anim.timeline.axis, 'horizontal');
+ }, 'Inner animation can see scroll timeline defined by ::part');
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-nearest-dirty.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-nearest-dirty.html
new file mode 100644
index 0000000000..1a79c9bb22
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-nearest-dirty.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<title>Unrelated style mutation does not affect anonymous timeline</title>
+<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#scroll-notation">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/scroll-animations/scroll-timelines/testcommon.js"></script>
+<script src="/css/css-animations/support/testcommon.js"></script>
+<script src="support/testcommon.js"></script>
+<style>
+ @keyframes anim {
+ from { z-index: 100; }
+ to { z-index: 100; }
+ }
+ #scroller {
+ overflow: auto;
+ width: 100px;
+ height: 100px;
+ }
+ #element {
+ animation: anim forwards;
+ animation-timeline: scroll();
+ }
+ #spacer {
+ height: 200px;
+ }
+</style>
+<div id=scroller>
+ <div id=element></div>
+ <div id=spacer></div>
+</div>
+
+<script>
+setup(assert_implements_animation_timeline);
+
+promise_test(async () => {
+ await waitForCSSScrollTimelineStyle();
+ assert_equals(getComputedStyle(element).zIndex, '100');
+ // Unrelated style mutation does not change the effect value:
+ element.style.color = 'green';
+ assert_equals(getComputedStyle(element).zIndex, '100');
+});
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-paused-animations.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-paused-animations.html
new file mode 100644
index 0000000000..54518a5e87
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-paused-animations.html
@@ -0,0 +1,95 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Scroll timeline with paused animations</title>
+<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#scroll-notation">
+<link rel="help" href="https://drafts.csswg.org/css-animations/#animation-play-state">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/scroll-animations/scroll-timelines/testcommon.js"></script>
+<script src="/css/css-animations/support/testcommon.js"></script>
+<script src="support/testcommon.js"></script>
+<style>
+ @keyframes anim {
+ from { width: 100px; }
+ to { width: 200px; }
+ }
+
+ .fill-vh {
+ width: 100px;
+ height: 100vh;
+ }
+</style>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+setup(assert_implements_animation_timeline);
+
+async function resetScrollPosition() {
+ // Reset to 0 so we don't affect following tests.
+ document.scrollingElement.scrollTop = 0;
+ return waitForNextFrame();
+}
+
+promise_test(async t => {
+ const div = addDiv(t, { style: 'width: 50px; height: 100px;' });
+ const filling = addDiv(t, { class: 'fill-vh' });
+ const scroller = document.scrollingElement;
+ t.add_cleanup(resetScrollPosition);
+
+ div.style.animation = 'anim 100s linear paused';
+ div.style.animationTimeline = 'scroll(root)';
+ await waitForCSSScrollTimelineStyle();
+
+ const anim = div.getAnimations()[0];
+ await anim.ready;
+ assert_percents_equal(anim.currentTime, 0, 'timeline time reset');
+ assert_equals(getComputedStyle(div).width, '100px');
+
+ const maxScroll = scroller.scrollHeight - scroller.clientHeight;
+ scroller.scrollTop = maxScroll;
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(div).width, '100px');
+
+}, 'Test that the scroll animation is paused');
+
+promise_test(async t => {
+ const div = addDiv(t, { style: 'width: 50px; height: 100px;' });
+ const filling = addDiv(t, { class: 'fill-vh' });
+ const scroller = document.scrollingElement;
+ await waitForNextFrame();
+
+ div.style.animation = 'anim 100s linear forwards';
+ div.style.animationTimeline = 'scroll(root)';
+ await waitForCSSScrollTimelineStyle();
+
+ const anim = div.getAnimations()[0];
+ await anim.ready;
+ assert_percents_equal(anim.currentTime, 0, 'timeline time reset');
+ assert_equals(getComputedStyle(div).width, '100px');
+
+ await waitForNextFrame();
+ const maxScroll = scroller.scrollHeight - scroller.clientHeight;
+ scroller.scrollTop = maxScroll;
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(div).width, '200px');
+
+ div.style.animationPlayState = 'paused';
+ assert_equals(anim.playState, 'paused');
+ assert_equals(getComputedStyle(div).width, '200px',
+ 'Current time preserved when pause-pending.');
+ assert_true(anim.pending,
+ 'Pending state after changing animationPlayState');
+ await anim.ready;
+ assert_equals(getComputedStyle(div).width, '200px',
+ 'Current time preserved when paused.');
+ assert_percents_equal(anim.timeline.currentTime, 100);
+ document.scrollingElement.scrollTop = 0;
+ await waitForNextFrame();
+ assert_percents_equal(anim.timeline.currentTime, 0);
+ assert_equals(getComputedStyle(div).width, '200px');
+}, 'Test that the scroll animation is paused by updating animation-play-state');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-responsiveness-from-endpoint.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-responsiveness-from-endpoint.html
new file mode 100644
index 0000000000..71d3699077
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-responsiveness-from-endpoint.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Root-scrolling timeline with animation moving from end point</title>
+<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#scroll-notation">
+<link rel="help" href="https://drafts.csswg.org/web-animations/#update-an-animations-finished-state">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/css-animations/support/testcommon.js"></script>
+<script src="/scroll-animations/scroll-timelines/testcommon.js"></script>
+<script src="support/testcommon.js"></script>
+
+<style>
+ @keyframes anim {
+ from { width: 100px; }
+ to { width: 200px; }
+ }
+
+ .fill-vh {
+ width: 100px;
+ height: 100vh;
+ }
+</style>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+setup(assert_implements_animation_timeline);
+
+promise_test(async t => {
+ const div = addDiv(t, { style: 'width: 50px; height: 100px;' });
+ const filling = addDiv(t, { class: 'fill-vh' });
+ const scroller = document.scrollingElement;
+ scroller.scrollTop = 0;
+ await waitForNextFrame();
+
+ div.style.animation = 'anim 100s linear';
+ div.style.animationTimeline = 'scroll(root)';
+ await waitForCSSScrollTimelineStyle();
+
+ const anim = div.getAnimations()[0];
+ await anim.ready;
+ assert_percents_equal(anim.timeline.currentTime, 0,
+ 'Timeline time when animation is ready');
+ assert_equals(getComputedStyle(div).width, '100px',
+ 'Width at animation start');
+
+ await waitForNextFrame();
+ const maxScroll = scroller.scrollHeight - scroller.clientHeight;
+ scroller.scrollTop = maxScroll;
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(div).width, '200px',
+ 'Width at scroll limit');
+
+ document.scrollingElement.scrollTop = 0;
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(div).width, '100px',
+ 'Width after reset to scroll top');
+}, 'Test that the scroll animation is still responsive after moving from 100%');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-root-dirty.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-root-dirty.html
new file mode 100644
index 0000000000..1c0b73ab45
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-root-dirty.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<title>Unrelated style mutation does not affect anonymous timeline (root)</title>
+<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#scroll-notation">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/scroll-animations/scroll-timelines/testcommon.js"></script>
+<script src="/css/css-animations/support/testcommon.js"></script>
+<script src="support/testcommon.js"></script>
+<style>
+ @keyframes anim {
+ from { z-index: 100; }
+ to { z-index: 100; }
+ }
+ #element {
+ animation: anim forwards;
+ animation-timeline: scroll(root);
+ }
+ #spacer {
+ height: 200vh;
+ }
+</style>
+<div id=element></div>
+<div id=spacer></div>
+
+<script>
+setup(assert_implements_animation_timeline);
+
+promise_test(async () => {
+ await waitForCSSScrollTimelineStyle();
+ assert_equals(getComputedStyle(element).zIndex, '100');
+ // Unrelated style mutation does not change the effect value:
+ element.style.color = 'green';
+ assert_equals(getComputedStyle(element).zIndex, '100');
+});
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-sampling.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-sampling.html
new file mode 100644
index 0000000000..1fe354edfd
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-sampling.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#avoiding-cycles">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<style>
+ #scroller {
+ overflow: hidden;
+ width: 100px;
+ height: 100px;
+ scroll-timeline: timeline;
+ }
+ #contents {
+ height: 200px;
+ }
+ @keyframes expand {
+ from { width: 100px; }
+ to { width: 200px; }
+ }
+ #element {
+ width: 0px;
+ animation: expand 10s linear;
+ animation-timeline: timeline;
+ }
+ /* Ensure stable expectations if feature is not supported */
+ @supports not (animation-timeline:foo) {
+ #element { animation-play-state: paused; }
+ }
+</style>
+<div id=scroller>
+ <div id=contents></div>
+</div>
+<div id=element></div>
+<script>
+ promise_test(async (t) => {
+ // The scroll timeline is initially inactive until the first frame.
+ assert_equals(getComputedStyle(element).width, '0px');
+ await waitForNextFrame();
+ scroller.scrollTop = 50;
+ // Scrolling position should not yet be reflected in the animation,
+ // since the new scroll position has not yet been sampled.
+ assert_equals(getComputedStyle(element).width, '100px');
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(element).width, '150px');
+ }, 'Scroll position is sampled once per frame');
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-shorthand.tentative.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-shorthand.tentative.html
new file mode 100644
index 0000000000..b340ff34ff
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-shorthand.tentative.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/6674">
+<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/rewrite#scroll-timeline-axis">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/computed-testcommon.js"></script>
+<script src="/css/support/parsing-testcommon.js"></script>
+<script src="/css/support/shorthand-testcommon.js"></script>
+<div id="target"></div>
+<script>
+test_valid_value('scroll-timeline', 'block none', 'block');
+test_valid_value('scroll-timeline', 'none inline', 'inline');
+test_valid_value('scroll-timeline', 'horizontal abc');
+test_valid_value('scroll-timeline', 'inline abc');
+test_valid_value('scroll-timeline', 'inline aBc');
+test_valid_value('scroll-timeline', 'vertical none', 'vertical');
+test_valid_value('scroll-timeline', 'inline inline');
+test_valid_value('scroll-timeline', 'abc');
+test_valid_value('scroll-timeline', 'inline');
+
+test_invalid_value('scroll-timeline', '');
+test_invalid_value('scroll-timeline', 'abc abc');
+
+test_computed_value('scroll-timeline', 'block none', 'block');
+test_computed_value('scroll-timeline', 'abc inline', 'inline abc');
+test_computed_value('scroll-timeline', 'none vertical', 'vertical');
+test_computed_value('scroll-timeline', 'horizontal abc');
+test_computed_value('scroll-timeline', 'vertical vertical');
+test_computed_value('scroll-timeline', 'abc');
+
+test_shorthand_value('scroll-timeline', 'vertical abc',
+{
+ 'scroll-timeline-axis': 'vertical',
+ 'scroll-timeline-name': 'abc',
+});
+test_shorthand_value('scroll-timeline', 'horizontal inline',
+{
+ 'scroll-timeline-axis': 'horizontal',
+ 'scroll-timeline-name': 'inline',
+});
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-sibling-gcs.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-sibling-gcs.html
new file mode 100644
index 0000000000..6062e795f4
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-sibling-gcs.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<title>scroll-timeline and container queries</title>
+<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#scroll-timeline-shorthand">
+<link rel="help" src="https://drafts.csswg.org/css-contain-3/#container-queries">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<script src="support/testcommon.js"></script>
+<style>
+ #scroller {
+ overflow: auto;
+ width: auto;
+ height: 100px;
+ }
+ #scroller > div {
+ height: 200px;
+ }
+
+ @keyframes anim {
+ from { background-color: rgb(100, 100, 100); }
+ to { background-color: rgb(200, 200, 200); }
+ }
+ #element {
+ height: 10px;
+ width: 10px;
+ animation: anim 10s linear;
+ animation-timeline: timeline;
+ background-color: rgb(0, 0, 0);
+ }
+</style>
+<div>
+ <div id=scroller>
+ <div></div>
+ </div>
+ <div>
+ <div id=element></div>
+ </div>
+</div>
+<script>
+ setup(assert_implements_animation_timeline);
+
+ promise_test(async (t) => {
+ element.offsetTop;
+ scroller.scrollTop = 50;
+ await waitForNextFrame();
+ // Unknown timeline, time held at zero.
+ assert_equals(getComputedStyle(element).backgroundColor, 'rgb(100, 100, 100)');
+ scroller.style.scrollTimeline = 'timeline';
+ await waitForCSSScrollTimelineStyle();
+ assert_equals(getComputedStyle(element).backgroundColor, 'rgb(150, 150, 150)');
+ }, 'Timelines appearing on preceding siblings are visible to getComputedStyle');
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/support/testcommon.js b/testing/web-platform/tests/scroll-animations/css/support/testcommon.js
new file mode 100644
index 0000000000..66bc27bb10
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/support/testcommon.js
@@ -0,0 +1,19 @@
+'use strict';
+
+/**
+ * Returns a Promise that is resolved after a CSS scroll timeline is created (as
+ * the result of a style change) and a snapshot has been taken, so that the
+ * animation style is correctly reflected by getComputedStyle().
+ * Technically, this only takes a full frame update. We implement this as two
+ * requestAnimationFrame callbacks because the result will be available at the
+ * beginning of the second frame.
+ */
+async function waitForCSSScrollTimelineStyle() {
+ await waitForNextFrame();
+ await waitForNextFrame();
+}
+
+function assert_implements_animation_timeline() {
+ assert_implements(CSS.supports('animation-timeline:foo'),
+ 'animation-timeline not supported');
+}
diff --git a/testing/web-platform/tests/scroll-animations/css/timeline-range-name-offset-in-keyframes.tentative.html b/testing/web-platform/tests/scroll-animations/css/timeline-range-name-offset-in-keyframes.tentative.html
new file mode 100644
index 0000000000..993046c5f2
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/timeline-range-name-offset-in-keyframes.tentative.html
@@ -0,0 +1,94 @@
+<!DOCTYPE html>
+<html>
+<meta charset="utf-8">
+<title>Timeline offset in Animation Keyframes</title>
+<link rel="help" href="https://w3c.github.io/csswg-drafts/scroll-animations-1/#named-range-keyframes">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<script src="support/testcommon.js"></script>
+<style>
+ @keyframes fade-in-out-animation {
+ enter 0%, exit 100% { opacity: 0 }
+ enter 100%, exit 0% { opacity: 1 }
+ }
+
+ #subject {
+ background-color: rgba(0, 0, 255);
+ height: 200px;
+ width: 200px;
+ view-timeline-name: foo;
+ animation: linear 1s both fade-in-out-animation;
+ animation-timeline: foo;
+ }
+
+ #container {
+ border: 5px solid black;
+ height: 400px;
+ width: 400px;
+ overflow-y: scroll;
+ resize: both;
+ }
+
+ .spacer {
+ height: 600px;
+ width: 100%;
+ }
+</style>
+<body onload="runTests()">
+ <div id="container">
+ <div class="spacer"></div>
+ <div id="subject"></div>
+ <div class="spacer"></div>
+ </div>
+</body>
+
+<script type="text/javascript">
+ setup(assert_implements_animation_timeline);
+
+ function runTests() {
+ promise_test(async t => {
+ // scrollTop=200 to 400 is the enter range
+ container.scrollTop = 200;
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(subject).opacity, '0', 'Effect at enter 0%');
+
+ container.scrollTop = 300;
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(subject).opacity, '0.5', 'Effect at enter 50%');
+
+ container.scrollTop = 400;
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(subject).opacity, '1', 'Effect at enter 100%');
+
+ // scrollTop=600-800 is the exit range
+ container.scrollTop = 600;
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(subject).opacity, '1', 'Effect at exit 0%');
+
+ container.scrollTop = 700;
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(subject).opacity, '0.5', 'Effect at exit 50%');
+
+ container.scrollTop = 800;
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(subject).opacity, '0', 'Effect at exit 100%');
+
+ // First change scrollTop so that you are at enter 100%, then resize the container in a way
+ // that scrollTop is the same, but now the animation is at enter 50% and check opacity.
+ // After changing the height of container, scrollTop=300-500 is the enter range
+ container.scrollTop = 400;
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(subject).opacity, '1', 'Effect at enter 100%');
+ container.style.height = '300px';
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(subject).opacity, '0.5', 'Effect at enter 50%');
+
+ // After changing the height of container, scrollTop=600-800 is still the exit range
+ container.scrollTop = 700;
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(subject).opacity, '0.5', 'Effect at exit 50%');
+ });
+ }
+</script>
+</html>
diff --git a/testing/web-platform/tests/scroll-animations/css/view-timeline-animation.html b/testing/web-platform/tests/scroll-animations/css/view-timeline-animation.html
new file mode 100644
index 0000000000..b816bb6897
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-animation.html
@@ -0,0 +1,219 @@
+<!DOCTYPE html>
+<title>Animations using view-timeline</title>
+<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#view-timelines-named">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<script src="support/testcommon.js"></script>
+<style>
+ @keyframes anim {
+ from { z-index: 0; }
+ to { z-index: 100; }
+ }
+ .vertical-scroller {
+ overflow: auto;
+ width: 100px;
+ height: 100px;
+ }
+ .vertical-scroller > div {
+ height: 50px;
+ z-index: -1;
+ }
+ .horizontal-scroller {
+ overflow: auto;
+ width: 100px;
+ height: 100px;
+ writing-mode: vertical-lr;
+ }
+ .horizontal-scroller > div {
+ width: 50px;
+ z-index: -1;
+ }
+</style>
+<main id=main></main>
+<script>
+ setup(assert_implements_animation_timeline);
+
+ function inflate(t, template) {
+ t.add_cleanup(() => main.replaceChildren());
+ main.append(template.content.cloneNode(true));
+ }
+ async function scrollTop(e, value) {
+ e.scrollTop = value;
+ await waitForNextFrame();
+ }
+ async function scrollLeft(e, value) {
+ e.scrollLeft = value;
+ await waitForNextFrame();
+ }
+</script>
+
+<template id=default_view_timeline>
+ <style>
+ #target {
+ view-timeline: t1;
+ animation: anim 1s linear;
+ animation-timeline: t1;
+ }
+ </style>
+ <div id=scroller class=vertical-scroller>
+ <div></div> <!-- [0px, 50px] -->
+ <div></div> <!-- [50px, 100px] -->
+ <div></div> <!-- [100px, 150px] -->
+ <div id=target></div> <!-- [150px, 200px] -->
+ <div></div> <!-- [200px, 250px] -->
+ <div></div> <!-- [250px, 300px] -->
+ <div></div> <!-- [300px, 350px] -->
+ </div>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, default_view_timeline);
+ assert_equals(getComputedStyle(target).zIndex, '-1');
+ await scrollTop(scroller, 25);
+ assert_equals(getComputedStyle(target).zIndex, '-1');
+ await scrollTop(scroller, 50); // 0% (enter 0%)
+ assert_equals(getComputedStyle(target).zIndex, '0');
+ await scrollTop(scroller, 125); // 50%
+ assert_equals(getComputedStyle(target).zIndex, '50');
+ await scrollTop(scroller, 200); // 100% (exit 100%)
+ assert_equals(getComputedStyle(target).zIndex, '100');
+ await scrollTop(scroller, 225);
+ assert_equals(getComputedStyle(target).zIndex, '-1');
+ }, 'Default view-timeline');
+</script>
+
+<template id=horizontal_timeline>
+ <style>
+ #target {
+ view-timeline: t1 horizontal;
+ animation: anim 1s linear;
+ animation-timeline: t1;
+ }
+ </style>
+ <div id=scroller class=horizontal-scroller>
+ <div></div> <!-- [0px, 50px] -->
+ <div></div> <!-- [50px, 100px] -->
+ <div></div> <!-- [100px, 150px] -->
+ <div id=target></div> <!-- [150px, 200px] -->
+ <div></div> <!-- [200px, 250px] -->
+ <div></div> <!-- [250px, 300px] -->
+ <div></div> <!-- [300px, 350px] -->
+ </div>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, horizontal_timeline);
+ assert_equals(getComputedStyle(target).zIndex, '-1');
+ await scrollLeft(scroller, 25);
+ assert_equals(getComputedStyle(target).zIndex, '-1');
+ await scrollLeft(scroller, 50); // 0% (enter 0%)
+ assert_equals(getComputedStyle(target).zIndex, '0');
+ await scrollLeft(scroller, 125); // 50%
+ assert_equals(getComputedStyle(target).zIndex, '50');
+ await scrollLeft(scroller, 200); // 100% (exit 100%)
+ assert_equals(getComputedStyle(target).zIndex, '100');
+ await scrollLeft(scroller, 225);
+ assert_equals(getComputedStyle(target).zIndex, '-1');
+ }, 'Horizontal view-timeline');
+</script>
+
+<template id=multiple_timelines>
+ <style>
+ #timelines {
+ view-timeline: tv vertical, th horizontal;
+ background-color: red;
+ }
+ #scroller {
+ width: 100px;
+ height: 100px;
+ overflow: hidden;
+ display: grid;
+ grid-template-columns: 50px 50px 50px 50px 50px 50px 50px;
+ grid-template-row: 50px 50px 50px 50px 50px 50px 50px;
+ }
+ #scroller > div {
+ z-index: -1;
+ width: 50px;
+ height: 50px;
+ }
+ #target_v {
+ animation: anim 1s linear;
+ animation-timeline: tv;
+ }
+ #target_h {
+ animation: anim 1s linear;
+ animation-timeline: th;
+ }
+ </style>
+ <div id=scroller>
+ <!-- Created dynamically -->
+ </div>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, multiple_timelines);
+
+ // Create a 350px x 350px grid (7x7 items of 50x50px each), with the
+ // timelines at item [3,3], an element attached to the horizontal timeline
+ // at [4,3], and an element attached to the vertical timeline at [3,4].
+
+ // x x x x x x x
+ // x x x x x x x
+ // x x x x x x x
+ // x x x T H x x
+ // x x x V x x x
+ // x x x x x x x
+ // x x x x x x x
+ // x x x x x x x
+
+ let grid_size = 7;
+ for (let i = 0; i < (grid_size*grid_size); ++i) {
+ let div = document.createElement('div');
+ if (i == (3 * grid_size + 3))
+ div.id = 'timelines';
+ if (i == (3 * grid_size + 4))
+ div.id = 'target_h';
+ if (i == (4 * grid_size + 3))
+ div.id = 'target_v';
+ scroller.append(div);
+ }
+
+ assert_equals(getComputedStyle(target_v).zIndex, '-1');
+ assert_equals(getComputedStyle(target_h).zIndex, '-1');
+
+ // First scroll vertically.
+ await scrollTop(scroller, 25);
+ assert_equals(getComputedStyle(target_v).zIndex, '-1');
+ assert_equals(getComputedStyle(target_h).zIndex, '-1');
+ await scrollTop(scroller, 50); // 0% (enter 0%)
+ assert_equals(getComputedStyle(target_v).zIndex, '0');
+ assert_equals(getComputedStyle(target_h).zIndex, '-1');
+ await scrollTop(scroller, 125); // 50%
+ assert_equals(getComputedStyle(target_v).zIndex, '50');
+ assert_equals(getComputedStyle(target_h).zIndex, '-1');
+ await scrollTop(scroller, 200); // 100% (exit 100%)
+ assert_equals(getComputedStyle(target_v).zIndex, '100');
+ assert_equals(getComputedStyle(target_h).zIndex, '-1');
+ await scrollTop(scroller, 225);
+ assert_equals(getComputedStyle(target_v).zIndex, '-1');
+ assert_equals(getComputedStyle(target_h).zIndex, '-1');
+
+ // Then horizontally.
+ await scrollLeft(scroller, 25);
+ assert_equals(getComputedStyle(target_v).zIndex, '-1');
+ assert_equals(getComputedStyle(target_h).zIndex, '-1');
+ await scrollLeft(scroller, 50); // 0% (enter 0%)
+ assert_equals(getComputedStyle(target_v).zIndex, '-1');
+ assert_equals(getComputedStyle(target_h).zIndex, '0');
+ await scrollLeft(scroller, 125); // 50%
+ assert_equals(getComputedStyle(target_v).zIndex, '-1');
+ assert_equals(getComputedStyle(target_h).zIndex, '50');
+ await scrollLeft(scroller, 200); // 100% (exit 100%)
+ assert_equals(getComputedStyle(target_v).zIndex, '-1');
+ assert_equals(getComputedStyle(target_h).zIndex, '100');
+ await scrollLeft(scroller, 225);
+ assert_equals(getComputedStyle(target_v).zIndex, '-1');
+ assert_equals(getComputedStyle(target_h).zIndex, '-1');
+ }, 'Multiple view-timelines on the same element');
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/view-timeline-axis-computed.html b/testing/web-platform/tests/scroll-animations/css/view-timeline-axis-computed.html
new file mode 100644
index 0000000000..f4649dab04
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-axis-computed.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#view-timeline-axis">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/computed-testcommon.js"></script>
+</head>
+<style>
+ #outer { view-timeline-axis: block, inline; }
+ #target { view-timeline-axis: vertical; }
+</style>
+<div id=outer>
+ <div id=target></div>
+</div>
+<script>
+test_computed_value('view-timeline-axis', 'initial', 'block');
+test_computed_value('view-timeline-axis', 'inherit', 'block, inline');
+test_computed_value('view-timeline-axis', 'unset', 'block');
+test_computed_value('view-timeline-axis', 'revert', 'block');
+test_computed_value('view-timeline-axis', 'block');
+test_computed_value('view-timeline-axis', 'inline');
+test_computed_value('view-timeline-axis', 'vertical');
+test_computed_value('view-timeline-axis', 'horizontal');
+test_computed_value('view-timeline-axis', 'block, inline');
+test_computed_value('view-timeline-axis', 'inline, block');
+test_computed_value('view-timeline-axis', 'block, vertical, horizontal, inline');
+test_computed_value('view-timeline-axis', 'inline, inline, inline, inline');
+
+test(() => {
+ let style = getComputedStyle(document.getElementById('target'));
+ assert_not_equals(Array.from(style).indexOf('view-timeline-axis'), -1);
+}, 'The view-timeline-axis property shows up in CSSStyleDeclaration enumeration');
+
+test(() => {
+ let style = document.getElementById('target').style;
+ assert_not_equals(style.cssText.indexOf('view-timeline-axis'), -1);
+}, 'The view-timeline-axis property shows up in CSSStyleDeclaration.cssText');
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/view-timeline-axis-parsing.html b/testing/web-platform/tests/scroll-animations/css/view-timeline-axis-parsing.html
new file mode 100644
index 0000000000..ffcc36c320
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-axis-parsing.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#view-timeline-axis">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/parsing-testcommon.js"></script>
+</head>
+<div id="target"></div>
+<script>
+test_valid_value('view-timeline-axis', 'initial');
+test_valid_value('view-timeline-axis', 'inherit');
+test_valid_value('view-timeline-axis', 'unset');
+test_valid_value('view-timeline-axis', 'revert');
+
+test_valid_value('view-timeline-axis', 'block');
+test_valid_value('view-timeline-axis', 'inline');
+test_valid_value('view-timeline-axis', 'vertical');
+test_valid_value('view-timeline-axis', 'horizontal');
+test_valid_value('view-timeline-axis', 'block, inline');
+test_valid_value('view-timeline-axis', 'inline, block');
+test_valid_value('view-timeline-axis', 'block, vertical, horizontal, inline');
+test_valid_value('view-timeline-axis', 'inline, inline, inline, inline');
+
+test_invalid_value('view-timeline-axis', 'abc');
+test_invalid_value('view-timeline-axis', '10px');
+test_invalid_value('view-timeline-axis', 'auto');
+test_invalid_value('view-timeline-axis', 'none');
+test_invalid_value('view-timeline-axis', 'block inline');
+test_invalid_value('view-timeline-axis', 'block / inline');
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/view-timeline-delay-animation.html b/testing/web-platform/tests/scroll-animations/css/view-timeline-delay-animation.html
new file mode 100644
index 0000000000..dfb0e59f5d
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-delay-animation.html
@@ -0,0 +1,144 @@
+<!DOCTYPE html>
+<title>Animations using named timeline ranges</title>
+<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#named-timeline-range">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<script src="support/testcommon.js"></script>
+<style>
+ @keyframes anim {
+ from { z-index: 0; background-color: skyblue;}
+ to { z-index: 100; background-color: coral; }
+ }
+ #scroller {
+ border: 10px solid lightgray;
+ overflow-y: scroll;
+ width: 200px;
+ height: 200px;
+ }
+ #target {
+ margin: 800px 0px;
+ width: 100px;
+ height: 100px;
+ z-index: -1;
+ background-color: green;
+ }
+</style>
+<main id=main>
+</main>
+<template>
+ <div id=scroller>
+ <div id=target></div>
+ </div>
+</template>
+<script>
+ setup(assert_implements_animation_timeline);
+
+ function inflate(t, template) {
+ t.add_cleanup(() => main.replaceChildren());
+ main.append(template.content.cloneNode(true));
+ }
+ async function scrollTop(e, value) {
+ e.scrollTop = value;
+ await waitForNextFrame();
+ }
+ async function waitForAnimationReady(target) {
+ await waitForNextFrame();
+ await Promise.all(target.getAnimations().map(x => x.promise));
+ }
+ async function assertValueAt(scroller, target, args) {
+ await waitForAnimationReady(target);
+ await scrollTop(scroller, args.scrollTop);
+ assert_equals(getComputedStyle(target).zIndex, args.expected.toString());
+ }
+ function test_animation_delay(options) {
+ promise_test(async (t) => {
+ inflate(t, document.querySelector('template'));
+ let scroller = main.querySelector('#scroller');
+ let target = main.querySelector('#target');
+
+ target.style.viewTimeline = 't1 block';
+ // TODO(crbug.com/1375998): Create the timeline in a separate frame to
+ // work around a bug.
+ await waitForNextFrame();
+
+ target.style.animation = 'anim 10s linear';
+ target.style.animationTimeline = 't1';
+ target.style.animationDelayStart = options.startDelay;
+ target.style.animationDelayEnd = options.endDelay;
+
+ // Accommodates floating point precision errors at the endpoints.
+ target.style.animationFillMode = 'both';
+
+ // 0%
+ await assertValueAt(scroller, target,
+ { scrollTop: options.rangeStart, expected: 0 });
+ // 50%
+ await assertValueAt(scroller, target,
+ { scrollTop: (options.rangeStart + options.rangeEnd) / 2, expected: 50 });
+ // 100%
+ await assertValueAt(scroller, target,
+ { scrollTop: options.rangeEnd, expected: 100 });
+
+ // Test before/after phases (need to clear the fill mode for that).
+ target.style.animationFillMode = 'initial';
+ await assertValueAt(scroller, target,
+ { scrollTop: options.rangeStart - 10, expected: -1 });
+ await assertValueAt(scroller, target,
+ { scrollTop: options.rangeEnd + 10, expected: -1 });
+ // Check 50% again without fill mode.
+ await assertValueAt(scroller, target,
+ { scrollTop: (options.rangeStart + options.rangeEnd) / 2, expected: 50 });
+
+ }, `Animation with delays [${options.startDelay}, ${options.endDelay}]`);
+ }
+
+ test_animation_delay({
+ startDelay: 'initial',
+ endDelay: 'initial',
+ rangeStart: 600,
+ rangeEnd: 900
+ });
+
+ test_animation_delay({
+ startDelay: 'cover 0%',
+ endDelay: 'cover 100%',
+ rangeStart: 600,
+ rangeEnd: 900
+ });
+
+ test_animation_delay({
+ startDelay: 'contain 0%',
+ endDelay: 'contain 100%',
+ rangeStart: 700,
+ rangeEnd: 800
+ });
+
+ test_animation_delay({
+ startDelay: 'enter 0%',
+ endDelay: 'enter 100%',
+ rangeStart: 600,
+ rangeEnd: 700
+ });
+
+ test_animation_delay({
+ startDelay: 'exit 0%',
+ endDelay: 'exit 100%',
+ rangeStart: 800,
+ rangeEnd: 900
+ });
+
+ test_animation_delay({
+ startDelay: 'contain -50%',
+ endDelay: 'enter 200%',
+ rangeStart: 650,
+ rangeEnd: 800
+ });
+
+ test_animation_delay({
+ startDelay: 'enter 0%',
+ endDelay: 'exit 100%',
+ rangeStart: 600,
+ rangeEnd: 900
+ });
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/view-timeline-dynamic.html b/testing/web-platform/tests/scroll-animations/css/view-timeline-dynamic.html
new file mode 100644
index 0000000000..74da8850d4
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-dynamic.html
@@ -0,0 +1,183 @@
+<!DOCTYPE html>
+<title>Changes to view-timeline are reflected in dependent elements</title>
+<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#view-timeline-shorthand">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<script src="support/testcommon.js"></script>
+<style>
+ @keyframes anim {
+ from { z-index: 0; }
+ to { z-index: 100; }
+ }
+ .scroller {
+ overflow: hidden;
+ width: 100px;
+ height: 100px;
+ }
+ .scroller > div {
+ height: 100px;
+ }
+ #target {
+ height: 0px;
+ z-index: -1;
+ }
+</style>
+<main id=main></main>
+<script>
+ setup(assert_implements_animation_timeline);
+
+ function inflate(t, template) {
+ t.add_cleanup(() => main.replaceChildren());
+ main.append(template.content.cloneNode(true));
+ main.offsetTop;
+ }
+ async function scrollTop(e, value) {
+ e.scrollTop = value;
+ await waitForNextFrame();
+ }
+ async function scrollLeft(e, value) {
+ e.scrollLeft = value;
+ await waitForNextFrame();
+ }
+</script>
+
+<template id=dynamic_view_timeline_name>
+ <style>
+ .timeline {
+ view-timeline-name: t1;
+ }
+ #target {
+ animation: anim 1s linear;
+ animation-timeline: t1;
+ }
+ </style>
+ <div id=scroller class=scroller>
+ <div id=div75></div>
+ <div id=div25></div>
+ <div id=div_before></div>
+ <div id=target></div>
+ </div>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, dynamic_view_timeline_name);
+
+ await scrollTop(scroller, 50);
+
+ // scrollTop=50 is 75% for div75.
+ div75.classList.add('timeline');
+ await waitForCSSScrollTimelineStyle();
+ assert_equals(getComputedStyle(target).zIndex, '75');
+
+ // scrollTop=50 is 25% for div25.
+ div25.classList.add('timeline');
+ await waitForCSSScrollTimelineStyle();
+ assert_equals(getComputedStyle(target).zIndex, '25');
+
+ // scrollTop=50 is before the timeline start for div_before.
+ div_before.classList.add('timeline');
+ await waitForCSSScrollTimelineStyle();
+ assert_equals(getComputedStyle(target).zIndex, '-1');
+ // Scroll to 25% (for div_before) to verify that we're linked to that
+ // timeline.
+ await scrollTop(scroller, 150);
+ assert_equals(getComputedStyle(target).zIndex, '25');
+
+ // Now we should be back to div25's timeline, although with the new
+ // scrollTop=150, it's actually at 75%.
+ div_before.classList.remove('timeline');
+ await waitForCSSScrollTimelineStyle();
+ assert_equals(getComputedStyle(target).zIndex, '75');
+ }, 'Dynamically changing view-timeline-name');
+</script>
+
+<template id=dynamic_view_timeline_axis>
+ <style>
+ #timeline {
+ width: 100px;
+ height: 100px;
+ margin: 100px;
+ view-timeline-name: t1;
+ }
+ #target {
+ animation: anim 1s linear;
+ animation-timeline: t1;
+ }
+ </style>
+ <div id=scroller class=scroller>
+ <div id=timeline style="background: red;"></div>
+ <div id=target></div>
+ </div>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, dynamic_view_timeline_axis);
+
+ await scrollTop(scroller, 50); // 25% (vertical)
+ await scrollLeft(scroller, 20); // 10% (horizontal)
+
+ assert_equals(getComputedStyle(target).zIndex, '25');
+ timeline.style.viewTimelineAxis = 'horizontal';
+ await waitForCSSScrollTimelineStyle();
+ assert_equals(getComputedStyle(target).zIndex, '10');
+ }, 'Dynamically changing view-timeline-axis');
+</script>
+
+<template id=dynamic_view_timeline_inset>
+ <style>
+ #timeline {
+ width: 100px;
+ height: 100px;
+ margin: 100px;
+ view-timeline-name: t1;
+ }
+ #target {
+ animation: anim 1s linear;
+ animation-timeline: t1;
+ }
+ </style>
+ <div id=scroller class=scroller>
+ <div id=timeline style="background: red;"></div>
+ <div id=target></div>
+ </div>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, dynamic_view_timeline_inset);
+
+ await scrollTop(scroller, 50); // 25% (without inset).
+
+ assert_equals(getComputedStyle(target).zIndex, '25');
+ timeline.style.viewTimelineInset = '0px 50px';
+ await waitForCSSScrollTimelineStyle();
+ assert_equals(getComputedStyle(target).zIndex, '0');
+ }, 'Dynamically changing view-timeline-inset');
+</script>
+
+<template id=timeline_display_none>
+ <style>
+ #timeline {
+ view-timeline-name: t1;
+ }
+ #target {
+ animation: anim 1s linear;
+ animation-timeline: t1;
+ }
+ </style>
+ <div id=scroller class=scroller>
+ <div></div>
+ <div id=timeline></div>
+ <div id=target></div>
+ </div>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, timeline_display_none);
+
+ await scrollTop(scroller, 50);
+ assert_equals(getComputedStyle(target).zIndex, '25');
+ timeline.style.display = 'none';
+ assert_equals(getComputedStyle(target).zIndex, '-1');
+ }, 'Element with view-timeline becoming display:none');
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/view-timeline-inset-animation.html b/testing/web-platform/tests/scroll-animations/css/view-timeline-inset-animation.html
new file mode 100644
index 0000000000..a95086b62b
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-inset-animation.html
@@ -0,0 +1,768 @@
+<!DOCTYPE html>
+<title>Animations using view-timeline-inset</title>
+<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#propdef-view-timeline-inset">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<script src="support/testcommon.js"></script>
+<style>
+ @keyframes anim {
+ from { z-index: 0; }
+ to { z-index: 100; }
+ }
+ #scroller {
+ overflow: hidden;
+ width: 80px;
+ height: 100px;
+ }
+ #target {
+ margin: 150px;
+ width: 50px;
+ height: 50px;
+ z-index: -1;
+ }
+</style>
+<main id=main></main>
+<script>
+ setup(assert_implements_animation_timeline);
+
+ function inflate(t, template) {
+ t.add_cleanup(() => main.replaceChildren());
+ main.append(template.content.cloneNode(true));
+ }
+ async function scrollTop(e, value) {
+ e.scrollTop = value;
+ await waitForNextFrame();
+ }
+ async function scrollLeft(e, value) {
+ e.scrollLeft = value;
+ await waitForNextFrame();
+ }
+ async function assertValueAt(scroller, target, args) {
+ if (args.scrollTop !== undefined)
+ await scrollTop(scroller, args.scrollTop);
+ if (args.scrollLeft !== undefined)
+ await scrollLeft(scroller, args.scrollLeft);
+ assert_equals(getComputedStyle(target).zIndex, args.expected.toString());
+ }
+</script>
+
+<!--
+ Explanation of scroll positions
+ ===============================
+
+ Please note the following:
+
+ - The scroller has a width x height of 80x100px.
+ - The content is 50x50px with a 150px margin on all sides.
+ In other words, the size of the scroller content is 200x200px.
+
+ This means that, for vertical direction scrolling, assuming no insets:
+
+ - The start offset is 50px (scroller height + 50px is 150px, which consumes
+ exactly the margin of the content).
+ - The end offset is 200px (this is where the bottom edge of the scroller has
+ just cleared the content).
+ - The halfway point is (50px + 200px) / 2 = 125px.
+
+ For horizontal direction scrolling, assuming no insets:
+
+ - The start offset is 70px (scroller width + 70px is 150px, which consumes
+ exactly the margin of the content).
+ - The end offset is 200px (this is where the left edge of the scroller has
+ just cleared the content).
+ - The halfway point is (70px + 200px) / 2 = 135px.
+
+ The start and end insets will adjust the start and end offsets accordingly,
+ and the expectations in this file explicitly write out those adjustments.
+ For example, if the start offset is normally 50px, but there's an inset of
+ 10px, we'll expect 50px + 10px rather than 60px.
+
+ Halfway-point expectations write out the adjustment from the "normal"
+ halfway-point, e.g. for start-inset:10px and end-inset:20px, we expect
+ "125px + 5px" since (20-10)/2 == 5.
+
+ Finally, note that for right-to-left and bottom-to-top scrolling directions
+ scroll offsets go the in the negative direction. This is why some expectations
+ negate all the offsets.
+-->
+
+<template id=test_one_value>
+ <style>
+ #target {
+ view-timeline: t1;
+ view-timeline-inset: 10px;
+ animation: anim 1s linear;
+ animation-timeline: t1;
+ }
+ </style>
+ <div id=scroller class=vertical>
+ <div id=target></div>
+ </div>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, test_one_value);
+ await assertValueAt(scroller, target, { scrollTop:50, expected:-1 });
+ await assertValueAt(scroller, target, { scrollTop:50 + 10, expected:0 }); // 0%
+ await assertValueAt(scroller, target, { scrollTop:125 + 0, expected:50 }); // 50%
+ await assertValueAt(scroller, target, { scrollTop:200 - 10, expected:100 }); // 100%
+ await assertValueAt(scroller, target, { scrollTop:200, expected:-1 });
+ }, 'view-timeline-inset with one value');
+</script>
+
+<template id=test_two_values>
+ <style>
+ #target {
+ view-timeline: t1;
+ view-timeline-inset: 10px 20px;
+ animation: anim 1s linear;
+ animation-timeline: t1;
+ }
+ </style>
+ <div id=scroller class=vertical>
+ <div id=target></div>
+ </div>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, test_two_values);
+ await assertValueAt(scroller, target, { scrollTop:50, expected:-1 });
+ await assertValueAt(scroller, target, { scrollTop:50 + 20, expected:0 }); // 0%
+ await assertValueAt(scroller, target, { scrollTop:125 + 5, expected:50 }); // 50%
+ await assertValueAt(scroller, target, { scrollTop:200 - 10, expected:100 }); // 100%
+ await assertValueAt(scroller, target, { scrollTop:200, expected:-1 });
+ }, 'view-timeline-inset with two values');
+</script>
+
+<template id=test_em_values>
+ <style>
+ #target {
+ font-size: 10px;
+ view-timeline: t1;
+ view-timeline-inset: 10px 2em;
+ animation: anim 1s linear;
+ animation-timeline: t1;
+ }
+ </style>
+ <div id=scroller class=vertical>
+ <div id=target></div>
+ </div>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, test_em_values);
+ await assertValueAt(scroller, target, { scrollTop:50, expected:-1 });
+ await assertValueAt(scroller, target, { scrollTop:50 + 20, expected:0 }); // 0%
+ await assertValueAt(scroller, target, { scrollTop:125 + 5, expected:50 }); // 50%
+ await assertValueAt(scroller, target, { scrollTop:200 - 10, expected:100 }); // 100%
+ await assertValueAt(scroller, target, { scrollTop:200, expected:-1 });
+ }, 'view-timeline-inset with em values');
+</script>
+
+<template id=test_percentage_values>
+ <style>
+ #target {
+ font-size: 10px;
+ view-timeline: t1;
+ view-timeline-inset: calc(5px + max(1%, 5%)) 20%;
+ animation: anim 1s linear;
+ animation-timeline: t1;
+ }
+ </style>
+ <div id=scroller class=vertical>
+ <div id=target></div>
+ </div>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, test_percentage_values);
+ await assertValueAt(scroller, target, { scrollTop:50, expected:-1 });
+ await assertValueAt(scroller, target, { scrollTop:50 + 20, expected:0 }); // 0%
+ await assertValueAt(scroller, target, { scrollTop:125 + 5, expected:50 }); // 50%
+ await assertValueAt(scroller, target, { scrollTop:200 - 10, expected:100 }); // 100%
+ await assertValueAt(scroller, target, { scrollTop:200, expected:-1 });
+ }, 'view-timeline-inset with percentage values');
+</script>
+
+<template id=test_outset>
+ <style>
+ #target {
+ view-timeline: t1;
+ view-timeline-inset: -10px -20px;
+ animation: anim 1s linear;
+ animation-timeline: t1;
+ }
+ </style>
+ <div id=scroller class=vertical>
+ <div id=target></div>
+ </div>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, test_outset);
+ await assertValueAt(scroller, target, { scrollTop:20, expected:-1 });
+ await assertValueAt(scroller, target, { scrollTop:50 - 20, expected:0 }); // 0%
+ await assertValueAt(scroller, target, { scrollTop:125 - 5, expected:50 }); // 50%
+ await assertValueAt(scroller, target, { scrollTop:200 + 10, expected:100 }); // 100%
+ await assertValueAt(scroller, target, { scrollTop:220, expected:-1 });
+ }, 'view-timeline-inset with negative values');
+</script>
+
+<template id=test_horizontal>
+ <style>
+ #target {
+ view-timeline: t1 horizontal;
+ view-timeline-inset: 10px 20px;
+ animation: anim 1s linear;
+ animation-timeline: t1;
+ }
+ </style>
+ <div id=scroller>
+ <div id=target></div>
+ </div>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, test_horizontal);
+ await assertValueAt(scroller, target, { scrollLeft:20, expected:-1 });
+ await assertValueAt(scroller, target, { scrollLeft:70 + 20, expected:0 }); // 0%
+ await assertValueAt(scroller, target, { scrollLeft:135 + 5, expected:50 }); // 50%
+ await assertValueAt(scroller, target, { scrollLeft:200 - 10, expected:100 }); // 100%
+ await assertValueAt(scroller, target, { scrollLeft:200, expected:-1 });
+ }, 'view-timeline-inset with horizontal scroller');
+</script>
+
+<template id=test_block>
+ <style>
+ #target {
+ view-timeline: t1 block;
+ view-timeline-inset: 10px 20px;
+ animation: anim 1s linear;
+ animation-timeline: t1;
+ }
+ </style>
+ <div id=scroller>
+ <div id=target></div>
+ </div>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, test_block);
+ await assertValueAt(scroller, target, { scrollTop:50, expected:-1 });
+ await assertValueAt(scroller, target, { scrollTop:50 + 20, expected:0 }); // 0%
+ await assertValueAt(scroller, target, { scrollTop:125 + 5, expected:50 }); // 50%
+ await assertValueAt(scroller, target, { scrollTop:200 - 10, expected:100 }); // 100%
+ await assertValueAt(scroller, target, { scrollTop:200, expected:-1 });
+ }, 'view-timeline-inset with block scroller');
+</script>
+
+<template id=test_inline>
+ <style>
+ #target {
+ view-timeline: t1 inline;
+ view-timeline-inset: 10px 20px;
+ animation: anim 1s linear;
+ animation-timeline: t1;
+ }
+ </style>
+ <div id=scroller>
+ <div id=target></div>
+ </div>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, test_inline);
+ await assertValueAt(scroller, target, { scrollLeft:20, expected:-1 });
+ await assertValueAt(scroller, target, { scrollLeft:70 + 20, expected:0 }); // 0%
+ await assertValueAt(scroller, target, { scrollLeft:135 + 5, expected:50 }); // 50%
+ await assertValueAt(scroller, target, { scrollLeft:200 - 10, expected:100 }); // 100%
+ await assertValueAt(scroller, target, { scrollLeft:200, expected:-1 });
+ }, 'view-timeline-inset with inline scroller');
+</script>
+
+<template id=test_auto_block>
+ <style>
+ #scroller {
+ scroll-padding-block: 10px 20px;
+ }
+ #target {
+ view-timeline: t1 block;
+ view-timeline-inset: auto auto;
+ animation: anim 1s linear;
+ animation-timeline: t1;
+ }
+ </style>
+ <div id=scroller>
+ <div id=target></div>
+ </div>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, test_auto_block);
+ await assertValueAt(scroller, target, { scrollTop:50, expected:-1 });
+ await assertValueAt(scroller, target, { scrollTop:50 + 20, expected:0 }); // 0%
+ await assertValueAt(scroller, target, { scrollTop:125 + 5, expected:50 }); // 50%
+ await assertValueAt(scroller, target, { scrollTop:200 - 10, expected:100 }); // 100%
+ await assertValueAt(scroller, target, { scrollTop:200, expected:-1 });
+ }, 'view-timeline-inset:auto, block');
+</script>
+
+<template id=test_auto_block_vertical_lr>
+ <style>
+ #scroller {
+ scroll-padding-block: 10px 20px;
+ writing-mode: vertical-lr;
+ }
+ #target {
+ view-timeline: t1 block;
+ view-timeline-inset: auto auto;
+ animation: anim 1s linear;
+ animation-timeline: t1;
+ }
+ </style>
+ <div id=scroller>
+ <div id=target></div>
+ </div>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, test_auto_block_vertical_lr);
+ await assertValueAt(scroller, target, { scrollLeft:20, expected:-1 });
+ await assertValueAt(scroller, target, { scrollLeft:70 + 20, expected:0 }); // 0%
+ await assertValueAt(scroller, target, { scrollLeft:135 + 5, expected:50 }); // 50%
+ await assertValueAt(scroller, target, { scrollLeft:200 - 10, expected:100 }); // 100%
+ await assertValueAt(scroller, target, { scrollLeft:200, expected:-1 });
+ }, 'view-timeline-inset:auto, block, vertical-lr');
+</script>
+
+<template id=test_auto_block_vertical_rl>
+ <style>
+ #scroller {
+ scroll-padding-block: 10px 20px;
+ writing-mode: vertical-rl;
+ }
+ #target {
+ view-timeline: t1 block;
+ view-timeline-inset: auto auto;
+ animation: anim 1s linear;
+ animation-timeline: t1;
+ }
+ </style>
+ <div id=scroller>
+ <div id=target></div>
+ </div>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, test_auto_block_vertical_rl);
+ // Note: this represents horizontal scrolling from right to left.
+ await assertValueAt(scroller, target, { scrollLeft:-20, expected:-1 });
+ await assertValueAt(scroller, target, { scrollLeft:-(70 + 20), expected:0 }); // 0%
+ await assertValueAt(scroller, target, { scrollLeft:-(135 + 5), expected:50 }); // 50%
+ await assertValueAt(scroller, target, { scrollLeft:-(200 - 10), expected:100 }); // 100%
+ await assertValueAt(scroller, target, { scrollLeft:-200, expected:-1 });
+ }, 'view-timeline-inset:auto, block, vertical-rl');
+</script>
+
+<template id=test_auto_inline>
+ <style>
+ #scroller {
+ scroll-padding-inline: 10px 20px;
+ }
+ #target {
+ view-timeline: t1 inline;
+ view-timeline-inset: auto auto;
+ animation: anim 1s linear;
+ animation-timeline: t1;
+ }
+ </style>
+ <div id=scroller>
+ <div id=target></div>
+ </div>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, test_auto_inline);
+ await assertValueAt(scroller, target, { scrollLeft:20, expected:-1 });
+ await assertValueAt(scroller, target, { scrollLeft:70 + 20, expected:0 }); // 0%
+ await assertValueAt(scroller, target, { scrollLeft:135 + 5, expected:50 }); // 50%
+ await assertValueAt(scroller, target, { scrollLeft:200 - 10, expected:100 }); // 100%
+ await assertValueAt(scroller, target, { scrollLeft:200, expected:-1 });
+ }, 'view-timeline-inset:auto, inline');
+</script>
+
+<template id=test_auto_inline_vertical_rl>
+ <style>
+ #scroller {
+ scroll-padding-inline: 10px 20px;
+ writing-mode: vertical-rl;
+ }
+ #target {
+ view-timeline: t1 inline;
+ view-timeline-inset: auto auto;
+ animation: anim 1s linear;
+ animation-timeline: t1;
+ }
+ </style>
+ <div id=scroller>
+ <div id=target></div>
+ </div>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, test_auto_inline_vertical_rl);
+ await assertValueAt(scroller, target, { scrollTop:50, expected:-1 });
+ await assertValueAt(scroller, target, { scrollTop:50 + 20, expected:0 }); // 0%
+ await assertValueAt(scroller, target, { scrollTop:125 + 5, expected:50 }); // 50%
+ await assertValueAt(scroller, target, { scrollTop:200 - 10, expected:100 }); // 100%
+ await assertValueAt(scroller, target, { scrollTop:200, expected:-1 });
+ }, 'view-timeline-inset:auto, inline, vertical-rl');
+</script>
+
+<template id=test_auto_inline_vertical_lr>
+ <style>
+ #scroller {
+ scroll-padding-inline: 10px 20px;
+ writing-mode: vertical-lr;
+ }
+ #target {
+ view-timeline: t1 inline;
+ view-timeline-inset: auto auto;
+ animation: anim 1s linear;
+ animation-timeline: t1;
+ }
+ </style>
+ <div id=scroller>
+ <div id=target></div>
+ </div>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, test_auto_inline_vertical_lr);
+ await assertValueAt(scroller, target, { scrollTop:50, expected:-1 });
+ await assertValueAt(scroller, target, { scrollTop:50 + 20, expected:0 }); // 0%
+ await assertValueAt(scroller, target, { scrollTop:125 + 5, expected:50 }); // 50%
+ await assertValueAt(scroller, target, { scrollTop:200 - 10, expected:100 }); // 100%
+ await assertValueAt(scroller, target, { scrollTop:200, expected:-1 });
+ }, 'view-timeline-inset:auto, inline, vertical-lr');
+</script>
+
+<template id=test_auto_inline_rtl>
+ <style>
+ #scroller {
+ scroll-padding-inline: 10px 20px;
+ direction: rtl;
+ }
+ #target {
+ view-timeline: t1 inline;
+ view-timeline-inset: auto auto;
+ animation: anim 1s linear;
+ animation-timeline: t1;
+ }
+ </style>
+ <div id=scroller>
+ <div id=target></div>
+ </div>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, test_auto_inline_rtl);
+ await assertValueAt(scroller, target, { scrollLeft:-20, expected:-1 });
+ await assertValueAt(scroller, target, { scrollLeft:-(70 + 20), expected:0 }); // 0%
+ await assertValueAt(scroller, target, { scrollLeft:-(135 + 5), expected:50 }); // 50%
+ await assertValueAt(scroller, target, { scrollLeft:-(200 - 10), expected:100 }); // 100%
+ await assertValueAt(scroller, target, { scrollLeft:-200, expected:-1 });
+ }, 'view-timeline-inset:auto, inline, rtl');
+</script>
+
+<template id=test_auto_inline_vertical_rl_rtl>
+ <style>
+ #scroller {
+ scroll-padding-inline: 10px 20px;
+ writing-mode: vertical-rl;
+ direction: rtl;
+ }
+ #target {
+ view-timeline: t1 inline;
+ view-timeline-inset: auto auto;
+ animation: anim 1s linear;
+ animation-timeline: t1;
+ }
+ </style>
+ <div id=scroller>
+ <div id=target></div>
+ </div>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, test_auto_inline_vertical_rl_rtl);
+ await assertValueAt(scroller, target, { scrollTop:-50, expected:-1 });
+ await assertValueAt(scroller, target, { scrollTop:-(50 + 20), expected:0 }); // 0%
+ await assertValueAt(scroller, target, { scrollTop:-(125 + 5), expected:50 }); // 50%
+ await assertValueAt(scroller, target, { scrollTop:-(200 - 10), expected:100 }); // 100%
+ await assertValueAt(scroller, target, { scrollTop:-200, expected:-1 });
+ }, 'view-timeline-inset:auto, inline, vertical-rl, rtl');
+</script>
+
+<template id=test_auto_inline_vertical_lr_rtl>
+ <style>
+ #scroller {
+ scroll-padding-inline: 10px 20px;
+ writing-mode: vertical-lr;
+ direction: rtl;
+ }
+ #target {
+ view-timeline: t1 inline;
+ view-timeline-inset: auto auto;
+ animation: anim 1s linear;
+ animation-timeline: t1;
+ }
+ </style>
+ <div id=scroller>
+ <div id=target></div>
+ </div>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, test_auto_inline_vertical_lr_rtl);
+ await assertValueAt(scroller, target, { scrollTop:-50, expected:-1 });
+ await assertValueAt(scroller, target, { scrollTop:-(50 + 20), expected:0 }); // 0%
+ await assertValueAt(scroller, target, { scrollTop:-(125 + 5), expected:50 }); // 50%
+ await assertValueAt(scroller, target, { scrollTop:-(200 - 10), expected:100 }); // 100%
+ await assertValueAt(scroller, target, { scrollTop:-200, expected:-1 });
+ }, 'view-timeline-inset:auto, inline, vertical-lr, rtl');
+</script>
+
+<template id=test_auto_vertical>
+ <style>
+ #scroller {
+ scroll-padding-block: 10px 20px;
+ }
+ #target {
+ view-timeline: t1 vertical;
+ view-timeline-inset: auto auto;
+ animation: anim 1s linear;
+ animation-timeline: t1;
+ }
+ </style>
+ <div id=scroller>
+ <div id=target></div>
+ </div>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, test_auto_vertical);
+ await assertValueAt(scroller, target, { scrollTop:50, expected:-1 });
+ await assertValueAt(scroller, target, { scrollTop:50 + 20, expected:0 }); // 0%
+ await assertValueAt(scroller, target, { scrollTop:125 + 5, expected:50 }); // 50%
+ await assertValueAt(scroller, target, { scrollTop:200 - 10, expected:100 }); // 100%
+ await assertValueAt(scroller, target, { scrollTop:200, expected:-1 });
+ }, 'view-timeline-inset:auto, vertical');
+</script>
+
+<template id=test_auto_vertical_vertical_rl>
+ <style>
+ #scroller {
+ scroll-padding-inline: 10px 20px;
+ writing-mode: vertical-rl;
+ }
+ #target {
+ view-timeline: t1 vertical;
+ view-timeline-inset: auto auto;
+ animation: anim 1s linear;
+ animation-timeline: t1;
+ }
+ </style>
+ <div id=scroller>
+ <div id=target></div>
+ </div>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, test_auto_vertical_vertical_rl);
+ await assertValueAt(scroller, target, { scrollTop:50, expected:-1 });
+ await assertValueAt(scroller, target, { scrollTop:50 + 20, expected:0 }); // 0%
+ await assertValueAt(scroller, target, { scrollTop:125 + 5, expected:50 }); // 50%
+ await assertValueAt(scroller, target, { scrollTop:200 - 10, expected:100 }); // 100%
+ await assertValueAt(scroller, target, { scrollTop:200, expected:-1 });
+ }, 'view-timeline-inset:auto, vertical, vertical-rl');
+</script>
+
+<template id=test_auto_vertical_vertical_rl_rtl>
+ <style>
+ #scroller {
+ scroll-padding-inline: 10px 20px;
+ writing-mode: vertical-rl;
+ direction: rtl;
+ }
+ #target {
+ view-timeline: t1 vertical;
+ view-timeline-inset: auto auto;
+ animation: anim 1s linear;
+ animation-timeline: t1;
+ }
+ </style>
+ <div id=scroller>
+ <div id=target></div>
+ </div>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, test_auto_vertical_vertical_rl_rtl);
+ await assertValueAt(scroller, target, { scrollTop:-50, expected:-1 });
+ await assertValueAt(scroller, target, { scrollTop:-(50 + 20), expected:0 }); // 0%
+ await assertValueAt(scroller, target, { scrollTop:-(125 + 5), expected:50 }); // 50%
+ await assertValueAt(scroller, target, { scrollTop:-(200 - 10), expected:100 }); // 100%
+ await assertValueAt(scroller, target, { scrollTop:-200, expected:-1 });
+ }, 'view-timeline-inset:auto, vertical, vertical-rl, rtl');
+</script>
+
+<template id=test_auto_horizontal>
+ <style>
+ #scroller {
+ scroll-padding-inline: 10px 20px;
+ }
+ #target {
+ view-timeline: t1 horizontal;
+ view-timeline-inset: auto auto;
+ animation: anim 1s linear;
+ animation-timeline: t1;
+ }
+ </style>
+ <div id=scroller>
+ <div id=target></div>
+ </div>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, test_auto_horizontal);
+ await assertValueAt(scroller, target, { scrollLeft:20, expected:-1 });
+ await assertValueAt(scroller, target, { scrollLeft:70 + 20, expected:0 }); // 0%
+ await assertValueAt(scroller, target, { scrollLeft:135 + 5, expected:50 }); // 50%
+ await assertValueAt(scroller, target, { scrollLeft:200 - 10, expected:100 }); // 100%
+ await assertValueAt(scroller, target, { scrollLeft:200, expected:-1 });
+ }, 'view-timeline-inset:auto, horizontal');
+</script>
+
+<template id=test_auto_horizontal_rtl>
+ <style>
+ #scroller {
+ scroll-padding-inline: 10px 20px;
+ direction: rtl;
+ }
+ #target {
+ view-timeline: t1 horizontal;
+ view-timeline-inset: auto auto;
+ animation: anim 1s linear;
+ animation-timeline: t1;
+ }
+ </style>
+ <div id=scroller>
+ <div id=target></div>
+ </div>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, test_auto_horizontal_rtl);
+ await assertValueAt(scroller, target, { scrollLeft:-20, expected:-1 });
+ await assertValueAt(scroller, target, { scrollLeft:-(70 + 20), expected:0 }); // 0%
+ await assertValueAt(scroller, target, { scrollLeft:-(135 + 5), expected:50 }); // 50%
+ await assertValueAt(scroller, target, { scrollLeft:-(200 - 10), expected:100 }); // 100%
+ await assertValueAt(scroller, target, { scrollLeft:-200, expected:-1 });
+ }, 'view-timeline-inset:auto, horizontal, rtl');
+</script>
+
+<template id=test_auto_horizontal_vertical_lr>
+ <style>
+ #scroller {
+ scroll-padding-block: 10px 20px;
+ writing-mode: vertical-lr;
+ }
+ #target {
+ view-timeline: t1 horizontal;
+ view-timeline-inset: auto auto;
+ animation: anim 1s linear;
+ animation-timeline: t1;
+ }
+ </style>
+ <div id=scroller>
+ <div id=target></div>
+ </div>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, test_auto_horizontal_vertical_lr);
+ await assertValueAt(scroller, target, { scrollLeft:20, expected:-1 });
+ await assertValueAt(scroller, target, { scrollLeft:70 + 20, expected:0 }); // 0%
+ await assertValueAt(scroller, target, { scrollLeft:135 + 5, expected:50 }); // 50%
+ await assertValueAt(scroller, target, { scrollLeft:200 - 10, expected:100 }); // 100%
+ await assertValueAt(scroller, target, { scrollLeft:200, expected:-1 });
+ }, 'view-timeline-inset:auto, horizontal, vertical-lr');
+</script>
+
+<template id=test_auto_horizontal_vertical_rl>
+ <style>
+ #scroller {
+ scroll-padding-block: 10px 20px;
+ writing-mode: vertical-rl;
+ }
+ #target {
+ view-timeline: t1 horizontal;
+ view-timeline-inset: auto auto;
+ animation: anim 1s linear;
+ animation-timeline: t1;
+ }
+ </style>
+ <div id=scroller>
+ <div id=target></div>
+ </div>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, test_auto_horizontal_vertical_rl);
+ await assertValueAt(scroller, target, { scrollLeft:-20, expected:-1 });
+ await assertValueAt(scroller, target, { scrollLeft:-(70 + 20), expected:0 }); // 0%
+ await assertValueAt(scroller, target, { scrollLeft:-(135 + 5), expected:50 }); // 50%
+ await assertValueAt(scroller, target, { scrollLeft:-(200 - 10), expected:100 }); // 100%
+ await assertValueAt(scroller, target, { scrollLeft:-200, expected:-1 });
+ }, 'view-timeline-inset:auto, horizontal, vertical-rl');
+</script>
+
+<template id=test_auto_mix>
+ <style>
+ #scroller {
+ font-size: 10px;
+ scroll-padding-block: 50px calc(10% + 1em);
+ }
+ #target {
+ view-timeline: t1;
+ view-timeline-inset: 10% auto;
+ animation: anim 1s linear;
+ animation-timeline: t1;
+ }
+ </style>
+ <div id=scroller>
+ <div id=target></div>
+ </div>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, test_auto_mix);
+ // Note: 10% of scroller height 100px is 10px, and 1em with font-size:10px
+ // is also 10px. Hence we expect the end inset specified as calc(10% + 1em)
+ // to be 20px.
+ await assertValueAt(scroller, target, { scrollTop:50, expected:-1 });
+ await assertValueAt(scroller, target, { scrollTop:50 + 20, expected:0 }); // 0%
+ await assertValueAt(scroller, target, { scrollTop:125 + 5, expected:50 }); // 50%
+ await assertValueAt(scroller, target, { scrollTop:200 - 10, expected:100 }); // 100%
+ await assertValueAt(scroller, target, { scrollTop:200, expected:-1 });
+ }, 'view-timeline-inset:auto, mix');
+</script>
+
+<!--
+ TODO: How to test view-timeline:auto + scroll-padding:auto? The UA may
+ in theory use any value in that case.
+
+ https://drafts.csswg.org/css-scroll-snap-1/#valdef-scroll-padding-auto
+-->
diff --git a/testing/web-platform/tests/scroll-animations/css/view-timeline-inset-computed.html b/testing/web-platform/tests/scroll-animations/css/view-timeline-inset-computed.html
new file mode 100644
index 0000000000..77683a4678
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-inset-computed.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#view-timeline-inset">
+<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/7243">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/computed-testcommon.js"></script>
+<style>
+ #outer { font-size:10px; }
+ #outer { view-timeline-inset: 1px 2px, auto 3px; }
+ #target { view-timeline-inset: 42px; }
+</style>
+<div id=outer>
+ <div id=target></div>
+</div>
+<script>
+test_computed_value('view-timeline-inset', 'initial', '0px');
+test_computed_value('view-timeline-inset', 'inherit', '1px 2px, auto 3px');
+test_computed_value('view-timeline-inset', 'unset', '0px');
+test_computed_value('view-timeline-inset', 'revert', '0px');
+test_computed_value('view-timeline-inset', '1px');
+test_computed_value('view-timeline-inset', '1%');
+test_computed_value('view-timeline-inset', 'calc(1% + 1px)');
+test_computed_value('view-timeline-inset', '1px 2px');
+test_computed_value('view-timeline-inset', '1px 2em', '1px 20px');
+test_computed_value('view-timeline-inset', 'calc(1px + 1em) 2px', '11px 2px');
+test_computed_value('view-timeline-inset', '1px 2px, 3px 4px');
+test_computed_value('view-timeline-inset', '1px auto, auto 4px');
+test_computed_value('view-timeline-inset', '1px, 2px, 3px');
+test_computed_value('view-timeline-inset', '1px 1px, 2px 3px', '1px, 2px 3px');
+test_computed_value('view-timeline-inset', 'auto auto, auto auto', 'auto, auto');
+
+test(() => {
+ let style = getComputedStyle(document.getElementById('target'));
+ assert_not_equals(Array.from(style).indexOf('view-timeline-inset'), -1);
+}, 'The view-timeline-inset property shows up in CSSStyleDeclaration enumeration');
+
+test(() => {
+ let style = document.getElementById('target').style;
+ assert_not_equals(style.cssText.indexOf('view-timeline-inset'), -1);
+}, 'The view-timeline-inset property shows up in CSSStyleDeclaration.cssText');
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/view-timeline-inset-parsing.html b/testing/web-platform/tests/scroll-animations/css/view-timeline-inset-parsing.html
new file mode 100644
index 0000000000..d502b13593
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-inset-parsing.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#view-timeline-inset">
+<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/7243">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/parsing-testcommon.js"></script>
+<div id="target"></div>
+<script>
+test_valid_value('view-timeline-inset', 'initial');
+test_valid_value('view-timeline-inset', 'inherit');
+test_valid_value('view-timeline-inset', 'unset');
+test_valid_value('view-timeline-inset', 'revert');
+
+test_valid_value('view-timeline-inset', '1px');
+test_valid_value('view-timeline-inset', '1px 2px');
+test_valid_value('view-timeline-inset', '1px 2em');
+test_valid_value('view-timeline-inset', 'calc(1em + 1px) 2px');
+test_valid_value('view-timeline-inset', '1px 2px, 3px 4px');
+test_valid_value('view-timeline-inset', '1px auto, auto 4px');
+test_valid_value('view-timeline-inset', '1px, 2px, 3px');
+test_valid_value('view-timeline-inset', '1px 1px, 2px 3px', '1px, 2px 3px');
+test_valid_value('view-timeline-inset', 'auto auto, auto auto', 'auto, auto');
+
+test_invalid_value('view-timeline-inset', 'none');
+test_invalid_value('view-timeline-inset', 'foo bar');
+test_invalid_value('view-timeline-inset', '"foo" "bar"');
+test_invalid_value('view-timeline-inset', 'rgb(1, 2, 3)');
+test_invalid_value('view-timeline-inset', '#fefefe');
+test_invalid_value('view-timeline-inset', '1px 2px 3px');
+test_invalid_value('view-timeline-inset', '1px 2px auto');
+test_invalid_value('view-timeline-inset', 'auto 2px 3px');
+test_invalid_value('view-timeline-inset', 'auto auto auto');
+test_invalid_value('view-timeline-inset', '1px / 2px');
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/view-timeline-lookup.html b/testing/web-platform/tests/scroll-animations/css/view-timeline-lookup.html
new file mode 100644
index 0000000000..c1797c7ba1
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-lookup.html
@@ -0,0 +1,301 @@
+<!DOCTYPE html>
+<title>Named view-timeline lookup</title>
+<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#view-timelines-named">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<script src="support/testcommon.js"></script>
+<style>
+ @keyframes anim {
+ from { z-index: 0; }
+ to { z-index: 100; }
+ }
+ .scroller {
+ overflow: auto;
+ width: 100px;
+ height: 100px;
+ }
+ .scroller > div {
+ height: 25px;
+ z-index: -1;
+ }
+</style>
+<main id=main></main>
+<script>
+ function inflate(t, template) {
+ t.add_cleanup(() => main.replaceChildren());
+ main.append(template.content.cloneNode(true));
+ main.offsetTop;
+ }
+</script>
+
+<template id=timeline_self>
+ <style>
+ #target {
+ height: 0px;
+ view-timeline: t1;
+ animation: anim 1s linear;
+ animation-timeline: t1;
+ }
+ </style>
+ <div id=scroller class=scroller>
+ <div id=target></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ </div>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, timeline_self);
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(target).zIndex, '100');
+ }, 'view-timeline on self');
+</script>
+
+<template id=timeline_preceding_sibling>
+ <style>
+ #timeline {
+ height: 0px;
+ view-timeline: t1;
+ }
+ #target {
+ animation: anim 1s linear;
+ animation-timeline: t1;
+ }
+ </style>
+ <div id=scroller class=scroller>
+ <div></div>
+ <div id=timeline></div>
+ <div></div>
+ <div></div>
+ <div id=target></div>
+ <div></div>
+ <div></div>
+ </div>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, timeline_preceding_sibling);
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(target).zIndex, '75');
+ }, 'view-timeline on preceding sibling');
+</script>
+
+<template id=timeline_ancestor>
+ <style>
+ #timeline {
+ height: 0px;
+ view-timeline: t1;
+ }
+ #target {
+ animation: anim 1s linear;
+ animation-timeline: t1;
+ }
+ </style>
+ <div id=scroller class=scroller>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div id=timeline>
+ <div>
+ <div id=target></div>
+ </div>
+ </div>
+ <div></div>
+ <div></div>
+ </div>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, timeline_ancestor);
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(target).zIndex, '25');
+ }, 'view-timeline on ancestor');
+</script>
+
+<template id=timeline_ancestor_sibling>
+ <style>
+ #timeline {
+ height: 0px;
+ view-timeline: t1;
+ }
+ #target {
+ animation: anim 1s linear;
+ animation-timeline: t1;
+ }
+ </style>
+ <div id=scroller class=scroller>
+ <div></div>
+ <div id=timeline></div>
+ <div></div>
+ <div>
+ <div>
+ <div id=target></div>
+ </div>
+ </div>
+ <div></div>
+ <div></div>
+ </div>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, timeline_ancestor_sibling);
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(target).zIndex, '75');
+ }, 'view-timeline on ancestor sibling');
+</script>
+
+<template id=timeline_ancestor_sibling_closest>
+ <style>
+ #timeline1, #timeline2 {
+ height: 0px;
+ view-timeline: t1;
+ }
+ #target {
+ animation: anim 1s linear;
+ animation-timeline: t1;
+ }
+ </style>
+ <div id=scroller class=scroller>
+ <div></div>
+ <div id=timeline1></div>
+ <div></div>
+ <div id=timeline2></div>
+ <div>
+ <div>
+ <div id=target></div>
+ </div>
+ </div>
+ <div></div>
+ <div></div>
+ </div>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, timeline_ancestor_sibling_closest);
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(target).zIndex, '50');
+ }, 'view-timeline on ancestor sibling, closest wins');
+</script>
+
+<template id=timeline_ancestor_sibling_skips_nonmatching>
+ <style>
+ #timeline1 {
+ height: 0px;
+ view-timeline: t1;
+ }
+ #timeline2 {
+ height: 0px;
+ view-timeline: t2;
+ }
+ #target {
+ animation: anim 1s linear;
+ animation-timeline: t1;
+ }
+ </style>
+ <div id=scroller class=scroller>
+ <div></div>
+ <div id=timeline1></div>
+ <div></div>
+ <div id=timeline2></div>
+ <div>
+ <div>
+ <div id=target></div>
+ </div>
+ </div>
+ <div></div>
+ <div></div>
+ </div>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, timeline_ancestor_sibling_skips_nonmatching);
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(target).zIndex, '75');
+ }, 'view-timeline on ancestor sibling, skips nonmatching names');
+</script>
+
+<template id=timeline_ancestor_closer_scroll_timeline_wins>
+ <style>
+ #timeline {
+ height: 0px;
+ view-timeline: t1;
+ }
+ #scroll {
+ overflow: auto;
+ scroll-timeline: t1;
+ }
+ #scroll > div {
+ height: 50px;
+ }
+ #target {
+ animation: anim 1s linear;
+ animation-timeline: t1;
+ }
+ </style>
+ <div id=scroller class=scroller>
+ <div></div>
+ <div id=timeline></div>
+ <div></div>
+ <div id=scroll>
+ <div></div>
+ </div>
+ <div>
+ <div>
+ <div id=target></div>
+ </div>
+ </div>
+ <div></div>
+ <div></div>
+ </div>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, timeline_ancestor_closer_scroll_timeline_wins);
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(target).zIndex, '0');
+ }, 'view-timeline on ancestor sibling, closer scroll-timeline wins');
+</script>
+
+
+<template id=timeline_ancestor_view_timeline_wins_on_same_element>
+ <style>
+ #timelines {
+ height: 0px;
+ view-timeline: t1;
+ scroll-timeline: t1;
+ overflow: auto;
+ }
+ #timelines > div {
+ height: 50px;
+ }
+ #target {
+ animation: anim 1s linear;
+ animation-timeline: t1;
+ }
+ </style>
+ <div id=scroller class=scroller>
+ <div></div>
+ <div id=timelines>
+ <div></div>
+ </div>
+ <div></div>
+ <div>
+ <div>
+ <div id=target></div>
+ </div>
+ </div>
+ <div></div>
+ <div></div>
+ </div>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, timeline_ancestor_view_timeline_wins_on_same_element);
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(target).zIndex, '75');
+ }, 'view-timeline on ancestor sibling, view-timeline wins on same element');
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/view-timeline-name-computed.html b/testing/web-platform/tests/scroll-animations/css/view-timeline-name-computed.html
new file mode 100644
index 0000000000..5657dc7817
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-name-computed.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#view-timeline-name">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/computed-testcommon.js"></script>
+</head>
+<style>
+ #outer { view-timeline-name: foo, bar; }
+ #target { view-timeline-name: faz; }
+</style>
+<div id=outer>
+ <div id=target></div>
+</div>
+<script>
+test_computed_value('view-timeline-name', 'initial', 'none');
+test_computed_value('view-timeline-name', 'inherit', 'foo, bar');
+test_computed_value('view-timeline-name', 'unset', 'none');
+test_computed_value('view-timeline-name', 'revert', 'none');
+test_computed_value('view-timeline-name', 'none');
+test_computed_value('view-timeline-name', 'foo');
+test_computed_value('view-timeline-name', 'foo, bar');
+test_computed_value('view-timeline-name', 'bar, foo');
+test_computed_value('view-timeline-name', 'a, b, c, D, e');
+test_computed_value('view-timeline-name', 'none, none');
+test_computed_value('view-timeline-name', 'a, b, c, none, d, e');
+
+test(() => {
+ let style = getComputedStyle(document.getElementById('target'));
+ assert_not_equals(Array.from(style).indexOf('view-timeline-name'), -1);
+}, 'The view-timeline-name property shows up in CSSStyleDeclaration enumeration');
+
+test(() => {
+ let style = document.getElementById('target').style;
+ assert_not_equals(style.cssText.indexOf('view-timeline-name'), -1);
+}, 'The view-timeline-name property shows up in CSSStyleDeclaration.cssText');
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/view-timeline-name-parsing.html b/testing/web-platform/tests/scroll-animations/css/view-timeline-name-parsing.html
new file mode 100644
index 0000000000..3878d5c583
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-name-parsing.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#view-timeline-name">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/parsing-testcommon.js"></script>
+<div id="target"></div>
+<script>
+test_valid_value('view-timeline-name', 'initial');
+test_valid_value('view-timeline-name', 'inherit');
+test_valid_value('view-timeline-name', 'unset');
+test_valid_value('view-timeline-name', 'revert');
+
+test_valid_value('view-timeline-name', 'none');
+test_valid_value('view-timeline-name', 'abc');
+test_valid_value('view-timeline-name', ' abc', 'abc');
+test_valid_value('view-timeline-name', 'abc ', 'abc');
+test_valid_value('view-timeline-name', 'aBc');
+test_valid_value('view-timeline-name', 'foo, bar');
+test_valid_value('view-timeline-name', 'bar, foo');
+test_valid_value('view-timeline-name', 'none, none');
+test_valid_value('view-timeline-name', 'a, none, b');
+test_valid_value('view-timeline-name', 'auto');
+
+test_invalid_value('view-timeline-name', 'default');
+test_invalid_value('view-timeline-name', '10px');
+test_invalid_value('view-timeline-name', 'foo bar');
+test_invalid_value('view-timeline-name', '"foo" "bar"');
+test_invalid_value('view-timeline-name', 'rgb(1, 2, 3)');
+test_invalid_value('view-timeline-name', '#fefefe');
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/view-timeline-name-tree-scoped.html b/testing/web-platform/tests/scroll-animations/css/view-timeline-name-tree-scoped.html
new file mode 100644
index 0000000000..c216c345ed
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-name-tree-scoped.html
@@ -0,0 +1,145 @@
+<!DOCTYPE html>
+<title>view-timelime-name and tree-scoped references</title>
+<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#view-timelines-named">
+<link rel="help" src="https://drafts.csswg.org/css-scoping-1/#shadow-names">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<script src="/resources/declarative-shadow-dom-polyfill.js"></script>
+
+<main id=main></main>
+<script>
+ function inflate(t, template) {
+ t.add_cleanup(() => main.replaceChildren());
+ main.append(template.content.cloneNode(true));
+ main.offsetTop;
+ }
+
+ setup(() => {
+ polyfill_declarative_shadow_dom(document);
+ });
+</script>
+<style>
+ @keyframes anim {
+ from { z-index: 100; }
+ to { z-index: 100; }
+ }
+</style>
+
+
+<template id=view_timeline_host>
+ <style>
+ .target {
+ animation: anim 10s linear;
+ animation-timeline: timeline;
+ }
+ .scroller > div {
+ view-timeline: timeline horizontal;
+ }
+ </style>
+ <div class=scroller>
+ <div>
+ <div class=target>
+ <template shadowroot=open>
+ <style>
+ :host {
+ view-timeline: timeline vertical;
+ }
+ </style>
+ </template>
+ </div>
+ </div>
+ </div>
+ <style>
+ </style>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, view_timeline_host);
+ let target = main.querySelector('.target');
+ assert_equals(target.getAnimations().length, 1);
+ let anim = target.getAnimations()[0];
+ assert_not_equals(anim.timeline, null);
+ assert_equals(anim.timeline.axis, 'horizontal');
+ }, 'Outer animation can not see view timeline defined by :host');
+</script>
+
+
+<template id=view_timeline_slotted>
+ <style>
+ .target {
+ animation: anim 10s linear;
+ animation-timeline: timeline;
+ }
+ .host {
+ view-timeline: timeline horizontal;
+ }
+ </style>
+ <div class=scroller>
+ <div class=host>
+ <template shadowroot=open>
+ <style>
+ ::slotted(.target) {
+ view-timeline: timeline vertical;
+ }
+ </style>
+ <slot></slot>
+ </template>
+ <div class=target></div>
+ </div>
+ </div>
+ <style>
+ </style>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, view_timeline_slotted);
+ let target = main.querySelector('.target');
+ assert_equals(target.getAnimations().length, 1);
+ let anim = target.getAnimations()[0];
+ assert_not_equals(anim.timeline, null);
+ assert_equals(anim.timeline.axis, 'horizontal');
+ }, 'Outer animation can not see view timeline defined by ::slotted');
+</script>
+
+
+<template id=view_timeline_part>
+ <style>
+ .host {
+ view-timeline: timeline vertical;
+ }
+ .host::part(foo) {
+ view-timeline: timeline horizontal;
+ }
+ </style>
+ <div class=host>
+ <template shadowroot=open>
+ <style>
+ /* Not using 'anim' at document scope, due to https://crbug.com/1334534 */
+ @keyframes anim2 {
+ from { z-index: 100; }
+ to { z-index: 100; }
+ }
+ .target {
+ animation: anim2 10s linear;
+ animation-timeline: timeline;
+ }
+ </style>
+ <div part=foo>
+ <div class=target></div>
+ </div>
+ </template>
+ </div>
+ <style>
+ </style>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, view_timeline_part);
+ let target = main.querySelector('.host').shadowRoot.querySelector('.target');
+ assert_equals(target.getAnimations().length, 1);
+ let anim = target.getAnimations()[0];
+ assert_not_equals(anim.timeline, null);
+ assert_equals(anim.timeline.axis, 'horizontal');
+ }, 'Inner animation can see view timeline defined by ::part');
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/view-timeline-shorthand.tentative.html b/testing/web-platform/tests/scroll-animations/css/view-timeline-shorthand.tentative.html
new file mode 100644
index 0000000000..f3efa88c92
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-shorthand.tentative.html
@@ -0,0 +1,104 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#view-timeline-shorthand">
+<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/7627">
+<link rel="help" href="https://github.com/w3c/csswg-drafts/pull/7694">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/computed-testcommon.js"></script>
+<script src="/css/support/parsing-testcommon.js"></script>
+<script src="/css/support/shorthand-testcommon.js"></script>
+<div id="target"></div>
+<script>
+test_valid_value('view-timeline', 'abcd');
+test_valid_value('view-timeline', 'none block', 'none');
+test_valid_value('view-timeline', 'none inline');
+
+// view-timeline-name: inline/block/horizontal/vertical.
+test_valid_value('view-timeline', 'inline block', 'inline');
+test_valid_value('view-timeline', 'block block', 'block');
+test_valid_value('view-timeline', 'vertical block', 'vertical');
+test_valid_value('view-timeline', 'horizontal block', 'horizontal');
+
+test_valid_value('view-timeline', 'a, b, c');
+test_valid_value('view-timeline', 'a inline, b block, c vertical', 'a inline, b, c vertical');
+test_valid_value('view-timeline', 'auto');
+
+test_invalid_value('view-timeline', 'abc abc');
+test_invalid_value('view-timeline', 'block none');
+test_invalid_value('view-timeline', 'none none');
+test_invalid_value('view-timeline', 'default');
+test_invalid_value('view-timeline', ',');
+test_invalid_value('view-timeline', ',,block,,');
+
+test_computed_value('view-timeline', 'abcd');
+test_computed_value('view-timeline', 'none block', 'none');
+test_computed_value('view-timeline', 'none inline');
+test_computed_value('view-timeline', 'inline block', 'inline');
+test_computed_value('view-timeline', 'block block', 'block');
+test_computed_value('view-timeline', 'vertical block', 'vertical');
+test_computed_value('view-timeline', 'horizontal block', 'horizontal');
+test_computed_value('view-timeline', 'a, b, c');
+test_computed_value('view-timeline', 'a inline, b block, c vertical', 'a inline, b, c vertical');
+
+test_shorthand_value('view-timeline', 'abc vertical',
+{
+ 'view-timeline-name': 'abc',
+ 'view-timeline-axis': 'vertical',
+});
+test_shorthand_value('view-timeline', 'abc vertical, def',
+{
+ 'view-timeline-name': 'abc, def',
+ 'view-timeline-axis': 'vertical, block',
+});
+test_shorthand_value('view-timeline', 'abc, def',
+{
+ 'view-timeline-name': 'abc, def',
+ 'view-timeline-axis': 'block, block',
+});
+test_shorthand_value('view-timeline', 'inline horizontal',
+{
+ 'view-timeline-name': 'inline',
+ 'view-timeline-axis': 'horizontal',
+});
+
+function test_shorthand_contraction(shorthand, longhands, expected) {
+ let longhands_fmt = Object.entries(longhands).map((e) => `${e[0]}:${e[1]}`).join(';');
+ test((t) => {
+ t.add_cleanup(() => {
+ for (let shorthand of Object.keys(longhands))
+ target.style.removeProperty(shorthand);
+ });
+ for (let [shorthand, value] of Object.entries(longhands))
+ target.style.setProperty(shorthand, value);
+ assert_equals(target.style.getPropertyValue(shorthand), expected, 'Declared value');
+ assert_equals(getComputedStyle(target).getPropertyValue(shorthand), expected, 'Computed value');
+ }, `Shorthand contraction of ${longhands_fmt}`);
+}
+
+test_shorthand_contraction('view-timeline', {
+ 'view-timeline-name': 'abc',
+ 'view-timeline-axis': 'inline',
+}, 'abc inline');
+
+test_shorthand_contraction('view-timeline', {
+ 'view-timeline-name': 'a, b',
+ 'view-timeline-axis': 'inline, block',
+}, 'a inline, b');
+
+test_shorthand_contraction('view-timeline', {
+ 'view-timeline-name': 'none, none',
+ 'view-timeline-axis': 'block, block',
+}, 'none, none');
+
+// Longhands with different lengths:
+
+test_shorthand_contraction('view-timeline', {
+ 'view-timeline-name': 'a, b, c',
+ 'view-timeline-axis': 'inline, inline',
+}, '');
+
+test_shorthand_contraction('view-timeline', {
+ 'view-timeline-name': 'a, b',
+ 'view-timeline-axis': 'inline, inline, inline',
+}, '');
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/css/view-timeline-used-values.html b/testing/web-platform/tests/scroll-animations/css/view-timeline-used-values.html
new file mode 100644
index 0000000000..6627eeb998
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-used-values.html
@@ -0,0 +1,104 @@
+<!DOCTYPE html>
+<title>Used values of view-timeline properties</title>
+<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#view-timeline-axis">
+<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#view-timeline-name">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<script src="support/testcommon.js"></script>
+<style>
+ @keyframes anim {
+ from { z-index: 0; }
+ to { z-index: 100; }
+ }
+ .scroller {
+ overflow: hidden;
+ width: 100px;
+ height: 100px;
+ }
+ .scroller > div {
+ width: 300px;
+ height: 300px;
+ z-index: -1;
+ }
+</style>
+<main id=main></main>
+<script>
+ setup(assert_implements_animation_timeline);
+
+ function inflate(t, template) {
+ t.add_cleanup(() => main.replaceChildren());
+ main.append(template.content.cloneNode(true));
+ }
+ async function scrollTop(e, value) {
+ e.scrollTop = value;
+ await waitForNextFrame();
+ }
+ async function scrollLeft(e, value) {
+ e.scrollLeft = value;
+ await waitForNextFrame();
+ }
+</script>
+
+<template id=omitted_axis>
+ <style>
+ #target {
+ view-timeline-name: t1, t2; /* Two items */
+ view-timeline-axis: inline; /* One item */
+ animation: anim 1s linear;
+ animation-timeline: t2;
+ }
+ </style>
+ <div id=scroller class=scroller>
+ <div id=target></div>
+ </div>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, omitted_axis);
+ assert_equals(getComputedStyle(target).zIndex, '-1');
+
+ // enter 0% is at scrollTop/Left = -100
+ // exit 100% is at scrollTop/Left = 300
+ // This means that at scrollTop/Left=0, the animation is at 25%.
+
+ await scrollTop(scroller, 0);
+ await scrollLeft(scroller, 0);
+ assert_equals(getComputedStyle(target).zIndex, '25');
+
+ // The timeline should be inline-axis:
+ await scrollTop(scroller, 100); // 50%
+ await scrollLeft(scroller, 40); // 35%
+ assert_equals(getComputedStyle(target).zIndex, '35');
+ }, 'Use the last value from view-timeline-axis if omitted');
+</script>
+
+<template id=omitted_inset>
+ <style>
+ #target {
+ view-timeline-name: t1, t2; /* Two items */
+ view-timeline-inset: 100px; /* One item */
+ animation: anim 1s linear;
+ animation-timeline: t2;
+ }
+ </style>
+ <div id=scroller class=scroller>
+ <div id=target></div>
+ </div>
+</template>
+<script>
+ promise_test(async (t) => {
+ inflate(t, omitted_inset);
+ assert_equals(getComputedStyle(target).zIndex, '-1');
+
+ // 0% is normally at at scrollTop = -100
+ // 100% is normally at scrollTop/Left = 300
+ // However, we have a 100px inset in both ends, which makes the
+ // range [0, 200].
+
+ await scrollTop(scroller, 0);
+ assert_equals(getComputedStyle(target).zIndex, '0');
+ await scrollTop(scroller, 100); // 50%
+ assert_equals(getComputedStyle(target).zIndex, '50');
+ }, 'Use the last value from view-timeline-inset if omitted');
+</script>