summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/scroll-animations/view-timelines
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
commit0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch)
treea31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /testing/web-platform/tests/scroll-animations/view-timelines
parentInitial commit. (diff)
downloadfirefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.tar.xz
firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.zip
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/scroll-animations/view-timelines')
-rw-r--r--testing/web-platform/tests/scroll-animations/view-timelines/block-view-timeline-current-time-vertical-rl.tentative.html97
-rw-r--r--testing/web-platform/tests/scroll-animations/view-timelines/block-view-timeline-current-time.tentative.html205
-rw-r--r--testing/web-platform/tests/scroll-animations/view-timelines/block-view-timeline-nested-subject.tentative.html113
-rw-r--r--testing/web-platform/tests/scroll-animations/view-timelines/change-animation-range-updates-play-state.html77
-rw-r--r--testing/web-platform/tests/scroll-animations/view-timelines/get-keyframes-with-timeline-offset.html203
-rw-r--r--testing/web-platform/tests/scroll-animations/view-timelines/inline-view-timeline-current-time.tentative.html301
-rw-r--r--testing/web-platform/tests/scroll-animations/view-timelines/testcommon.js145
-rw-r--r--testing/web-platform/tests/scroll-animations/view-timelines/timeline-offset-in-keyframe.html263
-rw-r--r--testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-get-current-time-range-name.html148
-rw-r--r--testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-get-set-range.html120
-rw-r--r--testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-inset.html226
-rw-r--r--testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-missing-subject.html54
-rw-r--r--testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-on-display-none-element.html59
-rw-r--r--testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-range-large-subject.html105
-rw-r--r--testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-range.html198
-rw-r--r--testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-root-source.html41
-rw-r--r--testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-snapport.html58
-rw-r--r--testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-source.tentative.html94
-rw-r--r--testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-subject-size-changes.html81
-rw-r--r--testing/web-platform/tests/scroll-animations/view-timelines/zero-intrinsic-iteration-duration.tentative.html106
20 files changed, 2694 insertions, 0 deletions
diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/block-view-timeline-current-time-vertical-rl.tentative.html b/testing/web-platform/tests/scroll-animations/view-timelines/block-view-timeline-current-time-vertical-rl.tentative.html
new file mode 100644
index 0000000000..5bc4598452
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/view-timelines/block-view-timeline-current-time-vertical-rl.tentative.html
@@ -0,0 +1,97 @@
+<!DOCTYPE html>
+<html id="top">
+<meta charset="utf-8">
+<title>View timeline current-time with vertical-rl writing mode</title>
+<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#viewtimeline-interface">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<script src="/scroll-animations/scroll-timelines/testcommon.js"></script>
+<script src="/scroll-animations/view-timelines/testcommon.js"></script>
+<style>
+ #container {
+ writing-mode: vertical-rl;
+ overflow-x: scroll;
+ height: 200px;
+ width: 200px;
+ }
+ .spacer {
+ width: 800px;
+ }
+ #target {
+ background-color: green;
+ height: 100px;
+ width: 200px;
+ }
+</style>
+<body>
+ <div id="container">
+ <div id="leading-space" class="spacer"></div>
+ <div id="target"></div>
+ <div id="trailing-space" class="spacer"></div>
+ </div>
+</body>
+<script type="text/javascript">
+ promise_test(async t => {
+ container.scrollLeft = 0;
+ await waitForNextFrame();
+
+ const anim = CreateViewTimelineOpacityAnimation(t, target, {axis: 'block'});
+ const timeline = anim.timeline;
+ await anim.ready;
+
+ // Initially before start-offset and animation effect is in the before
+ // phase.
+ assert_percents_equal(timeline.currentTime, -150,
+ "Timeline's currentTime at container start boundary");
+ assert_percents_equal(anim.currentTime, -150,
+ "Animation's currentTime at container start boundary");
+ assert_equals(getComputedStyle(target).opacity, "1",
+ 'Effect is inactive in the before phase');
+
+ // Advance to the start offset, which triggers entry to the active phase.
+ container.scrollLeft = -600;
+ await waitForNextFrame();
+ assert_percents_equal(timeline.currentTime, 0,
+ "Timeline's current time at start offset");
+ assert_percents_equal(anim.currentTime, 0,
+ "Animation's current time at start offset");
+ assert_equals(getComputedStyle(target).opacity, '0.3',
+ 'Effect at the start of the active phase');
+
+ // Advance to the midpoint of the animation.
+ container.scrollLeft = -800;
+ await waitForNextFrame();
+ assert_percents_equal(timeline.currentTime, 50,
+ "Timeline's currentTime at midpoint");
+ assert_percents_equal(anim.currentTime, 50,
+ "Animation's currentTime at midpoint");
+ assert_equals(getComputedStyle(target).opacity,'0.5',
+ 'Effect at the midpoint of the active range');
+
+ // Advance to the end of the animation.
+ container.scrollLeft = -1000;
+ await waitForNextFrame();
+ assert_percents_equal(timeline.currentTime, 100,
+ "Timeline's currentTime at end offset");
+ assert_percents_equal(anim.currentTime, 100,
+ "Animation's currentTime at end offset");
+ assert_equals(getComputedStyle(target).opacity, '0.7',
+ 'Effect is in the active phase at effect end time');
+
+ // Advance to the scroll limit.
+ container.scrollLeft = -1600;
+ await waitForNextFrame();
+ assert_percents_equal(timeline.currentTime, 250,
+ "Timeline's currentTime at scroll limit");
+ // Hold time set when the animation finishes, which clamps the value of
+ // the animation's currentTime.
+ assert_percents_equal(anim.currentTime, 100,
+ "Animation's currentTime at scroll limit");
+ // In the after phase, so the effect should not be applied.
+ assert_equals(getComputedStyle(target).opacity, '1',
+ 'After phase at scroll limit');
+ }, 'View timeline with container having vertical-rl layout' );
+
+</script>
+</html>
diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/block-view-timeline-current-time.tentative.html b/testing/web-platform/tests/scroll-animations/view-timelines/block-view-timeline-current-time.tentative.html
new file mode 100644
index 0000000000..a6530f6631
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/view-timelines/block-view-timeline-current-time.tentative.html
@@ -0,0 +1,205 @@
+<!DOCTYPE html>
+<html id="top">
+<meta charset="utf-8">
+<title>View timeline current-time</title>
+<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#viewtimeline-interface">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<script src="/scroll-animations/scroll-timelines/testcommon.js"></script>
+<script src="/scroll-animations/view-timelines/testcommon.js"></script>
+<style>
+ #container {
+ border: 10px solid lightgray;
+ overflow-y: scroll;
+ height: 200px;
+ width: 200px;
+ }
+ .spacer {
+ height: 800px;
+ }
+ #target {
+ background-color: green;
+ height: 200px;
+ width: 100px;
+ }
+</style>
+<body>
+ <div id="container">
+ <div id="leading-space" class="spacer"></div>
+ <div id="target"></div>
+ <div id="trailing-space" class="spacer"></div>
+ </div>
+</body>
+<script type="text/javascript">
+ promise_test(async t => {
+ container.scrollTop = 0;
+ await waitForNextFrame();
+
+ const anim = CreateViewTimelineOpacityAnimation(t, target);
+ const timeline = anim.timeline;
+ await anim.ready;
+
+ // Initially before start-offset and animation effect is in the before
+ // phase.
+ assert_percents_equal(timeline.currentTime, -150,
+ "Timeline's currentTime at container start boundary");
+ assert_percents_equal(anim.currentTime, -150,
+ "Animation's currentTime at container start boundary");
+ assert_equals(getComputedStyle(target).opacity, "1",
+ 'Effect is inactive in the before phase');
+
+ // Advance to the start offset, which triggers entry to the active phase.
+ container.scrollTop = 600;
+ await waitForNextFrame();
+ assert_percents_equal(timeline.currentTime, 0,
+ "Timeline's current time at start offset");
+ assert_percents_equal(anim.currentTime, 0,
+ "Animation's current time at start offset");
+ assert_equals(getComputedStyle(target).opacity, '0.3',
+ 'Effect at the start of the active phase');
+
+ // Advance to the midpoint of the animation.
+ container.scrollTop = 800;
+ await waitForNextFrame();
+ assert_percents_equal(timeline.currentTime, 50,
+ "Timeline's currentTime at midpoint");
+ assert_percents_equal(anim.currentTime, 50,
+ "Animation's currentTime at midpoint");
+ assert_equals(getComputedStyle(target).opacity,'0.5',
+ 'Effect at the midpoint of the active range');
+
+ // Advance to the end of the animation.
+ container.scrollTop = 1000;
+ await waitForNextFrame();
+ assert_percents_equal(timeline.currentTime, 100,
+ "Timeline's currentTime at end offset");
+ assert_percents_equal(anim.currentTime, 100,
+ "Animation's currentTime at end offset");
+ assert_equals(getComputedStyle(target).opacity, '0.7',
+ 'Effect is in the active phase at effect end time');
+
+ // Advance to the scroll limit.
+ container.scrollTop = 1600;
+ await waitForNextFrame();
+ assert_percents_equal(timeline.currentTime, 250,
+ "Timeline's currentTime at scroll limit");
+ // Hold time set when the animation finishes, which clamps the value of
+ // the animation's currentTime.
+ assert_percents_equal(anim.currentTime, 100,
+ "Animation's currentTime at scroll limit");
+ // In the after phase, so the effect should not be applied.
+ assert_equals(getComputedStyle(target).opacity, '1',
+ 'After phase at scroll limit');
+ }, 'View timeline with start and end scroll offsets that do not align with ' +
+ 'the scroll boundaries' );
+
+ promise_test(async t => {
+ const leading = document.getElementById('leading-space');
+ leading.style = 'display: none';
+ t.add_cleanup(() => {
+ leading.style = null;
+ });
+
+ container.scrollTop = 0;
+ await waitForNextFrame();
+
+ const anim = CreateViewTimelineOpacityAnimation(t, target);
+ const timeline = anim.timeline;
+ await anim.ready;
+
+ assert_percents_equal(timeline.currentTime, 50,
+ "Timeline's currentTime at container start boundary");
+ assert_percents_equal(anim.currentTime, 50,
+ "Animation's currentTime at container start boundary");
+ assert_equals(getComputedStyle(target).opacity, "0.5",
+ 'Effect enters active phase at container start boundary');
+
+
+ // Advance to midpoint
+ container.scrollTop = 100;
+ await waitForNextFrame();
+ assert_percents_equal(timeline.currentTime, 75,
+ "Timeline's current time at start offset");
+ assert_percents_equal(anim.currentTime, 75,
+ "Animation's current time at start offset");
+ assert_equals(getComputedStyle(target).opacity, '0.6',
+ 'Effect at the start of the active phase');
+
+ // Advance to end-offset
+ container.scrollTop = 200;
+ await waitForNextFrame();
+ assert_percents_equal(timeline.currentTime, 100,
+ "Timeline's current time at start offset");
+ assert_percents_equal(anim.currentTime, 100,
+ "Animation's current time at start offset");
+ assert_equals(getComputedStyle(target).opacity, '0.7',
+ 'Effect at the start of the active phase');
+
+ // Advance to scroll limit.
+ container.scrollTop = 800;
+ await waitForNextFrame();
+ assert_percents_equal(timeline.currentTime, 250,
+ "Timeline's current time at start offset");
+ assert_percents_equal(anim.currentTime, 100,
+ "Animation's current time at start offset");
+ assert_equals(getComputedStyle(target).opacity, '1',
+ 'Effect at the start of the active phase');
+
+ }, 'View timeline does not clamp starting scroll offset at 0');
+
+ promise_test(async t => {
+ const trailing = document.getElementById('trailing-space');
+ trailing.style = 'display: none';
+ t.add_cleanup(() => {
+ trailing.style = null;
+ });
+
+ container.scrollTop = 0;
+ await waitForNextFrame();
+
+ const anim = CreateViewTimelineOpacityAnimation(t, target);
+ const timeline = anim.timeline;
+ await anim.ready;
+
+ // Initially in before phase.
+ assert_percents_equal(timeline.currentTime, -150,
+ "Timeline's currentTime at container start boundary");
+ assert_percents_equal(anim.currentTime, -150,
+ "Animation's currentTime at container start boundary");
+ assert_equals(getComputedStyle(target).opacity, "1",
+ 'Effect enters active phase at container start boundary');
+
+ // Advance to start offset.
+ container.scrollTop = 600;
+ await waitForNextFrame();
+ assert_percents_equal(timeline.currentTime, 0,
+ "Timeline's current time at start offset");
+ assert_percents_equal(anim.currentTime, 0,
+ "Animation's current time at start offset");
+ assert_equals(getComputedStyle(target).opacity, '0.3',
+ 'Effect at the start of the active phase');
+
+ // Advance to midpoint.
+ container.scrollTop = 700;
+ await waitForNextFrame();
+ assert_percents_equal(timeline.currentTime, 25,
+ "Timeline's current time at start offset");
+ assert_percents_equal(anim.currentTime, 25,
+ "Animation's current time at start offset");
+ assert_equals(getComputedStyle(target).opacity, '0.4',
+ 'Effect at the start of the active phase');
+
+ // Advance to end offset.
+ container.scrollTop = 800;
+ await waitForNextFrame();
+ assert_percents_equal(timeline.currentTime, 50,
+ "Timeline's currentTime at max scroll offset");
+ assert_percents_equal(anim.currentTime, 50,
+ "Animation's currentTime at max scroll offset");
+ assert_equals(getComputedStyle(target).opacity, "0.5",
+ 'Effect at end of active phase');
+ }, 'View timeline does not clamp end scroll offset at max scroll');
+
+</script>
+</html>
diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/block-view-timeline-nested-subject.tentative.html b/testing/web-platform/tests/scroll-animations/view-timelines/block-view-timeline-nested-subject.tentative.html
new file mode 100644
index 0000000000..2cc8af882f
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/view-timelines/block-view-timeline-nested-subject.tentative.html
@@ -0,0 +1,113 @@
+<!DOCTYPE html>
+<html id="top">
+<meta charset="utf-8">
+<title>View timeline nested subject</title>
+<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#viewtimeline-interface">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<script src="/scroll-animations/scroll-timelines/testcommon.js"></script>
+<script src="/scroll-animations/view-timelines/testcommon.js"></script>
+<style type="text/css">
+ #container {
+ overflow-y: scroll;
+ height: 300px;
+ width: 300px;
+ }
+ .big-spacer {
+ height: 800px;
+ }
+ .small-spacer {
+ height: 100px;
+ }
+ #block {
+ background-color: #ddd;
+ }
+ #target {
+ background-color: green;
+ height: 100px;
+ width: 100px;
+ }
+</style>
+<body>
+ <div id="container">
+ <div class="big-spacer"></div>
+ <div id="block">
+ <div class="small-spacer"></div>
+ <div id="target"></div>
+ </div>
+ <div class="big-spacer"></div>
+ </div>
+</body>
+<script type="text/javascript">
+ promise_test(async t => {
+ container.scrollTop = 0;
+ await waitForNextFrame();
+
+ const anim = CreateViewTimelineOpacityAnimation(t, target);
+ const timeline = anim.timeline;
+ await anim.ready;
+
+ // start offset = 800 + 100 - 300 = 600
+ // end offset = 800 + 100 + 100 = 1000
+ // scroll limit = L = 800 + 200 + 800 - 300 = 1500
+ // progress = P = (current - start) / (end - start)
+ // P(0) = -600 / 400 = -1.5
+ // P(L) = 900 / 400 = 2.5
+
+ // Initially before start-offset and animation effect is in the before
+ // phase.
+ assert_percents_equal(timeline.currentTime, -150,
+ "Timeline's currentTime at container start boundary");
+ assert_percents_equal(anim.currentTime, -150,
+ "Animation's currentTime at container start boundary");
+ assert_equals(getComputedStyle(target).opacity, "1",
+ 'Effect is inactive in the before phase');
+
+
+ // Advance to the start offset, which triggers entry to the active phase.
+ container.scrollTop = 600;
+ await waitForNextFrame();
+ assert_percents_equal(timeline.currentTime, 0,
+ "Timeline's current time at start offset");
+ assert_percents_equal(anim.currentTime, 0,
+ "Animation's current time at start offset");
+ assert_equals(getComputedStyle(target).opacity, '0.3',
+ 'Effect at the start of the active phase');
+
+ // Advance to the midpoint of the animation.
+ container.scrollTop = 800;
+ await waitForNextFrame();
+ assert_percents_equal(timeline.currentTime, 50,
+ "Timeline's currentTime at midpoint");
+ assert_percents_equal(anim.currentTime, 50,
+ "Animation's currentTime at midpoint");
+ assert_equals(getComputedStyle(target).opacity,'0.5',
+ 'Effect at the midpoint of the active range');
+
+ // Advance to the end of the animation.
+ container.scrollTop = 1000;
+ await waitForNextFrame();
+ assert_percents_equal(timeline.currentTime, 100,
+ "Timeline's currentTime at end offset");
+ assert_percents_equal(anim.currentTime, 100,
+ "Animation's currentTime at end offset");
+ assert_equals(getComputedStyle(target).opacity, '0.7',
+ 'Effect is in the active phase at effect end time');
+
+ // Advance to the scroll limit.
+ container.scrollTop = 1600;
+ await waitForNextFrame();
+ assert_percents_equal(timeline.currentTime, 225,
+ "Timeline's currentTime at scroll limit");
+ // Hold time set when the animation finishes, which clamps the value of
+ // the animation's currentTime.
+ assert_percents_equal(anim.currentTime, 100,
+ "Animation's currentTime at scroll limit");
+ // In the after phase, so the effect should not be applied.
+ assert_equals(getComputedStyle(target).opacity, '1',
+ 'After phase at scroll limit');
+ }, 'View timeline with subject that is not a direct descendant of the ' +
+ 'scroll container');
+</script>
+</html>
diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/change-animation-range-updates-play-state.html b/testing/web-platform/tests/scroll-animations/view-timelines/change-animation-range-updates-play-state.html
new file mode 100644
index 0000000000..53330d32f1
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/view-timelines/change-animation-range-updates-play-state.html
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<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>
+<script src="/web-animations/resources/keyframe-utils.js"></script>
+<script src="/scroll-animations/scroll-timelines/testcommon.js"></script>
+<title>Animation range updates play state</title>
+</head>
+<style type="text/css">
+ @keyframes anim {
+ from { background-color: blue; }
+ to { background-color: white; }
+ }
+ #scroller {
+ border: 10px solid lightgray;
+ overflow-y: scroll;
+ overflow-x: hidden;
+ width: 300px;
+ height: 200px;
+ }
+ #target {
+ margin-top: 800px;
+ margin-bottom: 800px;
+ margin-left: 10px;
+ margin-right: 10px;
+ width: 100px;
+ height: 100px;
+ z-index: -1;
+ background-color: green;
+ animation: anim auto both linear;
+ animation-timeline: t1;
+ view-timeline: t1;
+ }
+</style>
+<body>
+ <div id="scroller">
+ <div id="target"></div>
+ </div>
+</body>
+<script type="text/javascript">
+ async function runTest() {
+ promise_test(async t => {
+ anim = target.getAnimations()[0];
+ await anim.ready;
+
+ scroller.scrollTop = 750;
+ await waitForNextFrame();
+
+ // Animation is running in the active phase.
+ anim.rangeStart = 'contain 0%'; // 700px
+ anim.rangeEnd = 'contain 100%'; // 800px
+ assert_equals(anim.playState, 'running');
+ assert_percents_equal(anim.currentTime, 100/6);
+
+ // Animation in the after phase and switches to the finished state.
+ anim.rangeStart = 'entry 0%'; // 600px
+ anim.rangeEnd = 'entry 100%'; // 700px
+ assert_equals(anim.playState, 'finished');
+ // Clamp to effect end when finished.
+ assert_percents_equal(anim.currentTime, 100/3);
+
+ // Animation in the before phase and switches back to the running state.
+ anim.rangeStart = 'exit 0%'; // 800px
+ anim.rangeEnd = 'exit 100%'; // 900px
+ assert_equals(anim.playState, 'running');
+ assert_percents_equal(anim.currentTime, -100/6);
+
+ }, 'Changing the animation range updates the play state');
+ }
+
+ window.onload = runTest;
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/get-keyframes-with-timeline-offset.html b/testing/web-platform/tests/scroll-animations/view-timelines/get-keyframes-with-timeline-offset.html
new file mode 100644
index 0000000000..02f910d04e
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/view-timelines/get-keyframes-with-timeline-offset.html
@@ -0,0 +1,203 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<!-- TODO(kevers): Insert link once resolutions present in spec -->
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<script src="/web-animations/resources/keyframe-utils.js"></script>
+<script src="support/testcommon.js"></script>
+<title>Reported keyframes containing timeline offset</title>
+</head>
+<style type="text/css">
+ #scroller {
+ border: 10px solid lightgray;
+ overflow-y: scroll;
+ overflow-x: hidden;
+ width: 300px;
+ height: 200px;
+ }
+ #target {
+ margin: 800px 10px;
+ width: 100px;
+ height: 100px;
+ z-index: -1;
+ background-color: green;
+ }
+</style>
+<body>
+ <div id=scroller>
+ <div id=target></div>
+ </div>
+</body>
+<script type="text/javascript">
+ async function runTest() {
+ function createAnimation(t, keyframes, use_view_timeline = true) {
+ const options = {
+ rangeStart: { rangeName: 'contain', offset: CSS.percent(0) },
+ rangeEnd: { rangeName: 'contain', offset: CSS.percent(100) },
+ duration: 'auto',
+ fill: 'both'
+ };
+ if (use_view_timeline) {
+ options.timeline = new ViewTimeline( { subject: target });
+ }
+ const anim = target.animate(keyframes, options);
+ t.add_cleanup(() => {
+ anim.cancel();
+ });
+ return anim;
+ }
+
+ promise_test(async t => {
+ let anim = createAnimation(t, [
+ { offset: "contain 25%", marginLeft: "0px", opacity: "0" },
+ { offset: "contain 75%", marginRight: "0px", opacity: "1" }
+ ]);
+ let frames = anim.effect.getKeyframes();
+ let expected = [
+ { offset: { rangeName: 'contain', offset: CSS.percent(25) },
+ computedOffset: 0.25, easing: "linear", composite: "auto",
+ marginLeft: "0px", opacity: "0" },
+ { offset: { rangeName: 'contain', offset: CSS.percent(75) },
+ computedOffset: 0.75, easing: "linear", composite: "auto",
+ marginRight: "0px", opacity: "1" }
+ ];
+ assert_frame_lists_equal(frames, expected);
+ }, 'Report specified timeline offsets');
+
+ promise_test(async t => {
+ let anim = createAnimation(t, [
+ { offset: "cover 0%", marginLeft: "0px", opacity: "0" },
+ { offset: "cover 100%", marginRight: "0px", opacity: "1" }
+ ]);
+ let frames = anim.effect.getKeyframes();
+ let expected = [
+ { offset: { rangeName: 'cover', offset: CSS.percent(0) },
+ computedOffset: -1, easing: "linear", composite: "auto",
+ marginLeft: "0px", opacity: "0" },
+ { offset: { rangeName: 'cover', offset: CSS.percent(100) },
+ computedOffset: 2, easing: "linear", composite: "auto",
+ marginRight: "0px", opacity: "1" }
+ ];
+ assert_frame_lists_equal(frames, expected);
+ }, 'Computed offsets can be outside [0,1] for keyframes with timeline ' +
+ 'offsets');
+
+ promise_test(async t => {
+ let anim = createAnimation(t, [
+ { offset: "contain 75%", marginLeft: "0px", opacity: "0" },
+ { offset: "contain 25%", marginRight: "0px", opacity: "1" }
+ ]);
+ let frames = anim.effect.getKeyframes();
+ let expected = [
+ { offset: { rangeName: 'contain', offset: CSS.percent(75) },
+ computedOffset: 0.75, easing: "linear", composite: "auto",
+ marginLeft: "0px", opacity: "0" },
+ { offset: { rangeName: 'contain', offset: CSS.percent(25) },
+ computedOffset: 0.25, easing: "linear", composite: "auto",
+ marginRight: "0px", opacity: "1" }
+ ];
+ assert_frame_lists_equal(frames, expected);
+ }, 'Retain specified ordering of keyframes with timeline offsets');
+
+ promise_test(async t => {
+ let anim = createAnimation(t, [
+ { offset: "cover 0%", marginLeft: "0px", opacity: "0" },
+ { offset: "cover 100%", marginRight: "0px", opacity: "1" }
+ ], /* use_view_timeline */ false);
+ let frames = anim.effect.getKeyframes();
+ let expected = [
+ { offset: { rangeName: 'cover', offset: CSS.percent(0) },
+ computedOffset: null, easing: "linear", composite: "auto",
+ marginLeft: "0px", opacity: "0" },
+ { offset: { rangeName: 'cover', offset: CSS.percent(100) },
+ computedOffset: null, easing: "linear", composite: "auto",
+ marginRight: "0px", opacity: "1" }
+ ];
+ assert_frame_lists_equal(frames, expected);
+ }, 'Include unreachable keyframes');
+
+
+ promise_test(async t => {
+ let anim = createAnimation(t, [
+ { offset: "cover 0%", marginLeft: "0px", opacity: 0 },
+ { offset: "cover 100%", marginRight: "0px", opacity: 1 },
+ { opacity: 0 },
+ { opacity: 0.5 },
+ { opacity: 1.0 }
+ ]);
+ let frames = anim.effect.getKeyframes();
+ let expected = [
+ { offset: { rangeName: 'cover', offset: CSS.percent(0) },
+ computedOffset: -1, easing: "linear", composite: "auto",
+ marginLeft: "0px", opacity: "0" },
+ { offset: { rangeName: 'cover', offset: CSS.percent(100) },
+ computedOffset: 2, easing: "linear", composite: "auto",
+ marginRight: "0px", opacity: "1" },
+ { offset: null, computedOffset: 0, easing: "linear", composite: "auto",
+ opacity: "0" },
+ { offset: null, computedOffset: 0.5, easing: "linear",
+ composite: "auto", opacity: "0.5" },
+ { offset: null, computedOffset: 1.0, easing: "linear",
+ composite: "auto", opacity: "1" }
+ ];
+ assert_frame_lists_equal(frames, expected);
+
+ anim = createAnimation(t, [
+ { opacity: 0 },
+ { offset: "cover 0%", marginLeft: "0px", opacity: 0 },
+ { opacity: 0.5 },
+ { offset: "cover 100%", marginRight: "0px", opacity: 1 },
+ { opacity: 1.0 }
+ ]);
+ frames = anim.effect.getKeyframes();
+ expected = [
+ { offset: null, computedOffset: 0, easing: "linear", composite: "auto",
+ opacity: "0" },
+ { offset: { rangeName: 'cover', offset: CSS.percent(0) },
+ computedOffset: -1, easing: "linear", composite: "auto",
+ marginLeft: "0px", opacity: "0" },
+ { offset: null, computedOffset: 0.5, easing: "linear",
+ composite: "auto", opacity: "0.5" },
+ { offset: { rangeName: 'cover', offset: CSS.percent(100) },
+ computedOffset: 2, easing: "linear", composite: "auto",
+ marginRight: "0px", opacity: "1" },
+ { offset: null, computedOffset: 1.0, easing: "linear",
+ composite: "auto", opacity: "1" }
+ ];
+ assert_frame_lists_equal(frames, expected);
+
+ anim = createAnimation(t, [
+ { opacity: 0.2, offset: 0.2 },
+ { offset: "cover 0%", marginLeft: "0px", opacity: 0 },
+ { opacity: 0.4 },
+ { opacity: 0.6 },
+ { offset: "cover 100%", marginRight: "0px", opacity: 1 },
+ { opacity: 0.8, offset: 0.8 }
+ ]);
+ frames = anim.effect.getKeyframes();
+ expected = [
+ { offset: 0.2, computedOffset: 0.2, easing: "linear", composite: "auto",
+ opacity: "0.2" },
+ { offset: { rangeName: 'cover', offset: CSS.percent(0) },
+ computedOffset: -1, easing: "linear", composite: "auto",
+ marginLeft: "0px", opacity: "0" },
+ { offset: null, computedOffset: 0.4, easing: "linear",
+ composite: "auto", opacity: "0.4" },
+ { offset: null, computedOffset: 0.6, easing: "linear",
+ composite: "auto", opacity: "0.6" },
+ { offset: { rangeName: 'cover', offset: CSS.percent(100) },
+ computedOffset: 2, easing: "linear", composite: "auto",
+ marginRight: "0px", opacity: "1" },
+ { offset: 0.8, computedOffset: 0.8, easing: "linear", composite: "auto",
+ opacity: "0.8" }
+ ];
+ assert_frame_lists_equal(frames, expected);
+ }, 'Mix of computed and timeline offsets.');
+ }
+
+ window.onload = runTest;
+</script>
+</html>
diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/inline-view-timeline-current-time.tentative.html b/testing/web-platform/tests/scroll-animations/view-timelines/inline-view-timeline-current-time.tentative.html
new file mode 100644
index 0000000000..5b37798fe8
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/view-timelines/inline-view-timeline-current-time.tentative.html
@@ -0,0 +1,301 @@
+<!DOCTYPE html>
+<html id="top">
+<meta charset="utf-8">
+<title>View timeline current-time</title>
+<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#viewtimeline-interface">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<script src="/scroll-animations/scroll-timelines/testcommon.js"></script>
+<script src="/scroll-animations/view-timelines/testcommon.js"></script>
+<style>
+ #container {
+ border: 10px solid lightgray;
+ overflow-x: scroll;
+ height: 200px;
+ width: 200px;
+ }
+ #content {
+ display: flex;
+ flex-flow: row nowrap;
+ justify-content: flex-start;
+ width: 1800px;
+ margin: 0;
+ }
+ .spacer {
+ width: 800px;
+ display: inline-block;
+ }
+ #target {
+ background-color: green;
+ height: 100px;
+ width: 200px;
+ display: inline-block;
+ }
+</style>
+<body>
+ <div id="container">
+ <div id="content">
+ <div id="leading-space" class="spacer"></div>
+ <div id="target"></div>
+ <div id="trailing-space" class="spacer"></div>
+ </div>
+ </div>
+</body>
+<script type="text/javascript">
+ promise_test(async t => {
+ container.scrollLeft = 0;
+ await waitForNextFrame();
+
+ const anim = CreateViewTimelineOpacityAnimation(t, target,
+ {
+ timeline:
+ {axis: 'inline'}
+ });
+ const timeline = anim.timeline;
+ await anim.ready;
+
+ // Initially before start-offset and animation effect is in the before
+ // phase.
+ assert_percents_equal(timeline.currentTime, -150,
+ "Timeline's currentTime at container start boundary");
+ assert_percents_equal(anim.currentTime, -150,
+ "Animation's currentTime at container start boundary");
+ assert_equals(getComputedStyle(target).opacity, "1",
+ 'Effect is inactive in the before phase');
+
+ // Advance to the start offset, which triggers entry to the active phase.
+ container.scrollLeft = 600;
+ await waitForNextFrame();
+ assert_percents_equal(timeline.currentTime, 0,
+ "Timeline's current time at start offset");
+ assert_percents_equal(anim.currentTime, 0,
+ "Animation's current time at start offset");
+ assert_equals(getComputedStyle(target).opacity, '0.3',
+ 'Effect at the start of the active phase');
+
+ // Advance to the midpoint of the animation.
+ container.scrollLeft = 800;
+ await waitForNextFrame();
+ assert_percents_equal(timeline.currentTime, 50,
+ "Timeline's currentTime at midpoint");
+ assert_percents_equal(anim.currentTime, 50,
+ "Animation's currentTime at midpoint");
+ assert_equals(getComputedStyle(target).opacity,'0.5',
+ 'Effect at the midpoint of the active range');
+
+ // Advance to the end of the animation.
+ container.scrollLeft = 1000;
+ await waitForNextFrame();
+ assert_percents_equal(timeline.currentTime, 100,
+ "Timeline's currentTime at end offset");
+ assert_percents_equal(anim.currentTime, 100,
+ "Animation's currentTime at end offset");
+ assert_equals(getComputedStyle(target).opacity, '0.7',
+ 'Effect is in the active phase at effect end time');
+
+ // Advance to the scroll limit.
+ container.scrollLeft = 1600;
+ await waitForNextFrame();
+ assert_percents_equal(timeline.currentTime, 250,
+ "Timeline's currentTime at scroll limit");
+ // Hold time set when the animation finishes, which clamps the value of
+ // the animation's currentTime.
+ assert_percents_equal(anim.currentTime, 100,
+ "Animation's currentTime at scroll limit");
+ // In the after phase, so the effect should not be applied.
+ assert_equals(getComputedStyle(target).opacity, '1',
+ 'After phase at scroll limit');
+ }, 'View timeline with start and end scroll offsets that do not align with ' +
+ 'the scroll boundaries' );
+
+ promise_test(async t => {
+ const leading = document.getElementById('leading-space');
+ leading.style = 'display: none';
+ content.style = 'width: 1000px';
+ t.add_cleanup(() => {
+ leading.style = null;
+ content.style = null;
+ });
+
+ container.scrollLeft = 0;
+ await waitForNextFrame();
+
+ const anim = CreateViewTimelineOpacityAnimation(t, target,
+ {
+ timeline:
+ {axis: 'inline'}
+ });
+ const timeline = anim.timeline;
+ await anim.ready;
+
+ assert_percents_equal(timeline.currentTime, 50,
+ "Timeline's currentTime at container start boundary");
+ assert_percents_equal(anim.currentTime, 50,
+ "Animation's currentTime at container start boundary");
+ assert_equals(getComputedStyle(target).opacity, "0.5",
+ 'Effect enters active phase at container start boundary');
+
+
+ // Advance to midpoint
+ container.scrollLeft = 100;
+ await waitForNextFrame();
+ assert_percents_equal(timeline.currentTime, 75,
+ "Timeline's current time at start offset");
+ assert_percents_equal(anim.currentTime, 75,
+ "Animation's current time at start offset");
+ assert_equals(getComputedStyle(target).opacity, '0.6',
+ 'Effect at the start of the active phase');
+
+ // Advance to end-offset
+ container.scrollLeft = 200;
+ await waitForNextFrame();
+ assert_percents_equal(timeline.currentTime, 100,
+ "Timeline's current time at start offset");
+ assert_percents_equal(anim.currentTime, 100,
+ "Animation's current time at start offset");
+ assert_equals(getComputedStyle(target).opacity, '0.7',
+ 'Effect at the start of the active phase');
+
+ // Advance to scroll limit.
+ container.scrollLeft = 800;
+ await waitForNextFrame();
+ assert_percents_equal(timeline.currentTime, 250,
+ "Timeline's current time at start offset");
+ assert_percents_equal(anim.currentTime, 100,
+ "Animation's current time at start offset");
+ assert_equals(getComputedStyle(target).opacity, '1',
+ 'Effect at the start of the active phase');
+
+ }, 'View timeline does not clamp starting scroll offset at 0');
+
+ promise_test(async t => {
+ const trailing = document.getElementById('trailing-space');
+ trailing.style = 'display: none';
+ content.style = 'width: 1000px';
+ t.add_cleanup(() => {
+ trailing.style = null;
+ content.style = null;
+ });
+
+ container.scrollLeft = 0;
+ await waitForNextFrame();
+
+ const anim = CreateViewTimelineOpacityAnimation(t, target,
+ {
+ timeline:
+ {axis: 'inline'}
+ });
+ const timeline = anim.timeline;
+ await anim.ready;
+
+ // Initially in before phase.
+ assert_percents_equal(timeline.currentTime, -150,
+ "Timeline's currentTime at container start boundary");
+ assert_percents_equal(anim.currentTime, -150,
+ "Animation's currentTime at container start boundary");
+ assert_equals(getComputedStyle(target).opacity, "1",
+ 'Effect enters active phase at container start boundary');
+
+ // Advance to start offset.
+ container.scrollLeft = 600;
+ await waitForNextFrame();
+ assert_percents_equal(timeline.currentTime, 0,
+ "Timeline's current time at start offset");
+ assert_percents_equal(anim.currentTime, 0,
+ "Animation's current time at start offset");
+ assert_equals(getComputedStyle(target).opacity, '0.3',
+ 'Effect at the start of the active phase');
+
+ // Advance to midpoint
+ container.scrollLeft = 700;
+ await waitForNextFrame();
+ assert_percents_equal(timeline.currentTime, 25,
+ "Timeline's current time at start offset");
+ assert_percents_equal(anim.currentTime, 25,
+ "Animation's current time at start offset");
+ assert_equals(getComputedStyle(target).opacity, '0.4',
+ 'Effect at the start of the active phase');
+
+ // Advance to end offset.
+ container.scrollLeft = 800;
+ await waitForNextFrame();
+ assert_percents_equal(timeline.currentTime, 50,
+ "Timeline's currentTime at max scroll offset");
+ assert_percents_equal(anim.currentTime, 50,
+ "Animation's currentTime at max scroll offset");
+ assert_equals(getComputedStyle(target).opacity, "0.5",
+ 'Effect at end of active phase');
+ }, 'View timeline does not clamp end scroll offset at max scroll');
+
+
+ promise_test(async t => {
+ container.style = "direction: rtl";
+ container.scrollLeft = 0;
+ t.add_cleanup(() => {
+ content.style = null;
+ });
+ await waitForNextFrame();
+
+ const anim = CreateViewTimelineOpacityAnimation(t, target,
+ {
+ timeline:
+ {axis: 'inline'}
+ });
+ const timeline = anim.timeline;
+ await anim.ready;
+
+ // Initially before start-offset and animation effect is in the before
+ // phase.
+ assert_percents_equal(timeline.currentTime, -150,
+ "Timeline's currentTime at container start boundary");
+ assert_percents_equal(anim.currentTime, -150,
+ "Animation's currentTime at container start boundary");
+ assert_equals(getComputedStyle(target).opacity, "1",
+ 'Effect is inactive in the before phase');
+
+ // Advance to the start offset, which triggers entry to the active phase.
+ container.scrollLeft = -600;
+ await waitForNextFrame();
+ assert_percents_equal(timeline.currentTime, 0,
+ "Timeline's current time at start offset");
+ assert_percents_equal(anim.currentTime, 0,
+ "Animation's current time at start offset");
+ assert_equals(getComputedStyle(target).opacity, '0.3',
+ 'Effect at the start of the active phase');
+
+ // Advance to the midpoint of the animation.
+ container.scrollLeft = -800;
+ await waitForNextFrame();
+ assert_percents_equal(timeline.currentTime, 50,
+ "Timeline's currentTime at midpoint");
+ assert_percents_equal(anim.currentTime, 50,
+ "Animation's currentTime at midpoint");
+ assert_equals(getComputedStyle(target).opacity,'0.5',
+ 'Effect at the midpoint of the active range');
+
+ // Advance to the end of the animation.
+ container.scrollLeft = -1000;
+ await waitForNextFrame();
+ assert_percents_equal(timeline.currentTime, 100,
+ "Timeline's currentTime at end offset");
+ assert_percents_equal(anim.currentTime, 100,
+ "Animation's currentTime at end offset");
+ assert_equals(getComputedStyle(target).opacity, '0.7',
+ 'Effect is in the active phase at effect end time');
+
+ // Advance to the scroll limit.
+ container.scrollLeft = -1600;
+ await waitForNextFrame();
+ assert_percents_equal(timeline.currentTime, 250,
+ "Timeline's currentTime at scroll limit");
+ // Hold time set when the animation finishes, which clamps the value of
+ // the animation's currentTime.
+ assert_percents_equal(anim.currentTime, 100,
+ "Animation's currentTime at scroll limit");
+ // In the after phase, so the effect should not be applied.
+ assert_equals(getComputedStyle(target).opacity, '1',
+ 'After phase at scroll limit');
+ }, 'View timeline with container having RTL layout' );
+</script>
+</html>
diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/testcommon.js b/testing/web-platform/tests/scroll-animations/view-timelines/testcommon.js
new file mode 100644
index 0000000000..65301215c4
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/view-timelines/testcommon.js
@@ -0,0 +1,145 @@
+'use strict';
+
+function assert_px_equals(observed, expected, description) {
+ assert_equals(observed.unit, 'px',
+ `Unexpected unit type for '${description}'`);
+ assert_approx_equals(observed.value, expected, 0.0001,
+ `Unexpected value for ${description}`);
+}
+
+function CreateViewTimelineOpacityAnimation(test, target, options) {
+ const timeline_options = {
+ subject: target,
+ axis: 'block'
+ };
+ if (options && 'timeline' in options) {
+ for (let key in options.timeline) {
+ timeline_options[key] = options.timeline[key];
+ }
+ }
+ const animation_options = {
+ timeline: new ViewTimeline(timeline_options)
+ };
+ if (options && 'animation' in options) {
+ for (let key in options.animation) {
+ animation_options[key] = options.animation[key];
+ }
+ }
+
+ const anim =
+ target.animate({ opacity: [0.3, 0.7] }, animation_options);
+ test.add_cleanup(() => {
+ anim.cancel();
+ });
+ return anim;
+}
+
+// Verify that range specified in the options aligns with the active range of
+// the animation.
+//
+// Sample call:
+// await runTimelineBoundsTest(t, {
+// timeline: { inset: [ CSS.percent(0), CSS.percent(20)] },
+// timing: { fill: 'both' }
+// startOffset: 600,
+// endOffset: 900
+// });
+async function runTimelineBoundsTest(t, options, message) {
+ container.scrollLeft = 0;
+ await waitForNextFrame();
+
+ const anim =
+ options.anim ||
+ CreateViewTimelineOpacityAnimation(t, target, options);
+ if (options.timing)
+ anim.effect.updateTiming(options.timing);
+
+ const timeline = anim.timeline;
+ await anim.ready;
+
+ // Advance to the start offset, which triggers entry to the active phase.
+ container.scrollLeft = options.startOffset;
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(target).opacity, '0.3',
+ `Effect at the start of the active phase: ${message}`);
+
+ // Advance to the midpoint of the animation.
+ container.scrollLeft = (options.startOffset + options.endOffset) / 2;
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(target).opacity,'0.5',
+ `Effect at the midpoint of the active range: ${message}`);
+
+ // Advance to the end of the animation.
+ container.scrollLeft = options.endOffset;
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(target).opacity, '0.7',
+ `Effect is in the active phase at effect end time: ${message}`);
+
+ // Return the animation so that we can continue testing with the same object.
+ return anim;
+}
+
+// Sets the start and end range for a view timeline and ensures that the
+// range aligns with expected values.
+//
+// Sample call:
+// await runTimelineRangeTest(t, {
+// rangeStart: { rangeName: 'cover', offset: CSS.percent(0) } ,
+// rangeEnd: { rangeName: 'cover', offset: CSS.percent(100) },
+// startOffset: 600,
+// endOffset: 900
+// });
+async function runTimelineRangeTest(t, options) {
+ const rangeToString = range => {
+ const parts = [];
+ if (range.rangeName)
+ parts.push(range.rangeName);
+ if (range.offset)
+ parts.push(`${range.offset.value}%`);
+ return parts.join(' ');
+ };
+ const range =
+ `${rangeToString(options.rangeStart)} to ` +
+ `${rangeToString(options.rangeEnd)}`;
+
+ options.timeline = {
+ axis: 'inline'
+ };
+ options.animation = {
+ rangeStart: options.rangeStart,
+ rangeEnd: options.rangeEnd,
+ };
+ options.timing = {
+ // Set fill to accommodate floating point precision errors at the
+ // endpoints.
+ fill: 'both'
+ };
+
+ return runTimelineBoundsTest(t, options, range);
+}
+
+// Sets the Inset for a view timeline and ensures that the range aligns with
+// expected values.
+//
+// Sample call:
+// await runTimelineInsetTest(t, {
+// inset: [ CSS.px(20), CSS.px(40) ]
+// startOffset: 600,
+// endOffset: 900
+// });
+async function runTimelineInsetTest(t, options) {
+ options.timeline = {
+ axis: 'inline',
+ inset: options.inset
+ };
+ options.timing = {
+ // Set fill to accommodate floating point precision errors at the
+ // endpoints.
+ fill: 'both'
+ }
+ const length = options.inset.length;
+ const range =
+ (options.inset instanceof Array) ? options.inset.join(' ')
+ : options.inset;
+ return runTimelineBoundsTest(t, options, range);
+}
diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/timeline-offset-in-keyframe.html b/testing/web-platform/tests/scroll-animations/view-timelines/timeline-offset-in-keyframe.html
new file mode 100644
index 0000000000..62a8d1387d
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/view-timelines/timeline-offset-in-keyframe.html
@@ -0,0 +1,263 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<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>
+<title>Animation range and delay</title>
+</head>
+<style type="text/css">
+ #scroller {
+ border: 10px solid lightgray;
+ overflow-y: scroll;
+ overflow-x: hidden;
+ width: 300px;
+ height: 200px;
+ }
+ #target {
+ margin: 800px 10px;
+ width: 100px;
+ height: 100px;
+ z-index: -1;
+ background-color: green;
+ }
+</style>
+<body>
+ <div id=scroller>
+ <div id=target></div>
+ </div>
+</body>
+<script type="text/javascript">
+ async function runTest() {
+ function assert_progress_equals(anim, expected, errorMessage) {
+ assert_approx_equals(
+ anim.effect.getComputedTiming().progress,
+ expected, 1e-6, errorMessage);
+ }
+
+ function assert_opacity_equals(expected, errorMessage) {
+ assert_approx_equals(
+ parseFloat(getComputedStyle(target).opacity), expected, 1e-6,
+ errorMessage);
+ }
+
+ async function runTimelineOffsetsInKeyframesTest(keyframes) {
+ const testcase = JSON.stringify(keyframes);
+ const anim = target.animate(keyframes, {
+ timeline: new ViewTimeline( { subject: target }),
+ rangeStart: { rangeName: 'contain', offset: CSS.percent(0) },
+ rangeEnd: { rangeName: 'contain', offset: CSS.percent(100) },
+ duration: 'auto', fill: 'both'
+ });
+ await anim.ready;
+ await waitForNextFrame();
+
+ // @ contain 0%
+ scroller.scrollTop = 700;
+ await waitForNextFrame();
+
+ assert_progress_equals(
+ anim, 0, `Testcase '${testcase}': progress at contain 0%`);
+ assert_opacity_equals(
+ 1/3, `Testcase '${testcase}': opacity at contain 0%`);
+
+ // @ contain 50%
+ scroller.scrollTop = 750;
+ await waitForNextFrame();
+ assert_progress_equals(
+ anim, 0.5, `Testcase '${testcase}': progress at contain 50%`);
+ assert_opacity_equals(
+ 0.5, `Testcase '${testcase}': opacity at contain 50%`);
+
+ // @ contain 100%
+ scroller.scrollTop = 800;
+ await waitForNextFrame();
+ assert_progress_equals(
+ anim, 1, `Testcase '${testcase}': progress at contain 100%`);
+ assert_opacity_equals(
+ 2/3, `Testcase '${testcase}': opacity at contain 100%`);
+ anim.cancel();
+ }
+
+ async function runParseNumberOrPercentInKeyframesTest(keyframes) {
+ const anim = target.animate(keyframes, {
+ timeline: new ViewTimeline( { subject: target }),
+ rangeStart: { rangeName: 'contain', offset: CSS.percent(0) },
+ rangeEnd: { rangeName: 'contain', offset: CSS.percent(100) },
+ duration: 'auto', fill: 'both'
+ });
+ await anim.ready;
+ await waitForNextFrame();
+
+ const maxScroll = scroller.scrollHeight - scroller.clientHeight;
+ scroller.scrollTop = maxScroll / 2;
+ await waitForNextFrame();
+
+ const testcase = JSON.stringify(keyframes);
+ assert_progress_equals(anim, 0.5, testcase);
+ assert_opacity_equals(0.5, testcase);
+ anim.cancel();
+ }
+
+ async function runInvalidKeyframesTest(keyframes) {
+ assert_throws_js(TypeError, () => {
+ target.animate(keyframes, {
+ timeline: new ViewTimeline( { subject: target }),
+ });
+ }, `Invalid keyframes test case "${JSON.stringify(keyframes)}"`);
+ }
+
+ promise_test(async t => {
+ // Test equivalent typed-OM and CSS representations of timeline offsets.
+ // Test array and object form for keyframes.
+ const keyframeTests = [
+ // BaseKeyframe form with offsets expressed as typed-OM.
+ [
+ {
+ offset: { rangeName: 'cover', offset: CSS.percent(0) },
+ opacity: 0
+ },
+ {
+ offset: { rangeName: 'cover', offset: CSS.percent(100) },
+ opacity: 1
+ }
+ ],
+ // BaseKeyframe form with offsets expressed as CSS text.
+ [
+ { offset: "cover 0%", opacity: 0 },
+ { offset: "cover 100%", opacity: 1 }
+ ],
+ // BasePropertyIndexedKeyframe form with offsets expressed as typed-OM.
+ {
+ opacity: [0, 1],
+ offset: [
+ { rangeName: 'cover', offset: CSS.percent(0) },
+ { rangeName: 'cover', offset: CSS.percent(100) }
+ ]
+ },
+ // BasePropertyIndexedKeyframe form with offsets expressed as CSS text.
+ { opacity: [0, 1], offset: [ "cover 0%", "cover 100%" ]}
+ ];
+
+ for (let i = 0; i < keyframeTests.length; i++) {
+ await runTimelineOffsetsInKeyframesTest(keyframeTests[i]);
+ }
+
+ }, 'Timeline offsets in programmatic keyframes');
+
+ promise_test(async t => {
+ const keyframeTests = [
+ [{offset: "0.5", opacity: 0.5 }],
+ [{offset: "50%", opacity: 0.5 }],
+ [{offset: "calc(20% + 30%)", opacity: 0.5 }]
+ ];
+
+ for (let i = 0; i < keyframeTests.length; i++) {
+ await runParseNumberOrPercentInKeyframesTest(keyframeTests[i]);
+ }
+
+ }, 'String offsets in programmatic keyframes');
+
+ promise_test(async t => {
+ const invalidKeyframeTests = [
+ // BasePropertyKefyrame:
+ [{ offset: { rangeName: 'somewhere', offset: CSS.percent(0) }}],
+ [{ offset: { rangeName: 'entry', offset: CSS.px(0) }}],
+ [{ offset: "here 0%" }],
+ [{ offset: "entry 3px" }],
+ // BasePropertyIndexedKeyframe with sequence:
+ { offset: [{ rangeName: 'somewhere', offset: CSS.percent(0) }]},
+ { offset: [{ rangeName: 'entry', offset: CSS.px(0) }]},
+ { offset: ["here 0%"] },
+ { offset: ["entry 3px" ]},
+ // BasePropertyIndexedKeyframe without sequence:
+ { offset: { rangeName: 'somewhere', offset: CSS.percent(0) }},
+ { offset: { rangeName: 'entry', offset: CSS.px(0) }},
+ { offset: "here 0%" },
+ { offset: "entry 3px" },
+ // <number> or <percent> as string:
+ [{ offset: "-1" }],
+ [{ offset: "2" }],
+ [{ offset: "-10%" }],
+ [{ offset: "110%" }],
+ { offset: ["-1"], opacity: [0.5] },
+ { offset: ["2"], opacity: [0.5] },
+ { offset: "-1", opacity: 0.5 },
+ { offset: "2", opacity: 0.5 },
+ // Extra stuff at the end.
+ [{ offset: "0.5 trailing nonsense" }],
+ [{ offset: "cover 50% eureka" }]
+ ];
+ for( let i = 0; i < invalidKeyframeTests.length; i++) {
+ await runInvalidKeyframesTest(invalidKeyframeTests[i]);
+ }
+ }, 'Invalid timeline offset in programmatic keyframe throws');
+
+
+ promise_test(async t => {
+ const anim = target.animate([
+ { offset: "cover 0%", opacity: 0 },
+ { offset: "cover 100%", opacity: 1 }
+ ], {
+ rangeStart: { rangeName: 'contain', offset: CSS.percent(0) },
+ rangeEnd: { rangeName: 'contain', offset: CSS.percent(100) },
+ duration: 10000, fill: 'both'
+ });
+
+ scroller.scrollTop = 750;
+
+ await anim.ready;
+ assert_opacity_equals(1, `Opacity with document timeline`);
+
+ anim.timeline = new ViewTimeline( { subject: target });
+ await waitForNextFrame();
+
+ assert_progress_equals(anim, 0.5, `Progress at contain 50%`);
+ assert_opacity_equals(0.5, `Opacity at contain 50%`);
+
+ anim.timeline = document.timeline;
+ await waitForNextFrame();
+ assert_opacity_equals(1, `Opacity after resetting timeline`);
+
+ anim.cancel();
+ }, 'Timeline offsets in programmatic keyframes adjust for change in ' +
+ 'timeline');
+
+ promise_test(async t => {
+ const anim = target.animate([], {
+ timeline: new ViewTimeline( { subject: target }),
+ rangeStart: { rangeName: 'contain', offset: CSS.percent(0) },
+ rangeEnd: { rangeName: 'contain', offset: CSS.percent(100) },
+ duration: 'auto', fill: 'both'
+ });
+
+ await anim.ready;
+ await waitForNextFrame();
+
+ scroller.scrollTop = 750;
+ await waitForNextFrame();
+ assert_progress_equals(
+ anim, 0.5, `Progress at contain 50% before effect change`);
+ assert_opacity_equals(1, `Opacity at contain 50% before effect change`);
+
+ anim.effect = new KeyframeEffect(target, [
+ { offset: "cover 0%", opacity: 0 },
+ { offset: "cover 100%", opacity: 1 }
+ ], { duration: 'auto', fill: 'both' });
+ await waitForNextFrame();
+ assert_progress_equals(
+ anim, 0.5, `Progress at contain 50% after effect change`);
+ assert_opacity_equals(0.5, `Opacity at contain 50% after effect change`);
+ }, 'Timeline offsets in programmetic keyframes resolved when updating ' +
+ 'the animation effect');
+ }
+
+ // TODO(kevers): Add tests for getKeyframes once
+ // https://github.com/w3c/csswg-drafts/issues/8507 is resolved.
+
+ window.onload = runTest;
+</script>
+</html>
diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-get-current-time-range-name.html b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-get-current-time-range-name.html
new file mode 100644
index 0000000000..25e477e1a9
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-get-current-time-range-name.html
@@ -0,0 +1,148 @@
+<!DOCTYPE html>
+<html id="top">
+<meta charset="utf-8">
+<title>View timeline delay</title>
+<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#viewtimeline-interface">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<script src="/scroll-animations/scroll-timelines/testcommon.js"></script>
+<script src="/scroll-animations/view-timelines/testcommon.js"></script>
+<style>
+ #container {
+ border: 10px solid lightgray;
+ overflow-x: scroll;
+ height: 200px;
+ width: 200px;
+ }
+ #content {
+ display: flex;
+ flex-flow: row nowrap;
+ justify-content: flex-start;
+ width: 1800px;
+ margin: 0;
+ }
+ .spacer {
+ width: 800px;
+ display: inline-block;
+ }
+ #target {
+ background-color: green;
+ height: 100px;
+ width: 100px;
+ display: inline-block;
+ }
+</style>
+<body>
+ <div id="container">
+ <div id="content">
+ <div class="spacer"></div>
+ <div id="target"></div>
+ <div class="spacer"></div>
+ </div>
+ </div>
+</body>
+<script type="text/javascript">
+ const MAX_SCROLL = 1600;
+
+ promise_test(async t => {
+ // Points of interest along view timeline:
+ // 600 px cover start, entry start
+ // 700 px contain start, entry end
+ // 800 px contain end, exit start
+ // 900 px cover end, exit end
+ const anim =
+ CreateViewTimelineOpacityAnimation(t, target,
+ {
+ timeline: { axis: 'inline' },
+ animation: { fill: 'both' }
+ });
+ let timeline = anim.timeline;
+
+ container.scrollLeft = 600;
+ await waitForNextFrame();
+
+ assert_percents_approx_equal(timeline.getCurrentTime('cover'), 0,
+ MAX_SCROLL, 'Scroll aligned with cover start');
+ assert_percents_approx_equal(timeline.getCurrentTime('entry'), 0,
+ MAX_SCROLL, 'Scroll aligned with entry start');
+ assert_percents_approx_equal(timeline.getCurrentTime(), 0,
+ MAX_SCROLL,
+ 'Scroll aligned with timeline start offset');
+
+ container.scrollLeft = 650;
+ await waitForNextFrame();
+
+ assert_percents_approx_equal(timeline.getCurrentTime('entry'), 50,
+ MAX_SCROLL, 'Scroll at entry midpoint');
+
+ container.scrollLeft = 700;
+ await waitForNextFrame();
+
+ assert_percents_approx_equal(timeline.getCurrentTime('entry'), 100,
+ MAX_SCROLL, 'Scroll at entry end');
+ assert_percents_approx_equal(timeline.getCurrentTime('contain'), 0,
+ MAX_SCROLL, 'Scroll at contain start');
+
+ container.scrollLeft = 750;
+ await waitForNextFrame();
+
+ assert_percents_approx_equal(timeline.getCurrentTime('contain'), 50,
+ MAX_SCROLL, 'Scroll at contain midpoint');
+ assert_percents_approx_equal(timeline.getCurrentTime(), 50,
+ MAX_SCROLL, 'Scroll at timeline midpoint');
+
+ container.scrollLeft = 800;
+ await waitForNextFrame();
+
+ assert_percents_approx_equal(timeline.getCurrentTime('exit'), 0,
+ MAX_SCROLL, 'Scroll at exit start');
+ assert_percents_approx_equal(timeline.getCurrentTime('contain'), 100,
+ MAX_SCROLL, 'Scroll at contain end');
+
+ container.scrollLeft = 850;
+ await waitForNextFrame();
+
+ assert_percents_approx_equal(timeline.getCurrentTime('exit'), 50,
+ MAX_SCROLL, 'Scroll at exit midpoint');
+
+ container.scrollLeft = 900;
+ await waitForNextFrame();
+
+ assert_percents_approx_equal(timeline.getCurrentTime('exit'), 100,
+ MAX_SCROLL, 'Scroll at exit end');
+ assert_percents_approx_equal(timeline.getCurrentTime('cover'), 100,
+ MAX_SCROLL, 'Scroll at cover end');
+ assert_percents_approx_equal(timeline.getCurrentTime(), 100,
+ MAX_SCROLL, 'Scroll at end of timeline');
+
+ assert_equals(timeline.getCurrentTime('gibberish'), null,
+ 'No current time for unknown named range');
+
+ // Add insets to force the start and end offsets to align. This forces
+ // the timeline to become inactive.
+ // start_offset = target_offset - viewport_size + end_side_inset
+ // = 600 + end_side_inset
+ // end_offset = target_offset + target_size - start_side_inset
+ // = 900 - start_side_inset
+ // Equating start_offset and end_offset:
+ // end_side_inset = 300 - start_side_inset;
+ timeline =
+ new ViewTimeline ({
+ subject: target,
+ axis: 'inline',
+ inset: [ CSS.px(150), CSS.px(150) ]
+ });
+ anim.timeline = timeline;
+ await waitForNextFrame();
+
+ assert_equals(timeline.currentTime, null,
+ 'Current time is null when scroll-range is zero');
+ assert_equals(timeline.getCurrentTime(), null,
+ 'getCurrentTime with an inactive timeline.');
+ assert_equals(timeline.getCurrentTime('contain'), null,
+ 'getCurrentTime on a ranged name with an inactive timeline.');
+
+ }, 'View timeline current time for named range');
+
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-get-set-range.html b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-get-set-range.html
new file mode 100644
index 0000000000..6de2d84df7
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-get-set-range.html
@@ -0,0 +1,120 @@
+<!DOCTYPE html>
+<html id="top">
+<meta charset="utf-8">
+<title>View timeline delay</title>
+<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#viewtimeline-interface">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<script src="/scroll-animations/scroll-timelines/testcommon.js"></script>
+<script src="/scroll-animations/view-timelines/testcommon.js"></script>
+<script src="/css/css-typed-om/resources/testhelper.js"></script>
+<style>
+ #container {
+ border: 10px solid lightgray;
+ overflow-x: scroll;
+ height: 200px;
+ width: 200px;
+ }
+ #content {
+ display: flex;
+ flex-flow: row nowrap;
+ justify-content: flex-start;
+ width: 1800px;
+ margin: 0;
+ }
+ .spacer {
+ width: 800px;
+ display: inline-block;
+ }
+ #target {
+ background-color: green;
+ height: 100px;
+ width: 100px;
+ display: inline-block;
+ font-size: 10px;
+ }
+</style>
+<body>
+ <div id="container">
+ <div id="content">
+ <div class="spacer"></div>
+ <div id="target"></div>
+ <div class="spacer"></div>
+ </div>
+ </div>
+</body>
+<script type="text/javascript">
+ function assert_timeline_offset(actual, expected, errorMessage) {
+ assert_equals(actual.rangeName, expected.rangeName, errorMessage);
+ assert_style_value_equals(actual.offset, expected.offset);
+ }
+
+ promise_test(async t => {
+ const timeline = new ViewTimeline({ subject: target, axis: 'inline' });
+ const anim = target.animate({ opacity: [0, 1 ] }, { timeline: timeline });
+ t.add_cleanup(() => {
+ anim.cancel();
+ });
+
+ container.scrollLeft = 750;
+ await waitForNextFrame();
+
+ // normal ==> cover 0% to cover 100%
+ // cover 0% @ 600px
+ // cover 100% @ 900px
+ // expected opacity = (750 - 600) / (900 - 600) = 0.5
+ assert_equals(anim.rangeStart, 'normal', 'Initial value for rangeStart');
+ assert_equals(anim.rangeEnd, 'normal', 'Initial value for rangeEnd');
+ assert_equals(getComputedStyle(target).opacity, '0.5',
+ 'Opacity with range set to [normal, normal]');
+
+ // contain 0% @ 700px
+ // cover 100% @ 900px
+ // expected opacity = (750 - 700) / (900 - 700) = 0.25
+ anim.rangeStart = "contain 0%";
+ anim.rangeEnd = "cover 100%";
+
+ assert_timeline_offset(
+ anim.rangeStart,
+ { rangeName: 'contain', offset: CSS.percent(0) },
+ 'rangeStart set to contain 0%');
+ assert_timeline_offset(
+ anim.rangeEnd,
+ { rangeName: 'cover', offset: CSS.percent(100) },
+ 'rangeEnd set to cover 100%');
+ assert_equals(getComputedStyle(target).opacity, '0.25',
+ 'opacity with range set to [contain 0%, cover 100%]');
+
+ // entry -20px @ 580px
+ // exit-crossing 10% @ 810px
+ // expected opacity = (750 - 580) / (810 - 580) = 0.739130
+ anim.rangeStart = { rangeName: 'entry', offset: CSS.px(-20), };
+ anim.rangeEnd = { rangeName: 'exit-crossing', offset: CSS.percent(10) };
+ assert_timeline_offset(
+ anim.rangeStart,
+ { rangeName: 'entry', offset: CSS.px(-20) },
+ 'rangeStart set to entry -20px');
+ assert_timeline_offset(
+ anim.rangeEnd,
+ { rangeName: 'exit-crossing', offset: CSS.percent(10) },
+ 'rangeEnd set to exit-crossing 10%');
+ assert_approx_equals(
+ parseFloat(getComputedStyle(target).opacity), 0.739130, 1e-6,
+ 'opacity with range set to [entry -20px, exit-crossing 10%]');
+
+ // normal [start] @ 600px
+ // contain 100% @ 800px
+ // expected opacity = (750 - 600) / (800 - 600) = 0.75
+ anim.rangeStart = "normal";
+ anim.rangeEnd = "contain calc(60% + 40%)";
+ assert_equals(anim.rangeStart, 'normal','rangeStart set to normal');
+ assert_timeline_offset(
+ anim.rangeEnd,
+ { rangeName: 'contain', offset: CSS.percent(100) },
+ 'rangeEnd set to contain 100%');
+ assert_equals(getComputedStyle(target).opacity, '0.75',
+ 'opacity with range set to [normal, contain 100%]');
+ }, 'Getting and setting the animation range');
+</script>
+</html>
diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-inset.html b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-inset.html
new file mode 100644
index 0000000000..357d8558f9
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-inset.html
@@ -0,0 +1,226 @@
+<!DOCTYPE html>
+<html id="top">
+<meta charset="utf-8">
+<title>View timeline delay</title>
+<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#viewtimeline-interface">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<script src="/scroll-animations/scroll-timelines/testcommon.js"></script>
+<script src="/scroll-animations/view-timelines/testcommon.js"></script>
+<style>
+ #container {
+ border: 10px solid lightgray;
+ overflow-x: scroll;
+ height: 200px;
+ width: 200px;
+ }
+ #content {
+ display: flex;
+ flex-flow: row nowrap;
+ justify-content: flex-start;
+ width: 1800px;
+ margin: 0;
+ }
+ .spacer {
+ width: 800px;
+ display: inline-block;
+ }
+ #target {
+ background-color: green;
+ height: 100px;
+ width: 100px;
+ display: inline-block;
+ font-size: 16px;
+ }
+ #target.big-font {
+ font-size: 20px;
+ }
+ #container.scroll-padded {
+ scroll-padding-inline: 10px 20px;
+ }
+</style>
+</style>
+<body>
+ <div id="container">
+ <div id="content">
+ <div class="spacer"></div>
+ <div id="target"></div>
+ <div class="spacer"></div>
+ </div>
+ </div>
+</body>
+<script type="text/javascript">
+
+ function verifyTimelineOffsets(anim, start, end) {
+ const timeline = anim.timeline;
+ assert_px_equals(timeline.startOffset, start, 'startOffset');
+ assert_px_equals(timeline.endOffset, end, 'endOffset');
+ };
+
+ promise_test(async t => {
+ // These tests are all based on the cover range, which has bounds
+ // [600, 900] if there are no insets.
+ // startOffset = target_pos - viewport_size + end_side_inset
+ // = 600 + end_side_inset
+ // endOffset = target_pos + target_size - start_side_inset
+ // = 900 - start_side_inset
+ await runTimelineInsetTest(t, {
+ inset: [ CSS.px(0), CSS.px(0) ],
+ startOffset: 600,
+ endOffset: 900
+ }).then(anim => verifyTimelineOffsets(anim, 600, 900));
+ await runTimelineInsetTest(t, {
+ inset: [ CSS.px(10), CSS.px(20) ],
+ startOffset: 620,
+ endOffset: 890
+ }).then(anim => verifyTimelineOffsets(anim, 620, 890));
+ await runTimelineInsetTest(t, {
+ inset: [ CSS.px(10) ],
+ startOffset: 610,
+ endOffset: 890
+ }).then(anim => verifyTimelineOffsets(anim, 610, 890));
+ }, 'View timeline with px based inset.');
+
+ promise_test(async t => {
+ // These tests are all based on the cover range, which has bounds
+ // [600, 900].
+ // Percentages are relative to the viewport size, which is 200 for this
+ // test.
+ await runTimelineInsetTest(t, {
+ inset: [ CSS.percent(0), CSS.percent(0) ],
+ startOffset: 600,
+ endOffset: 900
+ }).then(anim => verifyTimelineOffsets(anim, 600, 900));
+ await runTimelineInsetTest(t, {
+ inset: [ CSS.percent(10), CSS.percent(20) ],
+ startOffset: 640,
+ endOffset: 880
+ }).then(anim => verifyTimelineOffsets(anim, 640, 880));
+ await runTimelineInsetTest(t, {
+ inset: [ CSS.percent(10) ],
+ startOffset: 620,
+ endOffset: 880
+ }).then(anim => verifyTimelineOffsets(anim, 620, 880));
+ }, 'View timeline with percent based inset.');
+
+ promise_test(async t => {
+ t.add_cleanup(() => {
+ container.classList.remove('scroll-padded');
+ });
+ const anim = await runTimelineInsetTest(t, {
+ inset: [ "auto", "auto" ],
+ startOffset: 600,
+ endOffset: 900
+ });
+ verifyTimelineOffsets(anim, 600, 900);
+ container.classList.add('scroll-padded');
+ await runTimelineBoundsTest(t, {
+ anim: anim,
+ startOffset: 620,
+ endOffset: 890,
+ }, 'Adjust for scroll-padding')
+ .then(anim => verifyTimelineOffsets(anim, 620, 890));
+ }, 'view timeline with inset auto.');
+
+promise_test(async t => {
+ t.add_cleanup(() => {
+ target.classList.remove('big-font');
+ });
+ const anim = await runTimelineInsetTest(t, {
+ inset: [ CSS.em(1), CSS.em(2) ],
+ startOffset: 632,
+ endOffset: 884
+ });
+ verifyTimelineOffsets(anim, 632, 884);
+ target.classList.add('big-font');
+ await runTimelineBoundsTest(t, {
+ anim: anim,
+ startOffset: 640,
+ endOffset: 880,
+ }, 'Adjust for font size increase')
+ .then(anim => verifyTimelineOffsets(anim, 640, 880));
+}, 'view timeline with font relative inset.');
+
+promise_test(async t => {
+ const vw = window.innerWidth;
+ const vh = window.innerHeight;
+ const vmin = Math.min(vw, vh);
+ await runTimelineInsetTest(t, {
+ inset: [ CSS.vw(10), CSS.vw(20) ],
+ startOffset: 600 + 0.2 * vw,
+ endOffset: 900 - 0.1 * vw
+ });
+ await runTimelineInsetTest(t, {
+ inset: [ CSS.vmin(10), CSS.vmin(20) ],
+ startOffset: 600 + 0.2 * vmin,
+ endOffset: 900 - 0.1 * vmin
+ });
+}, 'view timeline with viewport relative insets.');
+
+promise_test(async t => {
+ await runTimelineInsetTest(t, {
+ inset: "10px",
+ startOffset: 610,
+ endOffset: 890
+ });
+ await runTimelineInsetTest(t, {
+ inset: "10px 20px",
+ startOffset: 620,
+ endOffset: 890
+ });
+ await runTimelineInsetTest(t, {
+ inset: "10%",
+ startOffset: 620,
+ endOffset: 880
+ });
+ await runTimelineInsetTest(t, {
+ inset: "10% 20%",
+ startOffset: 640,
+ endOffset: 880
+ });
+ await runTimelineInsetTest(t, {
+ inset: "auto",
+ startOffset: 600,
+ endOffset: 900
+ });
+ await runTimelineInsetTest(t, {
+ inset: "1em 2em",
+ startOffset: 632,
+ endOffset: 884
+ });
+ assert_throws_js(TypeError, () => {
+ new ViewTimeline({
+ subject: target,
+ inset: "go fish"
+ });
+ });
+
+ assert_throws_js(TypeError, () => {
+ new ViewTimeline({
+ subject: target,
+ inset: "1 2"
+ });
+ });
+
+}, 'view timeline inset as string');
+
+promise_test(async t => {
+ assert_throws_js(TypeError, () => {
+ new ViewTimeline({
+ subject: target,
+ inset: [ CSS.rad(1) ]
+ });
+ });
+
+ assert_throws_js(TypeError, () => {
+ new ViewTimeline({
+ subject: target,
+ inset: [ CSS.px(10), CSS.px(10), CSS.px(10) ]
+ });
+ });
+
+
+}, 'view timeline with invalid inset');
+
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-missing-subject.html b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-missing-subject.html
new file mode 100644
index 0000000000..01ca021524
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-missing-subject.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+<title>ViewTimeline with missing subject</title>
+<link rel="help" href="https://www.w3.org/TR/scroll-animations-1/#viewtimeline-interface">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<style type="text/css">
+ #target {
+ background: blue;
+ height: 100px;
+ width: 100px;
+ }
+ #scroller {
+ overflow: scroll;
+ }
+ #filler {
+ height: 300vh;
+ }
+</style>
+<body onload="runTests()">
+ <div id="scroller">
+ <div id="target"></div>
+ <div id="filler"></div>
+ </div>
+</body>
+<script type="text/javascript">
+ function raf() {
+ return new Promise(resolve => {
+ requestAnimationFrame(() => {
+ requestAnimationFrame(resolve);
+ })
+ });
+ }
+ function runTests() {
+ promise_test(async t => {
+ const timeline = new ViewTimeline();
+ const anim =
+ target.animate(
+ { backgroundColor: ['green', 'red' ] },
+ { duration: 100,
+ timeline: timeline });
+ await raf();
+ scroller.scrollTop = 50;
+ await raf();
+ assert_equals(timeline.currentTime, null,
+ 'ViewTimeline with missing subject is inactive');
+ });
+ }
+
+</script>
+</html>
diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-on-display-none-element.html b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-on-display-none-element.html
new file mode 100644
index 0000000000..1cc23fe626
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-on-display-none-element.html
@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>View timeline on element with display:none</title>
+<link rel="help" href="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="/scroll-animations/scroll-timelines/testcommon.js"></script>
+<script src="/scroll-animations/view-timelines/testcommon.js"></script>
+<script src="/css/css-typed-om/resources/testhelper.js"></script>
+
+<style>
+ #container {
+ border: 10px solid lightgray;
+ overflow-x: scroll;
+ height: 200px;
+ width: 200px;
+ }
+ #content {
+ display: flex;
+ flex-flow: row nowrap;
+ justify-content: flex-start;
+ width: 1800px;
+ margin: 0;
+ }
+ .spacer {
+ width: 800px;
+ display: inline-block;
+ }
+ #target {
+ background-color: green;
+ height: 100px;
+ width: 100px;
+ display: none;
+ }
+</style>
+
+<div id="container">
+ <div id="content">
+ <div class="spacer"></div>
+ <div id="target"></div>
+ <div class="spacer"></div>
+ </div>
+</div>
+
+<script>
+promise_test(async t => {
+ const timeline = new ViewTimeline({ subject: target });
+ const anim = target.animate({ opacity: [0, 0.5] }, { timeline: timeline });
+ t.add_cleanup(() => {
+ anim.cancel();
+ });
+ anim.rangeStart = "1em";
+ container.scrollLeft = 750;
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(target).opacity, "1",
+ "Opacity with inactive timeline");
+}, "element with display: none should have inactive viewtimeline");
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-range-large-subject.html b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-range-large-subject.html
new file mode 100644
index 0000000000..f87a57584e
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-range-large-subject.html
@@ -0,0 +1,105 @@
+<!DOCTYPE html>
+<html id="top">
+<meta charset="utf-8">
+<title>View timeline delay</title>
+<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#viewtimeline-interface">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<script src="/scroll-animations/scroll-timelines/testcommon.js"></script>
+<script src="/scroll-animations/view-timelines/testcommon.js"></script>
+<style>
+ #container {
+ border: 10px solid lightgray;
+ overflow-x: scroll;
+ height: 200px;
+ width: 200px;
+ }
+ #content {
+ display: flex;
+ flex-flow: row nowrap;
+ justify-content: flex-start;
+ width: 2100px;
+ margin: 0;
+ }
+ .spacer {
+ width: 800px;
+ display: inline-block;
+ }
+ #target {
+ background-color: green;
+ height: 100px;
+ /* target size > viewport size, which changes interpretation of the
+ contain range */
+ width: 400px;
+ display: inline-block;
+ }
+</style>
+<body>
+ <div id="container">
+ <div id="content">
+ <div class="spacer"></div>
+ <div id="target"></div>
+ <div class="spacer"></div>
+ </div>
+ </div>
+</body>
+<script type="text/javascript">
+ promise_test(async t => {
+ await runTimelineRangeTest(t, {
+ rangeStart: { rangeName: 'cover', offset: CSS.percent(0) } ,
+ rangeEnd: { rangeName: 'cover', offset: CSS.percent(100) },
+ startOffset: 600,
+ endOffset: 1200
+ });
+ await runTimelineRangeTest(t, {
+ rangeStart: { rangeName: 'contain', offset: CSS.percent(0) } ,
+ rangeEnd: { rangeName: 'contain', offset: CSS.percent(100) },
+ startOffset: 800,
+ endOffset: 1000
+ });
+ await runTimelineRangeTest(t, {
+ rangeStart: { rangeName: 'entry', offset: CSS.percent(0) },
+ rangeEnd: { rangeName: 'entry', offset: CSS.percent(100) },
+ startOffset: 600,
+ endOffset: 800
+ });
+ await runTimelineRangeTest(t, {
+ rangeStart: { rangeName: 'entry-crossing', offset: CSS.percent(0) },
+ rangeEnd: { rangeName: 'entry-crossing', offset: CSS.percent(100) },
+ startOffset: 600,
+ endOffset: 1000
+ });
+ await runTimelineRangeTest(t, {
+ rangeStart: { rangeName: 'exit', offset: CSS.percent(0) },
+ rangeEnd: { rangeName: 'exit', offset: CSS.percent(100) },
+ startOffset: 1000,
+ endOffset: 1200
+ });
+ await runTimelineRangeTest(t, {
+ rangeStart: { rangeName: 'exit-crossing', offset: CSS.percent(0) },
+ rangeEnd: { rangeName: 'exit-crossing', offset: CSS.percent(100) },
+ startOffset: 800,
+ endOffset: 1200
+ });
+ await runTimelineRangeTest(t, {
+ rangeStart: { rangeName: 'contain', offset: CSS.percent(-50) },
+ rangeEnd: { rangeName: 'entry', offset: CSS.percent(200) },
+ startOffset: 700,
+ endOffset: 1000
+ });
+ await runTimelineRangeTest(t, {
+ rangeStart: { rangeName: 'entry' },
+ rangeEnd: { rangeName: 'exit' },
+ startOffset: 600,
+ endOffset: 1200
+ });
+ await runTimelineRangeTest(t, {
+ rangeStart: { offset: CSS.percent(0) },
+ rangeEnd: { offset: CSS.percent(100) },
+ startOffset: 600,
+ endOffset: 1200
+ });
+
+ }, 'View timeline with range set via delays.' );
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-range.html b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-range.html
new file mode 100644
index 0000000000..5042c6c2a0
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-range.html
@@ -0,0 +1,198 @@
+<!DOCTYPE html>
+<html id="top">
+<meta charset="utf-8">
+<title>View timeline delay</title>
+<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#viewtimeline-interface">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<script src="/scroll-animations/scroll-timelines/testcommon.js"></script>
+<script src="/scroll-animations/view-timelines/testcommon.js"></script>
+<style>
+ #container {
+ border: 10px solid lightgray;
+ overflow-x: scroll;
+ height: 200px;
+ width: 200px;
+ }
+ #content {
+ display: flex;
+ flex-flow: row nowrap;
+ justify-content: flex-start;
+ width: 1800px;
+ margin: 0;
+ }
+ .spacer {
+ width: 800px;
+ display: inline-block;
+ }
+ #target {
+ background-color: green;
+ height: 100px;
+ width: 100px;
+ display: inline-block;
+ font-size: 10px;
+ }
+</style>
+<body>
+ <div id="container">
+ <div id="content">
+ <div class="spacer"></div>
+ <div id="target"></div>
+ <div class="spacer"></div>
+ </div>
+ </div>
+</body>
+<script type="text/javascript">
+ promise_test(async t => {
+ // Delays are associated with the animation and not with the timeline.
+ // Thus adjusting the delays has no effect on the timeline offsets. The
+ // offsets always correspond to the 'cover' range.
+ const verifyTimelineOffsets = anim => {
+ const timeline = anim.timeline;
+ assert_px_equals(timeline.startOffset, 600, 'startOffset');
+ assert_px_equals(timeline.endOffset, 900, 'endOffset');
+ };
+ await runTimelineRangeTest(t, {
+ rangeStart: { rangeName: 'cover', offset: CSS.percent(0) } ,
+ rangeEnd: { rangeName: 'cover', offset: CSS.percent(100) },
+ startOffset: 600,
+ endOffset: 900
+ }).then(anim => {
+ verifyTimelineOffsets(anim);
+ });
+ await runTimelineRangeTest(t, {
+ rangeStart: { rangeName: 'contain', offset: CSS.percent(0) } ,
+ rangeEnd: { rangeName: 'contain', offset: CSS.percent(100) },
+ startOffset: 700,
+ endOffset: 800
+ }).then(anim => {
+ verifyTimelineOffsets(anim);
+ });
+ await runTimelineRangeTest(t, {
+ rangeStart: { rangeName: 'entry', offset: CSS.percent(0) },
+ rangeEnd: { rangeName: 'entry', offset: CSS.percent(100) },
+ startOffset: 600,
+ endOffset: 700
+ });
+ await runTimelineRangeTest(t, {
+ rangeStart: { rangeName: 'entry-crossing', offset: CSS.percent(0) },
+ rangeEnd: { rangeName: 'entry-crossing', offset: CSS.percent(100) },
+ startOffset: 600,
+ endOffset: 700
+ });
+ await runTimelineRangeTest(t, {
+ rangeStart: { rangeName: 'exit', offset: CSS.percent(0) },
+ rangeEnd: { rangeName: 'exit', offset: CSS.percent(100) },
+ startOffset: 800,
+ endOffset: 900
+ });
+ await runTimelineRangeTest(t, {
+ rangeStart: { rangeName: 'exit-crossing', offset: CSS.percent(0) },
+ rangeEnd: { rangeName: 'exit-crossing', offset: CSS.percent(100) },
+ startOffset: 800,
+ endOffset: 900
+ });
+ await runTimelineRangeTest(t, {
+ rangeStart: { rangeName: 'contain', offset: CSS.percent(-50) },
+ rangeEnd: { rangeName: 'entry', offset: CSS.percent(200) },
+ startOffset: 650,
+ endOffset: 800
+ });
+ }, 'View timeline with range as <name> <percent> pair.' );
+
+ promise_test(async t => {
+ await runTimelineRangeTest(t, {
+ rangeStart: { rangeName: 'entry' },
+ rangeEnd: { rangeName: 'exit' },
+ startOffset: 600,
+ endOffset: 900
+ });
+ await runTimelineRangeTest(t, {
+ rangeStart: { offset: CSS.percent(0) },
+ rangeEnd: { offset: CSS.percent(100) },
+ startOffset: 600,
+ endOffset: 900
+ });
+ }, 'View timeline with range and inferred name or offset.' );
+
+ promise_test(async t => {
+ await runTimelineRangeTest(t, {
+ rangeStart: { rangeName: 'cover', offset: CSS.px(20) } ,
+ rangeEnd: { rangeName: 'cover', offset: CSS.px(100) },
+ startOffset: 620,
+ endOffset: 700
+ });
+
+ await runTimelineRangeTest(t, {
+ rangeStart: { rangeName: 'contain', offset: CSS.px(20) } ,
+ rangeEnd: { rangeName: 'contain', offset: CSS.px(100) },
+ startOffset: 720,
+ endOffset: 800
+ });
+
+ await runTimelineRangeTest(t, {
+ rangeStart: { rangeName: 'entry', offset: CSS.px(20) } ,
+ rangeEnd: { rangeName: 'entry', offset: CSS.px(100) },
+ startOffset: 620,
+ endOffset: 700
+ });
+
+ await runTimelineRangeTest(t, {
+ rangeStart: { rangeName: 'exit', offset: CSS.px(20) } ,
+ rangeEnd: { rangeName: 'exit', offset: CSS.px(80) },
+ startOffset: 820,
+ endOffset: 880
+ });
+
+ }, 'View timeline with range as <name> <px> pair.' );
+
+ promise_test(async t => {
+ await runTimelineRangeTest(t, {
+ rangeStart: {
+ rangeName: 'contain',
+ offset: new CSSMathSum(CSS.percent(0), CSS.px(20))
+ },
+ rangeEnd: {
+ rangeName: 'contain',
+ offset: new CSSMathSum(CSS.percent(100), CSS.px(-10))
+ },
+ startOffset: 720,
+ endOffset: 790
+ });
+
+ }, 'View timeline with range as <name> <percent+px> pair.' );
+
+ promise_test(async t => {
+ await runTimelineRangeTest(t, {
+ rangeStart: "contain -50%",
+ rangeEnd: "entry 200%",
+ startOffset: 650,
+ endOffset: 800
+ });
+
+ await runTimelineRangeTest(t, {
+ rangeStart: "contain 20px",
+ rangeEnd: "contain 100px",
+ startOffset: 720,
+ endOffset: 800
+ });
+
+ await runTimelineRangeTest(t, {
+ rangeStart: "contain calc(0% + 20px)",
+ rangeEnd: "contain calc(100% - 10px)",
+ startOffset: 720,
+ endOffset: 790
+ });
+
+ await runTimelineRangeTest(t, {
+ rangeStart: "exit 2em",
+ rangeEnd: "exit 8em",
+ startOffset: 820,
+ endOffset: 880
+ });
+
+
+ }, 'View timeline with range as strings.');
+
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-root-source.html b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-root-source.html
new file mode 100644
index 0000000000..20ac9c5464
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-root-source.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<html id="top">
+<meta charset="utf-8">
+<title>View timeline delay</title>
+<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#viewtimeline-interface">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<script src="/scroll-animations/scroll-timelines/testcommon.js"></script>
+<script src="/scroll-animations/view-timelines/testcommon.js"></script>
+<style>
+ #target {
+ margin: 200vh;
+ background-color: green;
+ height: 100px;
+ width: 100px;
+ }
+</style>
+<body>
+ <div id="target"></div>
+</body>
+<script type="text/javascript">
+ promise_test(async t => {
+ const timeline = new ViewTimeline({ subject: target });
+ const anim = target.animate({ opacity: [0, 1 ] },
+ { timeline: timeline,
+ rangeStart: "entry 0%",
+ rangeEnd: "entry 100%",
+ fill: "both" });
+ const scroller = document.scrollingElement;
+ const scrollRange = scroller.scrollHeight - scroller.clientHeight;
+
+ await anim.ready;
+
+ await waitForNextFrame();
+ scroller.scrollTop = scrollRange / 2;
+ await waitForNextFrame();
+
+ assert_equals(getComputedStyle(target).opacity, "1");
+ }, 'Test view-timeline with document scrolling element.');
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-snapport.html b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-snapport.html
new file mode 100644
index 0000000000..5d68d37037
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-snapport.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<title>ViewTimeline vs. scroll-padding-*</title>
+<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#view-timelines">
+<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#view-progress-visibility-range">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<script src="/scroll-animations/scroll-timelines/testcommon.js"></script>
+<script src="/scroll-animations/view-timelines/testcommon.js"></script>
+<style>
+ #container {
+ border: 10px solid lightgray;
+ overflow-y: scroll;
+ height: 200px;
+ width: 200px;
+ scroll-padding: 40px;
+ }
+ .spacer {
+ height: 800px;
+ }
+ #target {
+ background-color: green;
+ height: 200px;
+ width: 100px;
+ }
+</style>
+<body>
+ <div id="container">
+ <div id="leading-space" class="spacer"></div>
+ <div id="target"></div>
+ <div id="trailing-space" class="spacer"></div>
+ </div>
+</body>
+<script>
+ promise_test(async t => {
+ container.scrollTop = 0;
+ await waitForNextFrame();
+
+ const anim = CreateViewTimelineOpacityAnimation(t, target);
+ await anim.ready;
+
+ // 0%
+ container.scrollTop = 600;
+ await waitForNextFrame();
+ assert_percents_equal(anim.currentTime, 0);
+
+ // 50%
+ container.scrollTop = 800;
+ await waitForNextFrame();
+ assert_percents_equal(anim.currentTime, 50);
+
+ // 100%
+ container.scrollTop = 1000;
+ await waitForNextFrame();
+ assert_percents_equal(anim.currentTime, 100);
+ }, 'Default ViewTimeline is not affected by scroll-padding');
+</script>
+</html>
diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-source.tentative.html b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-source.tentative.html
new file mode 100644
index 0000000000..f8aabc8bdd
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-source.tentative.html
@@ -0,0 +1,94 @@
+<!DOCTYPE html>
+<html id="top">
+<meta charset="utf-8">
+<title>View timeline source</title>
+<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#viewtimeline-interface">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<style>
+#outer {
+ height: 400px;
+ width: 400px;
+ overflow: clip;
+}
+
+#inner {
+ height: 300px;
+ width: 300px;
+ overflow: clip;
+}
+
+#outer.scroller,
+#inner.scroller {
+ overflow: scroll;
+}
+
+#spacer {
+ height: 1000px;
+}
+
+#target {
+ background: green;
+ height: 40px;
+ width: 40px;
+}
+</style>
+<body>
+ <div id="outer" class="scroller">
+ <div id="inner" class="scroller">
+ <div id="target"></div>
+ <div id="spacer"></div>
+ </div>
+ </div>
+</body>
+<script>
+'use strict';
+
+function resetScrollers() {
+ inner.classList.add('scroller');
+ outer.classList.add('scroller');
+}
+
+function assert_source_id(viewTimeline, expected) {
+ const source = viewTimeline.source;
+ assert_true(!!source, 'No source');
+ assert_equals(source.id, expected);
+}
+
+promise_test(async t => {
+ t.add_cleanup(resetScrollers);
+ const viewTimeline = new ViewTimeline({ subject: target });
+ assert_equals(viewTimeline.subject, target);
+ assert_source_id(viewTimeline, 'inner');
+
+ inner.classList.remove('scroller');
+ assert_source_id(viewTimeline, 'outer');
+
+ outer.classList.remove('scroller');
+ assert_source_id(viewTimeline, 'top');
+}, 'Default source for a View timeline is the nearest scroll ' +
+ 'ancestor to the subject');
+
+promise_test(async t => {
+ t.add_cleanup(resetScrollers);
+ const viewTimeline =
+ new ViewTimeline({ source: outer, subject: target });
+ assert_equals(viewTimeline.subject, target);
+ assert_source_id(viewTimeline, 'inner');
+}, 'View timeline ignores explicitly set source');
+
+promise_test(async t => {
+ t.add_cleanup(resetScrollers);
+ const viewTimeline =
+ new ViewTimeline({ subject: target });
+ assert_equals(viewTimeline.subject, target);
+ assert_source_id(viewTimeline, 'inner');
+
+ target.style = "display: none";
+ assert_equals(viewTimeline.source, null);
+
+}, 'View timeline source is null when display:none');
+
+</script>
+</html>
diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-subject-size-changes.html b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-subject-size-changes.html
new file mode 100644
index 0000000000..9ae4b1df77
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-subject-size-changes.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<html id="top">
+<meta charset="utf-8">
+<title>View timeline Subject size changes after creation of Animation</title>
+<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#viewtimeline-interface">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<script src="/scroll-animations/scroll-timelines/testcommon.js"></script>
+<script src="/scroll-animations/view-timelines/testcommon.js"></script>
+<style>
+ #container {
+ border: 10px solid lightgray;
+ overflow-y: scroll;
+ height: 400px;
+ width: 400px;
+ }
+ .spacer {
+ height: 500px;
+ }
+ #target {
+ background-color: green;
+ height: 100px;
+ width: 100px;
+ }
+</style>
+<body>
+ <div id="container">
+ <div class="spacer"></div>
+ <div id="target"></div>
+ <div class="spacer"></div>
+ </div>
+</body>
+
+<script type="text/javascript">
+promise_test(async t => {
+ const options = {
+ timeline: { axis: 'vertical' },
+ animation: {
+ rangeStart: { rangeName: 'entry', offset: CSS.percent(0) },
+ rangeEnd: { rangeName: 'entry', offset: CSS.percent(100) },
+ // Set fill to accommodate floating point precision errors at the
+ // endpoints.
+ fill: 'both'
+ }
+ };
+
+ container.scrollTop = 0;
+ await waitForNextFrame();
+
+ const anim = CreateViewTimelineOpacityAnimation(t, target, options);
+ const timeline = anim.timeline;
+ anim.effect.updateTiming(options.timing);
+ await anim.ready;
+
+ // Advance to the start offset, which triggers entry to the active phase.
+ container.scrollTop = 100;
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(target).opacity, '0.3',
+ `Effect at the start of the active phase`);
+
+ // Advance to the midpoint of the animation.
+ container.scrollTop = 150;
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(target).opacity,'0.5',
+ `Effect at the midpoint of the active range`);
+
+ // Since the height of the target is cut in half, the animation should be at the end now.
+ target.style.height = '50px';
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(target).opacity, '0.7',
+ `Effect at the end of the active range`);
+
+ // Advance to the midpoint of the animation again.
+ container.scrollTop = 125;
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(target).opacity,'0.5',
+ `Effect at the midpoint of the active range again`);
+
+ }, 'View timeline with subject size change after the creation of the animation');
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/zero-intrinsic-iteration-duration.tentative.html b/testing/web-platform/tests/scroll-animations/view-timelines/zero-intrinsic-iteration-duration.tentative.html
new file mode 100644
index 0000000000..e77cf4629c
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/view-timelines/zero-intrinsic-iteration-duration.tentative.html
@@ -0,0 +1,106 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<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>
+<script src="/web-animations/resources/keyframe-utils.js"></script>
+<script src="/scroll-animations/scroll-timelines/testcommon.js"></script>
+<title>Animation range updates play state</title>
+</head>
+<style type="text/css">
+ @keyframes anim {
+ from { background-color: blue; }
+ to { background-color: white; }
+ }
+ #scroller {
+ border: 10px solid lightgray;
+ overflow-y: scroll;
+ overflow-x: hidden;
+ width: 300px;
+ height: 200px;
+ }
+ #target {
+ margin-top: 800px;
+ margin-bottom: 800px;
+ margin-left: 10px;
+ margin-right: 10px;
+ width: 100px;
+ height: 100px;
+ z-index: -1;
+ background-color: green;
+ animation: anim auto linear;
+ animation-timeline: t1;
+ view-timeline: t1;
+ }
+</style>
+<body>
+ <div id="scroller">
+ <div id="target"></div>
+ </div>
+</body>
+<script type="text/javascript">
+ async function runTest() {
+ promise_test(async t => {
+ const anim = target.getAnimations()[0];
+ await anim.ready;
+
+ let duration = anim.effect.getComputedTiming().duration;
+ assert_percents_equal(duration, CSS.percent(100),
+ 'Default duration is 100%');
+
+ // Start and end boundaries coincide.
+ anim.rangeStart = "entry 100%";
+ anim.rangeEnd = "contain 0%";
+ duration = anim.effect.getComputedTiming().duration;
+ assert_percents_equal(duration, CSS.percent(0),
+ "Duration is zero when boundaries coincide");
+
+ // Start > end, clamp at zero duration.
+ anim.rangeEnd = "entry 0%"
+ duration = anim.effect.getComputedTiming().duration;
+ assert_percents_equal(duration, CSS.percent(0),
+ "Duration is zero when start > end");
+
+ anim.rangeStart = "normal";
+ anim.rangeEnd = "normal";
+ duration = anim.effect.getComputedTiming().duration;
+ assert_percents_equal(duration, CSS.percent(100),
+ "Duration is 100% after range reset");
+
+ // Consumed 100% of timeline duration with delays
+ anim.effect.updateTiming({
+ delay: CSS.percent(60),
+ endDelay: CSS.percent(40)
+ });
+ duration = anim.effect.getComputedTiming().duration;
+ assert_percents_equal(duration, CSS.percent(0),
+ "Duration is 0% after delays sum to 100%");
+
+ // Delays sum to > 100%
+ anim.effect.updateTiming({
+ delay: CSS.percent(60),
+ endDelay: CSS.percent(60)
+ });
+ duration = anim.effect.getComputedTiming().duration;
+ assert_percents_equal(duration, CSS.percent(0),
+ "Duration is 0% after delays sum to > 100%");
+
+ anim.effect.updateTiming({
+ delay: CSS.percent(40),
+ endDelay: CSS.percent(40)
+ });
+ duration = anim.effect.getComputedTiming().duration;
+ assert_percents_equal(
+ duration, CSS.percent(20),
+ "Duration is 20% if normal range and delays sum to 80%");
+
+ }, 'Intrinsic iteration duration is non-negative');
+ }
+
+
+ window.onload = runTest;
+</script>