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-07 09:22:09 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
commit43a97878ce14b72f0981164f87f2e35e14151312 (patch)
tree620249daf56c0258faa40cbdcf9cfba06de2a846 /testing/web-platform/tests/scroll-animations/view-timelines
parentInitial commit. (diff)
downloadfirefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz
firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/scroll-animations/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/inline-view-timeline-current-time.tentative.html289
-rw-r--r--testing/web-platform/tests/scroll-animations/view-timelines/testcommon.js137
-rw-r--r--testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-delay-large-subject.html93
-rw-r--r--testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-delay.html103
-rw-r--r--testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-get-current-time-range-name.html145
-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-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.html79
12 files changed, 1639 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/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..412145b04c
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/view-timelines/inline-view-timeline-current-time.tentative.html
@@ -0,0 +1,289 @@
+<!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,
+ {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,
+ {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,
+ {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,
+ {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..969f282e67
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/view-timelines/testcommon.js
@@ -0,0 +1,137 @@
+'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 viewTimelineOptions = {
+ subject: target,
+ axis: 'block'
+ };
+ if (options) {
+ for (let key in options) {
+ viewTimelineOptions[key] = options[key];
+ }
+ }
+
+ const anim =
+ target.animate(
+ { opacity: [0.3, 0.7] },
+ { timeline: new ViewTimeline(viewTimelineOptions) });
+ 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 runTimelineRangeTest(t, {
+// timeline: { inset: [ CSS.percent(0), CSS.percent(20)] },
+// timing: { fill: 'both' }
+// rangeStart: 600,
+// rangeEnd: 900
+// });
+async function runTimelineRangeTest(t, options, message) {
+ container.scrollLeft = 0;
+ await waitForNextFrame();
+
+ const anim =
+ options.anim ||
+ CreateViewTimelineOpacityAnimation(t, target, options.timeline);
+ 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.rangeStart;
+ 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.rangeStart + options.rangeEnd) / 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.rangeEnd;
+ 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 delays for a view timeline and ensures that the
+// range aligns with expected values.
+//
+// Sample call:
+// await runTimelineDelayTest(t, {
+// delay: { phase: 'cover', percent: CSS.percent(0) } ,
+// endDelay: { phase: 'cover', percent: CSS.percent(100) },
+// rangeStart: 600,
+// rangeEnd: 900
+// });
+async function runTimelineDelayTest(t, options) {
+ const delayToString = delay => {
+ const parts = [];
+ if (delay.phase)
+ parts.push(delay.phase);
+ if (delay.percent)
+ parts.push(`${delay.percent.value}%`);
+ return parts.join(' ');
+ };
+ const range =
+ `${delayToString(options.delay)} to ` +
+ `${delayToString(options.endDelay)}`;
+
+ options.timeline = {
+ axis: 'inline'
+ };
+ options.timing = {
+ delay: options.delay,
+ endDelay: options.endDelay,
+ // Set fill to accommodate floating point precision errors at the
+ // endpoints.
+ fill: 'both'
+ };
+
+ return runTimelineRangeTest(t, options, range);
+}
+
+// Sets the Inset for a view timeline and ensures that the range aligns with
+// expected values.
+//
+// Sample call:
+// await runTimelineDelayTest(t, {
+// inset: [ CSS.px(20), CSS.px(40) ]
+// rangeStart: 600,
+// rangeEnd: 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 runTimelineRangeTest(t, options, range);
+}
diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-delay-large-subject.html b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-delay-large-subject.html
new file mode 100644
index 0000000000..edb37c1379
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-delay-large-subject.html
@@ -0,0 +1,93 @@
+<!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 runTimelineDelayTest(t, {
+ delay: { phase: 'cover', percent: CSS.percent(0) } ,
+ endDelay: { phase: 'cover', percent: CSS.percent(100) },
+ rangeStart: 600,
+ rangeEnd: 1200
+ });
+ await runTimelineDelayTest(t, {
+ delay: { phase: 'contain', percent: CSS.percent(0) } ,
+ endDelay: { phase: 'contain', percent: CSS.percent(100) },
+ rangeStart: 800,
+ rangeEnd: 1000
+ });
+ await runTimelineDelayTest(t, {
+ delay: { phase: 'enter', percent: CSS.percent(0) },
+ endDelay: { phase: 'enter', percent: CSS.percent(100) },
+ rangeStart: 600,
+ rangeEnd: 800
+ });
+ await runTimelineDelayTest(t, {
+ delay: { phase: 'exit', percent: CSS.percent(0) },
+ endDelay: { phase: 'exit', percent: CSS.percent(100) },
+ rangeStart: 1000,
+ rangeEnd: 1200
+ });
+ await runTimelineDelayTest(t, {
+ delay: { phase: 'contain', percent: CSS.percent(-50) },
+ endDelay: { phase: 'enter', percent: CSS.percent(200) },
+ rangeStart: 700,
+ rangeEnd: 1000
+ });
+ await runTimelineDelayTest(t, {
+ delay: { phase: 'enter' },
+ endDelay: { phase: 'exit' },
+ rangeStart: 600,
+ rangeEnd: 1200
+ });
+ await runTimelineDelayTest(t, {
+ delay: { percent: CSS.percent(0) },
+ endDelay: { percent: CSS.percent(100) },
+ rangeStart: 600,
+ rangeEnd: 1200
+ });
+
+ }, 'View timeline with range set via delays.' );
+</script>
diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-delay.html b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-delay.html
new file mode 100644
index 0000000000..1377dc339c
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-delay.html
@@ -0,0 +1,103 @@
+<!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">
+ 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 runTimelineDelayTest(t, {
+ delay: { phase: 'cover', percent: CSS.percent(0) } ,
+ endDelay: { phase: 'cover', percent: CSS.percent(100) },
+ rangeStart: 600,
+ rangeEnd: 900
+ }).then(anim => {
+ verifyTimelineOffsets(anim);
+ });
+ await runTimelineDelayTest(t, {
+ delay: { phase: 'contain', percent: CSS.percent(0) } ,
+ endDelay: { phase: 'contain', percent: CSS.percent(100) },
+ rangeStart: 700,
+ rangeEnd: 800
+ }).then(anim => {
+ verifyTimelineOffsets(anim);
+ });
+ await runTimelineDelayTest(t, {
+ delay: { phase: 'enter', percent: CSS.percent(0) },
+ endDelay: { phase: 'enter', percent: CSS.percent(100) },
+ rangeStart: 600,
+ rangeEnd: 700
+ });
+ await runTimelineDelayTest(t, {
+ delay: { phase: 'exit', percent: CSS.percent(0) },
+ endDelay: { phase: 'exit', percent: CSS.percent(100) },
+ rangeStart: 800,
+ rangeEnd: 900
+ });
+ await runTimelineDelayTest(t, {
+ delay: { phase: 'contain', percent: CSS.percent(-50) },
+ endDelay: { phase: 'enter', percent: CSS.percent(200) },
+ rangeStart: 650,
+ rangeEnd: 800
+ });
+ await runTimelineDelayTest(t, {
+ delay: { phase: 'enter' },
+ endDelay: { phase: 'exit' },
+ rangeStart: 600,
+ rangeEnd: 900
+ });
+ await runTimelineDelayTest(t, {
+ delay: { percent: CSS.percent(0) },
+ endDelay: { percent: CSS.percent(100) },
+ rangeStart: 600,
+ rangeEnd: 900
+ });
+
+ }, 'View timeline with range set via delays.' );
+</script>
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..8f385e7b6e
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-get-current-time-range-name.html
@@ -0,0 +1,145 @@
+<!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, enter start
+ // 700 px contain start, enter end
+ // 800 px contain end, exit start
+ // 900 px cover end, exit end
+ const anim =
+ CreateViewTimelineOpacityAnimation(t, target,
+ { axis: 'inline', 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('enter'), 0,
+ MAX_SCROLL, 'Scroll aligned with enter 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('enter'), 50,
+ MAX_SCROLL, 'Scroll at enter midpoint');
+
+ container.scrollLeft = 700;
+ await waitForNextFrame();
+
+ assert_percents_approx_equal(timeline.getCurrentTime('enter'), 100,
+ MAX_SCROLL, 'Scroll at enter 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-inset.html b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-inset.html
new file mode 100644
index 0000000000..72480ea9f3
--- /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.
+ // rangeStart = target_pos - viewport_size + end_side_inset
+ // = 600 + end_side_inset
+ // rangeEnd = target_pos + target_size - start_side_inset
+ // = 900 - start_side_inset
+ await runTimelineInsetTest(t, {
+ inset: [ CSS.px(0), CSS.px(0) ],
+ rangeStart: 600,
+ rangeEnd: 900
+ }).then(anim => verifyTimelineOffsets(anim, 600, 900));
+ await runTimelineInsetTest(t, {
+ inset: [ CSS.px(10), CSS.px(20) ],
+ rangeStart: 620,
+ rangeEnd: 890
+ }).then(anim => verifyTimelineOffsets(anim, 620, 890));
+ await runTimelineInsetTest(t, {
+ inset: [ CSS.px(10) ],
+ rangeStart: 610,
+ rangeEnd: 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) ],
+ rangeStart: 600,
+ rangeEnd: 900
+ }).then(anim => verifyTimelineOffsets(anim, 600, 900));
+ await runTimelineInsetTest(t, {
+ inset: [ CSS.percent(10), CSS.percent(20) ],
+ rangeStart: 640,
+ rangeEnd: 880
+ }).then(anim => verifyTimelineOffsets(anim, 640, 880));
+ await runTimelineInsetTest(t, {
+ inset: [ CSS.percent(10) ],
+ rangeStart: 620,
+ rangeEnd: 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" ],
+ rangeStart: 600,
+ rangeEnd: 900
+ });
+ verifyTimelineOffsets(anim, 600, 900);
+ container.classList.add('scroll-padded');
+ await runTimelineRangeTest(t, {
+ anim: anim,
+ rangeStart: 620,
+ rangeEnd: 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) ],
+ rangeStart: 632,
+ rangeEnd: 884
+ });
+ verifyTimelineOffsets(anim, 632, 884);
+ target.classList.add('big-font');
+ await runTimelineRangeTest(t, {
+ anim: anim,
+ rangeStart: 640,
+ rangeEnd: 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) ],
+ rangeStart: 600 + 0.2 * vw,
+ rangeEnd: 900 - 0.1 * vw
+ });
+ await runTimelineInsetTest(t, {
+ inset: [ CSS.vmin(10), CSS.vmin(20) ],
+ rangeStart: 600 + 0.2 * vmin,
+ rangeEnd: 900 - 0.1 * vmin
+ });
+}, 'view timeline with viewport relative insets.');
+
+promise_test(async t => {
+ await runTimelineInsetTest(t, {
+ inset: "10px",
+ rangeStart: 610,
+ rangeEnd: 890
+ });
+ await runTimelineInsetTest(t, {
+ inset: "10px 20px",
+ rangeStart: 620,
+ rangeEnd: 890
+ });
+ await runTimelineInsetTest(t, {
+ inset: "10%",
+ rangeStart: 620,
+ rangeEnd: 880
+ });
+ await runTimelineInsetTest(t, {
+ inset: "10% 20%",
+ rangeStart: 640,
+ rangeEnd: 880
+ });
+ await runTimelineInsetTest(t, {
+ inset: "auto",
+ rangeStart: 600,
+ rangeEnd: 900
+ });
+ await runTimelineInsetTest(t, {
+ inset: "1em 2em",
+ rangeStart: 632,
+ rangeEnd: 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-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..b438317f7c
--- /dev/null
+++ b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-subject-size-changes.html
@@ -0,0 +1,79 @@
+<!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 = {
+ axis: 'vertical',
+ timing: {
+ delay: { phase: 'enter', percent: CSS.percent(0) },
+ endDelay: { phase: 'enter', percent: 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);
+ 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>