diff options
Diffstat (limited to 'testing/web-platform/tests/scroll-animations')
159 files changed, 19381 insertions, 0 deletions
diff --git a/testing/web-platform/tests/scroll-animations/META.yml b/testing/web-platform/tests/scroll-animations/META.yml new file mode 100644 index 0000000000..c7f0e4903b --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/META.yml @@ -0,0 +1,4 @@ +spec: https://drafts.csswg.org/scroll-animations/ +suggested_reviewers: + - birtles + - graouts diff --git a/testing/web-platform/tests/scroll-animations/crashtests/invalid-animation-range.html b/testing/web-platform/tests/scroll-animations/crashtests/invalid-animation-range.html new file mode 100644 index 0000000000..43b23c93b6 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/crashtests/invalid-animation-range.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<title>Invalid animation range</title> +<body onload="runTest()"> + <div id="target"></div> +</body> +<script src="../../web-animations/testcommon.js"></script> +<script> + async function runTest() { + const anim = target.animate(undefined, {rangeStart: "initial" }); + await waitForNextFrame(); + await waitForNextFrame(); + } +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/animation-duration-auto.tentative.html b/testing/web-platform/tests/scroll-animations/css/animation-duration-auto.tentative.html new file mode 100644 index 0000000000..375489c26a --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/animation-duration-auto.tentative.html @@ -0,0 +1,56 @@ +<!DOCTYPE html> +<title>animation-duration: auto</title> +<link rel="help" src="https://github.com/w3c/csswg-drafts/issues/6530"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="./support/testcommon.js"></script> +<script src="/css/support/parsing-testcommon.js"></script> +<script src="/css/support/computed-testcommon.js"></script> +<style> + #scroller { + overflow: hidden; + width: 100px; + height: 100px; + } + #scroller > #content { + height: 200px; + width: 200px; + } + + @keyframes anim { + from { z-index: 0; } + to { z-index: 100; } + } + + #scroller { + scroll-timeline: timeline; + } + + #element { + z-index: -1; + animation-name: anim; + animation-duration: auto; + animation-timeline: timeline; + } +</style> +<main> + <div id=scroller> + <div id=content></div> + <div id=element></div> + </div> +</main> +<script> + promise_test(async (t) => { + await waitForCSSScrollTimelineStyle(); + assert_equals(getComputedStyle(element).zIndex, '0'); + }, 'A value of auto can be specified for animation-duration'); +</script> + +<div id="target"></div> +<script> + test_valid_value('animation-duration', 'auto'); + test_computed_value('animation-duration', 'auto'); + test_valid_value('animation', 'auto cubic-bezier(0, -2, 1, 3) -3s 4 reverse both paused anim'); + test_computed_value('animation', 'auto cubic-bezier(0, -2, 1, 3) -3s 4 reverse both paused anim'); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/animation-range-ignored.html b/testing/web-platform/tests/scroll-animations/css/animation-range-ignored.html new file mode 100644 index 0000000000..f08659635e --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/animation-range-ignored.html @@ -0,0 +1,229 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<link rel="help" src="https://www.w3.org/TR/scroll-animations-1/#named-range-animation-declaration"> +<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> +<script src="/scroll-animations/scroll-timelines/testcommon.js"></script> +<title>Programmatic API overrides animation-range-*</title> +</head> +<style type="text/css"> + #scroller { + border: 10px solid lightgray; + overflow-y: scroll; + overflow-x: hidden; + width: 300px; + height: 200px; + } + @keyframes anim { + from { margin-left: 0px; } + to { margin-left: 100px; } + } + #target { + margin: 800px 0px; + width: 100px; + height: 100px; + z-index: -1; + background-color: green; + } + .animate { + animation: anim auto linear; + view-timeline: timeline; + animation-timeline: timeline; + animation-range-start: entry 0%; + animation-range-end: entry 100%; + } +</style> +<body> + <div id=scroller> + <div id=target></div> + </div> +</body> +<script type="text/javascript"> + async function runTest() { + function startAnimation(t) { + target.classList.add('animate'); + t.add_cleanup(async () => { + target.classList.remove('animate'); + await waitForNextFrame(); + }); + return target.getAnimations()[0]; + } + + promise_test(async t => { + // Points of interest: + // entry 0% @ 600 + // entry 100% / contain 0% @ 700 + // exit 0% / contain 100% @ 800 + // exit 100% @ 900 + const anim = startAnimation(t); + await anim.ready; + + await waitForNextFrame(); + scroller.scrollTop = 650; + await waitForNextFrame(); + + // Timline time = (scroll pos - cover 0%) / (cover 100% - cover 0%) * 100% + // = (650 - 600)/(900 - 600) * 100% = 100/6% + assert_percents_equal(anim.timeline.currentTime, 100/6, + 'timeline\'s current time before style change'); + assert_percents_equal(anim.startTime, 0, + 'animation\'s start time before style change'); + // Range start of entry 0% aligns with timeline start. Thus, animation's + // and timeline's current time are equal. + assert_percents_equal(anim.currentTime, 100/6, + 'animation\'s current time before style change'); + // Iteration duration = + // (range end - range start) / (cover 100% - cover 0%) * 100% + // = (700 - 600) / (900 - 600) = 33.3333% + assert_percents_equal(anim.effect.getComputedTiming().duration, + 100/3, + 'iteration duration before first style change'); + assert_equals(getComputedStyle(target).marginLeft, '50px', + 'margin-left before style change'); + + // Step 1: Set the range end programmatically and range start via CSS. + // The start time will be respected since not previously set via the + // animation API. + anim.rangeEnd = 'contain 100%'; + target.style.animationRangeStart = 'entry 50%'; + await waitForNextFrame(); + + // Animation range does not affect timeline's currentTime. + assert_percents_equal( + anim.timeline.currentTime, 100/6, + 'timeline\'s current time after first set of range updates'); + assert_percents_equal( + anim.startTime, 100/6, + 'animation\'s start time after first set of range updates'); + // Scroll position aligns with range start. + assert_percents_equal( + anim.currentTime, 0, + 'animation\'s current time after first set of range updates'); + // Iteration duration = + // (range end - range start) / (cover 100% - cover 0%) * 100% + // = (800 - 650) / (900 - 600) = 50% + assert_percents_equal( + anim.effect.getComputedTiming().duration, 50, + 'iteration duration after first style change'); + assert_equals(getComputedStyle(target).marginLeft, '0px', + 'margin-left after first set of range updates'); + + // Step 2: Programmatically set the range start. + // Scroll position is current at entry 50%, thus the animation's current + // time is negative. + anim.rangeStart = 'contain 0%'; + // animation current time = + // (scroll pos - range start) / (cover 100% - cover 0%) * 100% + // = (650 - 700) / (900 - 600) * 100% = -100/6% + assert_percents_equal( + anim.currentTime, -100/6, + 'animation\'s current time after second set of range updates'); + // Iteration duration = + // (range end - range start) / (cover 100% - cover 0%) * 100% + // = (800 - 700) / (900 - 600) = 33.3333% + assert_percents_equal( + anim.effect.getComputedTiming().duration, 100/3, + 'iteration duration after second style change'); + assert_equals(getComputedStyle(target).marginLeft, '0px', + 'margin-left after second set of range updates'); + + // Jump to contain / cover 50% + scroller.scrollTop = 750; + await waitForNextFrame(); + + // animation current time = + // (scroll pos - range start) / (cover 100% - cover 0%) * 100% + // = (750 - 700) / (900 - 600) * 100% = 100/6% + assert_percents_equal( + anim.currentTime, 100/6, + 'animation\'s current time after bumping scroll position'); + assert_equals(getComputedStyle(target).marginLeft, '50px'); + + // Step 3: Try to update the range start via CSS. This change must be + // ignored since previously set programmatically. + target.style.animationRangeStart = "entry 50%"; + await waitForNextFrame(); + assert_percents_equal( + anim.currentTime, 100/6, + 'Current time unchanged after change to ignored CSS property'); + assert_equals( + getComputedStyle(target).marginLeft, '50px', + 'Margin-left unaffected by change to ignored CSS property'); + + }, 'Animation API call rangeStart overrides animation-range-start'); + + promise_test(async t => { + const anim = startAnimation(t); + await anim.ready; + + await waitForNextFrame(); + scroller.scrollTop = 650; + await waitForNextFrame(); + + // Step 1: Set the range start programmatically and range end via CSS. + // The start time will be respected since not previously set via the + // animation API. + anim.rangeStart = "entry 50%"; + target.style.animationRangeEnd = "contain 100%"; + await waitForNextFrame(); + + assert_percents_equal( + anim.timeline.currentTime, 100/6, + 'timeline\'s current time after first set of range updates'); + assert_percents_equal( + anim.startTime, 100/6, + 'animation\'s start time after first set of range updates'); + assert_percents_equal( + anim.currentTime, 0, + 'animation\'s current time after first set of range updates'); + assert_percents_equal( + anim.effect.getComputedTiming().duration, 50, + 'iteration duration after first style change'); + assert_equals(getComputedStyle(target).marginLeft, "0px", + 'margin-left after first set of range updates'); + + // Step 2: Programmatically set the range. + // Scroll position is current at entry 50%, thus the animation's current + // time is negative. + anim.rangeStart = "contain 0%"; + anim.rangeEnd = "contain 100%"; + + assert_percents_equal( + anim.currentTime, -100/6, + 'animation\'s current time after second set of range updates'); + assert_percents_equal( + anim.effect.getComputedTiming().duration, 100/3, + 'iteration duration after second style change'); + assert_equals(getComputedStyle(target).marginLeft, "0px", + 'margin-left after second set of range updates'); + + // Jump to contain / cover 50% + scroller.scrollTop = 750; + await waitForNextFrame(); + + assert_percents_equal( + anim.currentTime, 100/6, + 'animation\'s current time after bumping scroll position'); + assert_equals(getComputedStyle(target).marginLeft, "50px"); + + // Step 3: Try to update the range end via CSS. This change must be + // ignored since previously set programmatically. + target.style.animationRangeEnd = "cover 100%"; + await waitForNextFrame(); + assert_percents_equal( + anim.currentTime, 100/6, + 'Current time unchanged after change to ignored CSS property'); + assert_equals( + getComputedStyle(target).marginLeft, '50px', + 'Margin-left unaffected by change to ignored CSS property'); + + }, 'Animation API call rangeEnd overrides animation-range-end'); + } + + window.onload = runTest; +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/animation-range-normal-matches-cover.html b/testing/web-platform/tests/scroll-animations/css/animation-range-normal-matches-cover.html new file mode 100644 index 0000000000..44b08cab96 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/animation-range-normal-matches-cover.html @@ -0,0 +1,92 @@ +<!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="/scroll-animations/scroll-timelines/testcommon.js"></script> +<title>Animation range 'normal' is equivalent to animation range 'cover'</title> +</head> +<style type="text/css"> + @keyframes anim-1 { + from { background-color: blue; } + to { background-color: white; } + } + @keyframes anim-2 { + from { opacity: 0.3; } + to { opacity: 1; } + } + #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-1 auto linear, anim-2 auto linear; + animation-range: normal, cover; + view-timeline: t1; + animation-timeline: t1, t1; + } +</style> +<body> + <div id="scroller"> + <div id="target"></div> + </div> +</body> +<script type="text/javascript"> + async function runTest() { + function assert_range_equals(actual, expected) { + if (typeof expected == 'string') { + assert_equals(actual, expected); + } else { + assert_equals(actual.rangeName, expected.rangeName); + assert_equals(actual.offset.value, expected.offset.value); + } + } + + promise_test(async t => { + anims = target.getAnimations(); + assert_equals(anims.length, 2, "Expecting 2 animations"); + await anims[0].ready; + await anims[1].ready; + + assert_range_equals(anims[0].rangeStart, "normal"); + assert_range_equals(anims[0].rangeEnd, "normal"); + assert_range_equals(anims[1].rangeStart, + { rangeName: 'cover', offset: CSS.percent(0) }); + assert_range_equals(anims[1].rangeEnd, + { rangeName: 'cover', offset: CSS.percent(100) }); + + scroller.scrollTop = 600; // Start boundary for cover range. + await waitForNextFrame(); + + assert_percents_equal(anims[0].currentTime, 0, + 'currentTime at start of normal range'); + assert_percents_equal(anims[1].currentTime, 0, + 'currentTime at cover 0%'); + + scroller.scrollTop = 900; // End boundary for cover range. + await waitForNextFrame(); + + assert_percents_equal(anims[0].currentTime, 100, + 'currentTime at end of normal range'); + assert_percents_equal(anims[1].currentTime, 100, + 'currentTime at cover 100%'); + }, 'Changing the animation range updates the play state'); + } + + window.onload = runTest; +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/animation-shorthand.html b/testing/web-platform/tests/scroll-animations/css/animation-shorthand.html new file mode 100644 index 0000000000..7bd17b9919 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/animation-shorthand.html @@ -0,0 +1,142 @@ +<!DOCTYPE html> +<link rel="help" href="https://drafts.csswg.org/css-animations-2/#animation-shorthand"> +<link rel="help" href="https://drafts.csswg.org/css-animations-2/#animation-timeline"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/css/support/computed-testcommon.js"></script> +<script src="/css/support/parsing-testcommon.js"></script> +<script src="/css/support/shorthand-testcommon.js"></script> +<div id="target"></div> +<script> +test_valid_value('animation', + '1s linear 1s 2 reverse forwards paused anim'); + +test_invalid_value('animation', + '1s linear 1s 2 reverse forwards paused anim initial'); +test_invalid_value('animation', + '1s linear 1s 2 reverse forwards paused anim 2000'); +test_invalid_value('animation', + '1s linear 1s 2 reverse forwards paused anim scroll()'); +test_invalid_value('animation', + '1s linear 1s 2 reverse forwards paused anim view()'); +test_invalid_value('animation', + '1s linear 1s 2 reverse forwards paused anim timeline'); + +test_computed_value('animation', + '1s linear 1s 2 reverse forwards paused anim'); + +test_shorthand_value('animation', + `1s linear 1s 2 reverse forwards paused anim1, + 1s linear 1s 2 reverse forwards paused anim2, + 1s linear 1s 2 reverse forwards paused anim3`, +{ + 'animation-duration': '1s, 1s, 1s', + 'animation-timing-function': 'linear, linear, linear', + 'animation-delay': '1s, 1s, 1s', + 'animation-iteration-count': '2, 2, 2', + 'animation-direction': 'reverse, reverse, reverse', + 'animation-fill-mode': 'forwards, forwards, forwards', + 'animation-play-state': 'paused, paused, paused', + 'animation-name': 'anim1, anim2, anim3', + 'animation-timeline': 'auto, auto, auto', + 'animation-range-start': 'normal, normal, normal', + 'animation-range-end': 'normal, normal, normal', +}); + +test((t) => { + t.add_cleanup(() => { + target.style = ''; + }); + + target.style.animation = 'anim 1s'; + target.style.animationTimeline = 'timeline'; + assert_equals(target.style.animation, ''); + assert_equals(target.style.animationName, 'anim'); + assert_equals(target.style.animationDuration, '1s'); +}, 'Animation shorthand can not represent non-initial timelines (specified)'); + +test((t) => { + t.add_cleanup(() => { + target.style = ''; + }); + + target.style.animation = 'anim 1s'; + target.style.animationTimeline = 'timeline'; + assert_equals(getComputedStyle(target).animation, ''); + assert_equals(getComputedStyle(target).animationName, 'anim'); + assert_equals(getComputedStyle(target).animationDuration, '1s'); +}, 'Animation shorthand can not represent non-initial timelines (computed)'); + +test((t) => { + t.add_cleanup(() => { + target.style = ''; + }); + + target.style.animation = 'anim 1s'; + target.style.animationDelayEnd = '42s'; + assert_equals(target.style.animation, ''); + assert_equals(target.style.animationName, 'anim'); + assert_equals(target.style.animationDuration, '1s'); +}, 'Animation shorthand can not represent non-initial animation-delay-end (specified)'); + +test((t) => { + t.add_cleanup(() => { + target.style = ''; + }); + + target.style.animation = 'anim 1s'; + target.style.animationDelayEnd = '42s'; + assert_equals(getComputedStyle(target).animation, ''); + assert_equals(getComputedStyle(target).animationName, 'anim'); + assert_equals(getComputedStyle(target).animationDuration, '1s'); +}, 'Animation shorthand can not represent non-initial animation-delay-end (computed)'); + +test((t) => { + t.add_cleanup(() => { + target.style = ''; + }); + + target.style.animation = 'anim 1s'; + target.style.animationRangeStart = 'entry'; + assert_equals(target.style.animation, ''); + assert_equals(target.style.animationName, 'anim'); + assert_equals(target.style.animationDuration, '1s'); +}, 'Animation shorthand can not represent non-initial animation-range-start (specified)'); + +test((t) => { + t.add_cleanup(() => { + target.style = ''; + }); + + target.style.animation = 'anim 1s'; + target.style.animationRangeStart = 'entry'; + assert_equals(getComputedStyle(target).animation, ''); + assert_equals(getComputedStyle(target).animationName, 'anim'); + assert_equals(getComputedStyle(target).animationDuration, '1s'); +}, 'Animation shorthand can not represent non-initial animation-range-start (computed)'); + +test((t) => { + t.add_cleanup(() => { + target.style = ''; + }); + + target.style.animation = 'anim 1s'; + target.style.animationRangeEnd = 'entry'; + assert_equals(target.style.animation, ''); + assert_equals(target.style.animationName, 'anim'); + assert_equals(target.style.animationDuration, '1s'); +}, 'Animation shorthand can not represent non-initial animation-range-end (specified)'); + +test((t) => { + t.add_cleanup(() => { + target.style = ''; + }); + + target.style.animation = 'anim 1s'; + target.style.animationRangeEnd = 'entry'; + assert_equals(getComputedStyle(target).animation, ''); + assert_equals(getComputedStyle(target).animationName, 'anim'); + assert_equals(getComputedStyle(target).animationDuration, '1s'); +}, 'Animation shorthand can not represent non-initial animation-range-end (computed)'); + +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/animation-timeline-computed.html b/testing/web-platform/tests/scroll-animations/css/animation-timeline-computed.html new file mode 100644 index 0000000000..7759e799c6 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/animation-timeline-computed.html @@ -0,0 +1,73 @@ +<!DOCTYPE html> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#animation-timeline"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/css/support/computed-testcommon.js"></script> +</head> +<style> + #outer { animation-timeline: foo; } + #target { animation-timeline: bar; } +</style> +<div id="outer"> + <div id="target"></div> +</div> +<script> +test_computed_value('animation-timeline', 'initial', 'auto'); +test_computed_value('animation-timeline', 'inherit', 'foo'); +test_computed_value('animation-timeline', 'unset', 'auto'); +test_computed_value('animation-timeline', 'revert', 'auto'); +test_computed_value('animation-timeline', 'auto'); +test_computed_value('animation-timeline', 'none'); +test_computed_value('animation-timeline', 'auto, auto'); +test_computed_value('animation-timeline', 'none, none'); +test_computed_value('animation-timeline', 'auto, none'); +test_computed_value('animation-timeline', 'none, auto'); +test_computed_value('animation-timeline', 'test'); +test_computed_value('animation-timeline', 'test1, test2'); +test_computed_value('animation-timeline', 'test1, test2, none, test3, auto', 'test1, test2, none, test3, auto'); + +test(() => { + let style = getComputedStyle(document.getElementById('target')); + assert_not_equals(Array.from(style).indexOf('animation-timeline'), -1); +}, 'The animation-timeline property shows up in CSSStyleDeclaration enumeration'); + +test(() => { + let style = document.getElementById('target').style; + assert_not_equals(style.cssText.indexOf('animation-timeline'), -1); +}, 'The animation-timeline property shows up in CSSStyleDeclaration.cssText'); + +// https://drafts.csswg.org/scroll-animations-1/#scroll-notation +// +// animation-timeline: scroll(<axis>? <scroller>?); +// <axis> = block | inline | vertical | horizontal +// <scroller> = root | nearest | self +test_computed_value('animation-timeline', 'scroll()'); +test_computed_value('animation-timeline', 'scroll(block)', 'scroll()'); +test_computed_value('animation-timeline', 'scroll(inline)'); +test_computed_value('animation-timeline', 'scroll(horizontal)'); +test_computed_value('animation-timeline', 'scroll(vertical)'); +test_computed_value('animation-timeline', 'scroll(root)'); +test_computed_value('animation-timeline', 'scroll(nearest)', 'scroll()'); +test_computed_value('animation-timeline', 'scroll(self)'); +test_computed_value('animation-timeline', 'scroll(self), scroll(nearest)', 'scroll(self), scroll()'); +test_computed_value('animation-timeline', 'scroll(inline nearest)', 'scroll(inline)'); +test_computed_value('animation-timeline', 'scroll(nearest inline)', 'scroll(inline)'); +test_computed_value('animation-timeline', 'scroll(block self)', 'scroll(self)'); +test_computed_value('animation-timeline', 'scroll(self block)', 'scroll(self)'); +test_computed_value('animation-timeline', 'scroll(vertical root)', 'scroll(root vertical)'); + +// https://drafts.csswg.org/scroll-animations-1/#view-notation +test_computed_value('animation-timeline', 'view()'); +test_computed_value('animation-timeline', 'view(block)', 'view()'); +test_computed_value('animation-timeline', 'view(inline)', 'view(inline)'); +test_computed_value('animation-timeline', 'view(horizontal)', 'view(horizontal)'); +test_computed_value('animation-timeline', 'view(vertical)', 'view(vertical)'); +test_computed_value('animation-timeline', 'view(vertical 1px)'); +test_computed_value('animation-timeline', 'view(1px auto)'); +test_computed_value('animation-timeline', 'view(auto 1px)'); +test_computed_value('animation-timeline', 'view(vertical 1px auto)'); +test_computed_value('animation-timeline', 'view(1px vertical)', 'view(vertical 1px)'); +test_computed_value('animation-timeline', 'view(vertical auto)', 'view(vertical)'); +test_computed_value('animation-timeline', 'view(vertical auto auto)', 'view(vertical)'); + +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/animation-timeline-ignored.tentative.html b/testing/web-platform/tests/scroll-animations/css/animation-timeline-ignored.tentative.html new file mode 100644 index 0000000000..0ac7a9d63e --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/animation-timeline-ignored.tentative.html @@ -0,0 +1,147 @@ +<!DOCTYPE html> +<link rel="help" src="https://github.com/w3c/csswg-drafts/pull/5666"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<style> + main { + overflow: hidden; + height: 0px; + scroll-timeline-attachment: defer; + scroll-timeline-name: timeline1, timeline2, timeline3; + } + .scroller { + overflow: hidden; + width: 100px; + height: 100px; + scroll-timeline-attachment: ancestor; + } + .scroller > div { + height: 200px; + } + @keyframes expand { + from { width: 100px; } + to { width: 200px; } + } + #scroller1 { + scroll-timeline-name: timeline1; + } + #scroller2 { + scroll-timeline-name: timeline2; + } + #scroller3 { + scroll-timeline-name: timeline3; + } + #element { + width: 0px; + height: 20px; + animation-name: expand; + animation-duration: 1000s; + animation-timing-function: linear; + animation-timeline: timeline1; + } + /* Ensure stable expectations if feature is not supported */ + @supports not (animation-timeline:foo) { + #element { animation-play-state: paused; } + } +</style> +<main> + <div class=scroller id=scroller1><div></div></div> + <div class=scroller id=scroller2><div></div></div> + <div class=scroller id=scroller3><div></div></div> + <div class=scroller id=scroller4><div></div></div> + <div id=container></div> +</main> +<script> + // Force layout of scrollers. + scroller1.offsetTop; + scroller2.offsetTop; + scroller3.offsetTop; + scroller4.offsetTop; + + scroller1.scrollTop = 20; + scroller2.scrollTop = 40; + scroller3.scrollTop = 60; + scroller4.scrollTop = 80; + + // Create #element in #container, run |func|, then clean up afterwards. + function test_animation_timeline(func, description) { + promise_test(async () => { + try { + let element = document.createElement('element'); + element.setAttribute('id', 'element'); + container.append(element); + await func(); + } finally { + while (container.firstChild) + container.firstChild.remove(); + } + }, description); + } + + test_animation_timeline(async () => { + await waitForNextFrame(); + assert_equals(getComputedStyle(element).width, '120px'); + element.style = 'animation-timeline:timeline2'; + assert_equals(getComputedStyle(element).width, '140px'); + }, 'Changing animation-timeline changes the timeline (sanity check)'); + + test_animation_timeline(async () => { + await waitForNextFrame(); + assert_equals(getComputedStyle(element).width, '120px'); + + // Set a (non-CSS) ScrollTimeline on the CSSAnimation. + let timeline4 = new ScrollTimeline({ + source: scroller4, + scrollOffsets: [CSS.px(0), CSS.px(100)] + }); + + element.getAnimations()[0].timeline = timeline4; + assert_equals(getComputedStyle(element).width, '180px'); + + // Changing the animation-timeline property should have no effect. + element.style = 'animation-timeline:timeline2'; + assert_equals(getComputedStyle(element).width, '180px'); + }, 'animation-timeline ignored after setting timeline with JS (ScrollTimeline from JS)'); + + test_animation_timeline(async () => { + await waitForNextFrame(); + assert_equals(getComputedStyle(element).width, '120px'); + let animation = element.getAnimations()[0]; + let timeline1 = animation.timeline; + + element.style = 'animation-timeline:timeline2'; + assert_equals(getComputedStyle(element).width, '140px'); + + animation.timeline = timeline1; + assert_equals(getComputedStyle(element).width, '120px'); + + // Should have no effect. + element.style = 'animation-timeline:timeline3'; + assert_equals(getComputedStyle(element).width, '120px'); + }, 'animation-timeline ignored after setting timeline with JS (ScrollTimeline from CSS)'); + + test_animation_timeline(async () => { + await waitForNextFrame(); + assert_equals(getComputedStyle(element).width, '120px'); + element.getAnimations()[0].timeline = document.timeline; + + // (The animation continues from where the previous timeline left it). + assert_equals(getComputedStyle(element).width, '120px'); + + // Changing the animation-timeline property should have no effect. + element.style = 'animation-timeline:timeline2'; + assert_equals(getComputedStyle(element).width, '120px'); + }, 'animation-timeline ignored after setting timeline with JS (document timeline)'); + + test_animation_timeline(async () => { + await waitForNextFrame(); + assert_equals(getComputedStyle(element).width, '120px'); + element.getAnimations()[0].timeline = null; + assert_equals(getComputedStyle(element).width, '120px'); + + // Changing the animation-timeline property should have no effect. + element.style = 'animation-timeline:timeline2'; + assert_equals(getComputedStyle(element).width, '120px'); + }, 'animation-timeline ignored after setting timeline with JS (null)'); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/animation-timeline-in-keyframe.html b/testing/web-platform/tests/scroll-animations/css/animation-timeline-in-keyframe.html new file mode 100644 index 0000000000..7548333139 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/animation-timeline-in-keyframe.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#animation-timeline"> +<link rel="help" href="https://drafts.csswg.org/css-animations-1/#keyframes"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +</head> +<style> + @keyframes test { + from { width: 100px; animation-timeline: foo; } + to { width: 100px; animation-timeline: foo; } + } + #target { + width: 50px; + animation-name: test; + animation-duration: 1s; + animation-play-state: paused; + } +</style> +<div id="target"></div> +<script> +test(() => { + let style = getComputedStyle(document.getElementById('target')); + // Checking 'width' verifies that the animation is applied at all. + assert_equals(style.width, '100px'); + assert_equals(style.animationTimeline, 'auto'); +}, 'The animation-timeline property may not be used in keyframes'); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/animation-timeline-multiple.html b/testing/web-platform/tests/scroll-animations/css/animation-timeline-multiple.html new file mode 100644 index 0000000000..50a829c5b6 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/animation-timeline-multiple.html @@ -0,0 +1,99 @@ +<!DOCTYPE html> +<title>animation-timeline with multiple timelines</title> +<link rel="help" src="https://drafts.csswg.org/css-animations-2/#animation-timeline"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<style> + main { + scroll-timeline-attachment: defer; + scroll-timeline-name: top_timeline, bottom_timeline, left_timeline, right_timeline; + } + + .scroller { + overflow: hidden; + width: 100px; + height: 100px; + scroll-timeline-attachment: ancestor; + } + .scroller > div { + height: 200px; + width: 200px; + } + + @keyframes top { + from { top: 100px; } + to { top: 200px; } + } + @keyframes bottom { + from { bottom: 100px; } + to { bottom: 200px; } + } + @keyframes left { + from { left: 100px; } + to { left: 200px; } + } + @keyframes right { + from { right: 100px; } + to { right: 200px; } + } + + #top_scroller { + scroll-timeline-name: top_timeline; + scroll-timeline-axis: block; + } + #bottom_scroller { + scroll-timeline-name: bottom_timeline; + scroll-timeline-axis: inline; + } + #left_scroller { + scroll-timeline-name: left_timeline; + scroll-timeline-axis: block; + } + #right_scroller { + scroll-timeline-name: right_timeline; + scroll-timeline-axis: inline; + } + + #element { + animation-name: top, bottom, left, right; + animation-duration: 10s; + animation-timing-function: linear; + animation-timeline: top_timeline, bottom_timeline, left_timeline, right_timeline; + } + /* Ensure stable expectations if feature is not supported */ + @supports not (animation-timeline:foo) { + #element { animation-play-state: paused; } + } +</style> +<main> + <div class=scroller id=top_scroller><div></div></div> + <div class=scroller id=bottom_scroller><div></div></div> + <div class=scroller id=left_scroller><div></div></div> + <div class=scroller id=right_scroller><div></div></div> + <div id=element></div> +</main> +<script> + // Force layout of scrollers. + top_scroller.offsetTop; + bottom_scroller.offsetTop; + left_scroller.offsetTop; + right_scroller.offsetTop; + + top_scroller.scrollTop = 20; + top_scroller.scrollLeft = 40; + bottom_scroller.scrollTop = 20; + bottom_scroller.scrollLeft = 40; + left_scroller.scrollTop = 60; + left_scroller.scrollLeft = 80; + right_scroller.scrollTop = 60; + right_scroller.scrollLeft = 80; + + promise_test(async (t) => { + await waitForNextFrame(); + assert_equals(getComputedStyle(element).top, '120px'); + assert_equals(getComputedStyle(element).bottom, '140px'); + assert_equals(getComputedStyle(element).left, '160px'); + assert_equals(getComputedStyle(element).right, '180px'); + }, 'animation-timeline works with multiple timelines'); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/animation-timeline-named-scroll-progress-timeline.tentative.html b/testing/web-platform/tests/scroll-animations/css/animation-timeline-named-scroll-progress-timeline.tentative.html new file mode 100644 index 0000000000..8dcf48c4ac --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/animation-timeline-named-scroll-progress-timeline.tentative.html @@ -0,0 +1,431 @@ +<!DOCTYPE html> +<title>The animation-timeline: scroll-timeline-name</title> +<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1"> +<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/rewrite#scroll-timelines-named"> +<link rel="help" src="https://github.com/w3c/csswg-drafts/issues/6674"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="support/testcommon.js"></script> +<script src="/scroll-animations/scroll-timelines/testcommon.js"></script> +<style> + @keyframes anim { + from { translate: 50px; } + to { translate: 150px; } + } + @keyframes anim-2 { + from { z-index: 0; } + to { z-index: 100; } + } + + #target { + width: 100px; + height: 100px; + } + .square { + width: 100px; + height: 100px; + } + .square-container { + width: 300px; + height: 300px; + } + .scroller { + overflow: scroll; + } + .content { + inline-size: 100%; + block-size: 100%; + padding-inline-end: 100px; + padding-block-end: 100px; + } +</style> +<body> +<div id="log"></div> +<script> +"use strict"; + +setup(assert_implements_animation_timeline); + +function createScroller(t, scrollerSizeClass) { + let scroller = document.createElement('div'); + let className = scrollerSizeClass || 'square'; + scroller.className = `scroller ${className}`; + let content = document.createElement('div'); + content.className = 'content'; + + scroller.appendChild(content); + + t.add_cleanup(function() { + content.remove(); + scroller.remove(); + }); + + return scroller; +} + +function createTarget(t) { + let target = document.createElement('div'); + target.id = 'target'; + + t.add_cleanup(function() { + target.remove(); + }); + + return target; +} + +function createScrollerAndTarget(t, scrollerSizeClass) { + return [createScroller(t, scrollerSizeClass), createTarget(t)]; +} + +async function waitForScrollTop(scroller, percentage) { + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = maxScroll * percentage / 100; + return waitForNextFrame(); +} + +async function waitForScrollLeft(scroller, percentage) { + const maxScroll = scroller.scrollWidth - scroller.clientWidth; + scroller.scrollLeft = maxScroll * percentage / 100; + return waitForNextFrame(); +} + +// ------------------------- +// Test scroll-timeline-name +// ------------------------- + +promise_test(async t => { + let target = document.createElement('div'); + target.id = 'target'; + target.className = 'scroller'; + let content = document.createElement('div'); + content.className = 'content'; + + // <div id='target' class='scroller'> + // <div id='content'></div> + // </div> + document.body.appendChild(target); + target.appendChild(content); + + target.style.scrollTimelineName = 'timeline'; + target.style.animation = "anim 10s linear"; + target.style.animationTimeline = 'timeline'; + + target.scrollTop = 50; // 50% + await waitForNextFrame(); + assert_equals(getComputedStyle(target).translate, '100px'); + + content.remove(); + target.remove(); +}, 'scroll-timeline-name is referenceable in animation-timeline on the ' + + 'declaring element itself'); + +promise_test(async t => { + let [parent, target] = createScrollerAndTarget(t, 'square-container'); + + // <div id='parent' class='scroller'> + // <div id='target'></div> + // <div id='content'></div> + // </div> + document.body.appendChild(parent); + parent.insertBefore(target, parent.firstElementChild); + + parent.style.scrollTimelineName = 'timeline'; + target.style.animation = "anim 10s linear"; + target.style.animationTimeline = 'timeline'; + + parent.scrollTop = 100; // 50% + await waitForNextFrame(); + assert_equals(getComputedStyle(target).translate, '100px'); +}, "scroll-timeline-name is referenceable in animation-timeline on that " + + "element's descendants"); + +// See https://github.com/w3c/csswg-drafts/issues/7047 +promise_test(async t => { + let [sibling, target] = createScrollerAndTarget(t); + + // <div id='sibling' class='scroller'> ... </div> + // <div id='target'></div> + document.body.appendChild(sibling); + document.body.appendChild(target); + + // Resolvable if using a deferred timeline, but otherwise can only resolve + // if an ancestor container of the target element. + sibling.style.scrollTimelineName = 'timeline'; + target.style.animation = "anim 10s linear"; + target.style.animationTimeline = 'timeline'; + + sibling.scrollTop = 50; // 50% + await waitForNextFrame(); + assert_equals(getComputedStyle(target).translate, '50px', + 'Animation with unknown timeline name holds current time at zero'); +}, "scroll-timeline-name is not referenceable in animation-timeline on that " + + "element's siblings"); + +promise_test(async t => { + let parent = document.createElement('div'); + parent.className = 'square'; + parent.style.overflowX = 'clip'; // This makes overflow-y be clip as well. + let target = document.createElement('div'); + target.id = 'target'; + + // <div id='parent' style='overflow-x: clip'>... + // <div id='target'></div> + // </div> + document.body.appendChild(parent); + parent.appendChild(target); + + parent.style.scrollTimelineName = 'timeline'; + target.style.animation = "anim 10s linear"; + target.style.animationTimeline = 'timeline'; + + await waitForNextFrame(); + assert_equals(getComputedStyle(target).translate, 'none', + 'Animation with an unresolved current time'); + + target.remove(); + parent.remove(); +}, 'scroll-timeline-name on an element which is not a scroll-container'); + +promise_test(async t => { + let [scroller, target] = createScrollerAndTarget(t); + + // <div id='scroller' class='scroller'> ... + // <div id='target'></div> + // </div> + + document.body.appendChild(scroller); + scroller.appendChild(target); + + scroller.style.scrollTimelineName = 'timeline-A'; + scroller.scrollTop = 50; // 25% + target.style.animation = "anim 10s linear"; + target.style.animationTimeline = 'timeline-B'; + + await waitForNextFrame(); + + const anim = target.getAnimations()[0]; + assert_true(!!anim, 'Failed to create animation'); + assert_equals(anim.timeline, null); + // Hold time of animation is zero. + assert_equals(getComputedStyle(target).translate, '50px'); + + scroller.style.scrollTimelineName = 'timeline-B'; + await waitForNextFrame(); + + assert_true(!!anim.timeline, 'Failed to create timeline'); + assert_equals(getComputedStyle(target).translate, '75px'); +}, 'Change in scroll-timeline-name to match animation timeline updates animation.'); + +promise_test(async t => { + let [scroller, target] = createScrollerAndTarget(t); + + // <div id='scroller' class='scroller'> ... + // <div id='target'></div> + // </div> + + document.body.appendChild(scroller); + scroller.appendChild(target); + + scroller.style.scrollTimelineName = 'timeline-A'; + scroller.scrollTop = 50; // 25% + target.style.animation = "anim 10s linear"; + target.style.animationTimeline = 'timeline-A'; + + await waitForNextFrame(); + + const anim = target.getAnimations()[0]; + assert_true(!!anim, 'Failed to create animation'); + assert_true(!!anim.timeline, 'Failed to create timeline'); + assert_equals(getComputedStyle(target).translate, '75px'); + assert_percents_equal(anim.startTime, 0); + assert_percents_equal(anim.currentTime, 25); + + scroller.style.scrollTimelineName = 'timeline-B'; + await waitForNextFrame(); + + // Switching to a null timeline pauses the animation. + assert_equals(anim.timeline, null, 'Failed to remove timeline'); + assert_equals(getComputedStyle(target).translate, '75px'); + assert_equals(anim.startTime, null); + assert_times_equal(anim.currentTime, 2500); +}, 'Change in scroll-timeline-name to no longer match animation timeline updates animation.'); + +promise_test(async t => { + let target = createTarget(t); + let scroller1 = createScroller(t); + let scroller2 = createScroller(t); + + target.style.animation = 'anim 10s linear'; + target.style.animationTimeline = 'timeline'; + scroller1.style.scrollTimelineName = 'timeline'; + scroller1.id = 'A'; + scroller2.id = 'B'; + + // <div class='scroller' id='A'> ... + // <div class='scroller' id='B'> ... + // <div id='target'></div> + // </div> + // </div> + document.body.appendChild(scroller1); + scroller1.appendChild(scroller2); + scroller2.appendChild(target); + + scroller1.style.scrollTimelineName = 'timeline'; + scroller1.scrollTop = 50; // 25% + scroller2.scrollTop = 100; // 50% + + await waitForNextFrame(); + + const anim = target.getAnimations()[0]; + + assert_true(!!anim.timeline, 'Failed to retrieve animation'); + assert_equals(anim.timeline.source.id, 'A'); + assert_equals(getComputedStyle(target).translate, '75px'); + + scroller2.style.scrollTimelineName = 'timeline'; + await waitForNextFrame(); + + // The timeline should be updated to scroller2. + assert_true(!!anim.timeline, 'Animation no longer has a timeline'); + assert_equals(anim.timeline.source.id, 'B', 'Timeline not updated'); + assert_equals(getComputedStyle(target).translate, '100px'); +}, 'Timeline lookup updates candidate when closer match available.'); + +promise_test(async t => { + let wrapper = createScroller(t); + wrapper.classList.remove('scroller'); + let target = createTarget(t); + + // <div id='wrapper'> ... + // <div id='target'></div> + // </div> + document.body.appendChild(wrapper); + wrapper.appendChild(target); + target.style.animation = "anim 10s linear"; + target.style.animationTimeline = 'timeline'; + + await waitForNextFrame(); + + // Timeline initially cannot be resolved, resulting in a null + // timeline. The animation's hold time is zero. + let anim = document.getAnimations()[0]; + assert_equals(getComputedStyle(target).translate, '50px'); + + await waitForNextFrame(); + + wrapper.classList.add('scroller'); + wrapper.style.scrollTimelineName = 'timeline'; + + // <div id='wrapper' class="scroller"> ... + // <div id='target'></div> + // </div> + wrapper.scrollTop = 50; // 25% + await waitForNextFrame(); + + // The timeline should be updated to scroller. + assert_equals(getComputedStyle(target).translate, '75px'); +}, 'Timeline lookup updates candidate when match becomes available.'); + + +// ------------------------- +// Test scroll-timeline-axis +// ------------------------- + +promise_test(async t => { + let [scroller, target] = createScrollerAndTarget(t); + scroller.style.writingMode = 'vertical-lr'; + + // <div id='scroller' class='scroller'> ... + // <div id='target'></div> + // </div> + document.body.appendChild(scroller); + scroller.appendChild(target); + + scroller.style.scrollTimeline = 'timeline block'; + target.style.animation = "anim-2 10s linear"; + target.style.animationTimeline = 'timeline'; + + await waitForScrollLeft(scroller, 50); + assert_equals(getComputedStyle(target).zIndex, '50'); +}, 'scroll-timeline-axis is block'); + +promise_test(async t => { + let [scroller, target] = createScrollerAndTarget(t); + scroller.style.writingMode = 'vertical-lr'; + + // <div id='scroller' class='scroller'> ... + // <div id='target'></div> + // </div> + document.body.appendChild(scroller); + scroller.appendChild(target); + + scroller.style.scrollTimeline = 'timeline inline'; + target.style.animation = "anim-2 10s linear"; + target.style.animationTimeline = 'timeline'; + + await waitForScrollTop(scroller, 50); + assert_equals(getComputedStyle(target).zIndex, '50'); +}, 'scroll-timeline-axis is inline'); + +promise_test(async t => { + let [scroller, target] = createScrollerAndTarget(t); + scroller.style.writingMode = 'vertical-lr'; + + // <div id='scroller' class='scroller'> ... + // <div id='target'></div> + // </div> + document.body.appendChild(scroller); + scroller.appendChild(target); + + scroller.style.scrollTimeline = 'timeline horizontal'; + target.style.animation = "anim-2 10s linear"; + target.style.animationTimeline = 'timeline'; + + await waitForScrollLeft(scroller, 50); + assert_equals(getComputedStyle(target).zIndex, '50'); +}, 'scroll-timeline-axis is horizontal'); + +promise_test(async t => { + let [scroller, target] = createScrollerAndTarget(t); + scroller.style.writingMode = 'vertical-lr'; + + // <div id='scroller' class='scroller'> ... + // <div id='target'></div> + // </div> + document.body.appendChild(scroller); + scroller.appendChild(target); + + scroller.style.scrollTimeline = 'timeline vertical'; + target.style.animation = "anim-2 10s linear"; + target.style.animationTimeline = 'timeline'; + + await waitForScrollTop(scroller, 50); + assert_equals(getComputedStyle(target).zIndex, '50'); +}, 'scroll-timeline-axis is vertical'); + +promise_test(async t => { + let [scroller, target] = createScrollerAndTarget(t); + + // <div id='scroller' class='scroller'> ... + // <div id='target'></div> + // </div> + document.body.appendChild(scroller); + scroller.appendChild(target); + + scroller.style.scrollTimeline = 'timeline block'; + target.style.animation = "anim-2 10s linear"; + target.style.animationTimeline = 'timeline'; + + await waitForScrollTop(scroller, 25); + await waitForScrollLeft(scroller, 75); + assert_equals(getComputedStyle(target).zIndex, '25'); + + scroller.style.scrollTimelineAxis = 'inline'; + await waitForNextFrame(); + assert_equals(getComputedStyle(target).zIndex, '75'); +}, 'scroll-timeline-axis is mutated'); + +</script> +</body> diff --git a/testing/web-platform/tests/scroll-animations/css/animation-timeline-none.html b/testing/web-platform/tests/scroll-animations/css/animation-timeline-none.html new file mode 100644 index 0000000000..a8e07a44d6 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/animation-timeline-none.html @@ -0,0 +1,41 @@ +<!DOCTYPE html> +<link rel="help" src="https://drafts.csswg.org/css-animations-2/#animation-timeline"> +<link rel="help" src="https://drafts.csswg.org/web-animations/#playing-an-animation-section"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<style> + @keyframes expand { + from { width: 100px; } + to { width: 200px; } + } + + .test { + width: 0px; + animation-name: expand; + animation-duration: 1s; + } + + #element_timeline_none { + animation-timeline: none; + } + #element_unknown_timeline { + animation-timeline: unknown_timeline; + } + +</style> +<div class=test id=element_timeline_none></div> +<div class=test id=element_unknown_timeline></div> +<script> + promise_test(async (t) => { + assert_equals(getComputedStyle(element_timeline_none).width, '100px'); + await waitForAnimationFrames(3); + assert_equals(getComputedStyle(element_timeline_none).width, '100px'); + }, 'Animation with animation-timeline:none holds current time at zero'); + + promise_test(async (t) => { + assert_equals(getComputedStyle(element_unknown_timeline).width, '100px'); + await waitForAnimationFrames(3); + assert_equals(getComputedStyle(element_unknown_timeline).width, '100px'); + }, 'Animation with unknown timeline name holds current time at zero'); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/animation-timeline-parsing.html b/testing/web-platform/tests/scroll-animations/css/animation-timeline-parsing.html new file mode 100644 index 0000000000..4916f7726f --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/animation-timeline-parsing.html @@ -0,0 +1,85 @@ +<!DOCTYPE html> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#animation-timeline"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/css/support/parsing-testcommon.js"></script> +</head> +<div id="target"></div> +<script> +test_valid_value('animation-timeline', 'initial'); +test_valid_value('animation-timeline', 'inherit'); +test_valid_value('animation-timeline', 'unset'); +test_valid_value('animation-timeline', 'revert'); +test_valid_value('animation-timeline', 'auto'); +test_valid_value('animation-timeline', 'none'); +test_valid_value('animation-timeline', 'auto, auto'); +test_valid_value('animation-timeline', 'none, none'); +test_valid_value('animation-timeline', 'auto, none'); +test_valid_value('animation-timeline', 'none, auto'); +test_valid_value('animation-timeline', 'test'); +test_valid_value('animation-timeline', 'test1, test2'); +test_valid_value('animation-timeline', 'test1, test2, none, test3, auto', ["test1, test2, none, test3, auto", 'test1, test2, none, test3, auto']); + +test_invalid_value('animation-timeline', '10px'); +test_invalid_value('animation-timeline', 'auto auto'); +test_invalid_value('animation-timeline', 'none none'); +test_invalid_value('animation-timeline', 'foo bar'); +test_invalid_value('animation-timeline', '"foo" "bar"'); +test_invalid_value('animation-timeline', 'rgb(1, 2, 3)'); +test_invalid_value('animation-timeline', '#fefefe'); +test_invalid_value('animation-timeline', '"test"'); + +// https://drafts.csswg.org/scroll-animations-1/#scroll-notation +// +// animation-timeline: scroll(<axis>? <scroller>?); +// <axis> = block | inline | vertical | horizontal +// <scroller> = root | nearest | self +test_valid_value('animation-timeline', 'scroll()'); +test_valid_value('animation-timeline', 'scroll(block)', 'scroll()'); +test_valid_value('animation-timeline', 'scroll(inline)'); +test_valid_value('animation-timeline', 'scroll(horizontal)'); +test_valid_value('animation-timeline', 'scroll(vertical)'); +test_valid_value('animation-timeline', 'scroll(root)'); +test_valid_value('animation-timeline', 'scroll(nearest)', 'scroll()'); +test_valid_value('animation-timeline', 'scroll(self)'); +test_valid_value('animation-timeline', 'scroll(inline nearest)', 'scroll(inline)'); +test_valid_value('animation-timeline', 'scroll(nearest inline)', 'scroll(inline)'); +test_valid_value('animation-timeline', 'scroll(block self)', 'scroll(self)'); +test_valid_value('animation-timeline', 'scroll(self block)', 'scroll(self)'); +test_valid_value('animation-timeline', 'scroll(vertical root)', 'scroll(root vertical)'); + +test_invalid_value('animation-timeline', 'scroll(abc root)'); +test_invalid_value('animation-timeline', 'scroll(abc)'); +test_invalid_value('animation-timeline', 'scroll(vertical abc)'); +test_invalid_value('animation-timeline', 'scroll("string")'); + +// https://drafts.csswg.org/scroll-animations-1/#view-notation +test_valid_value('animation-timeline', 'view()'); +test_valid_value('animation-timeline', 'view(block)', 'view()'); +test_valid_value('animation-timeline', 'view(inline)'); +test_valid_value('animation-timeline', 'view(horizontal)'); +test_valid_value('animation-timeline', 'view(vertical)'); +test_valid_value('animation-timeline', 'view(vertical 1px 2px)'); +test_valid_value('animation-timeline', 'view(vertical 1px)'); +test_valid_value('animation-timeline', 'view(vertical auto)', 'view(vertical)'); +test_valid_value('animation-timeline', 'view(vertical auto auto)', 'view(vertical)'); +test_valid_value('animation-timeline', 'view(vertical auto 1px)'); +test_valid_value('animation-timeline', 'view(1px 2px vertical)', 'view(vertical 1px 2px)'); +test_valid_value('animation-timeline', 'view(1px vertical)', 'view(vertical 1px)'); +test_valid_value('animation-timeline', 'view(auto horizontal)', 'view(horizontal)'); +test_valid_value('animation-timeline', 'view(1px 2px)'); +test_valid_value('animation-timeline', 'view(1px)'); +test_valid_value('animation-timeline', 'view(1px 1px)', 'view(1px)'); +test_valid_value('animation-timeline', 'view(1px auto)'); +test_valid_value('animation-timeline', 'view(auto calc(1% + 1px))'); +test_valid_value('animation-timeline', 'view(auto)', 'view()'); +test_valid_value('animation-timeline', 'view(auto auto)', 'view()'); + +test_invalid_value('animation-timeline', 'view(vertical 1px 2px 3px)'); +test_invalid_value('animation-timeline', 'view(1px vertical 3px)'); +test_invalid_value('animation-timeline', 'view(1px 2px 3px)'); +test_invalid_value('animation-timeline', 'view(abc block)'); +test_invalid_value('animation-timeline', 'view(abc)'); +test_invalid_value('animation-timeline', 'view(vertical abc)'); +test_invalid_value('animation-timeline', 'view("string")'); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/animation-timeline-scroll-functional-notation.tentative.html b/testing/web-platform/tests/scroll-animations/css/animation-timeline-scroll-functional-notation.tentative.html new file mode 100644 index 0000000000..09917b4ba5 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/animation-timeline-scroll-functional-notation.tentative.html @@ -0,0 +1,166 @@ +<!DOCTYPE html> +<title>The animation-timeline: scroll() notation</title> +<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1"> +<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/rewrite#scroll-notation"> +<link rel="help" src="https://github.com/w3c/csswg-drafts/issues/6674"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="support/testcommon.js"></script> +<style> + @keyframes anim { + from { translate: 50px; } + to { translate: 150px; } + } + html { + min-height: 100vh; + /* This makes the max scrollable ragne be 100px in root element */ + padding-bottom: 100px; + } + #container { + width: 300px; + height: 300px; + overflow: scroll; + } + #target { + width: 100px; + /* This makes the max scrollable ragne be 100px in the block direction */ + height: 100px; + } + /* large block content */ + .block-content { + block-size: 100%; + } + /* large inline content */ + .inline-content { + inline-size: 100%; + block-size: 5px; + /* This makes the max scrollable ragne be 100px in the inline direction */ + padding-inline-end: 100px; + } +</style> +<body> +<div id="log"></div> +<script> +"use strict"; + +setup(assert_implements_animation_timeline); + +const root = document.scrollingElement; +const createTargetWithStuff = function(t, contentClass) { + let container = document.createElement('div'); + container.id = 'container'; + let target = document.createElement('div'); + target.id = 'target'; + let content = document.createElement('div'); + content.className = contentClass; + + // <div id='container'> + // <div id='target'></div> + // <div class=contentClass></div> + // </div> + document.body.appendChild(container); + container.appendChild(target); + container.appendChild(content); + + if (t && typeof t.add_cleanup === 'function') { + t.add_cleanup(() => { + content.remove(); + target.remove(); + container.remove(); + }); + } + + return [container, target]; +}; + +async function scrollLeft(element, value) { + element.scrollLeft = value; + await waitForNextFrame(); +} + +async function scrollTop(element, value) { + element.scrollTop = value; + await waitForNextFrame(); +} + +promise_test(async t => { + let [container, div] = createTargetWithStuff(t, 'block-content'); + div.style.animation = "anim 10s linear"; + div.style.animationTimeline = "scroll(nearest)"; + + await scrollTop(root, 50); + assert_equals(getComputedStyle(div).translate, '50px'); + + await scrollTop(container, 50); + assert_equals(getComputedStyle(div).translate, '100px'); + + await scrollTop(root, 0); +}, 'animation-timeline: scroll(nearest)'); + +promise_test(async t => { + let [container, div] = createTargetWithStuff(t, 'block-content'); + div.style.animation = "anim 10s linear"; + div.style.animationTimeline = "scroll(root)"; + + await scrollTop(container, 50); + assert_equals(getComputedStyle(div).translate, '50px'); + + await scrollTop(root, 50); + assert_equals(getComputedStyle(div).translate, '100px'); + + await scrollTop(root, 0); +}, 'animation-timeline: scroll(root)'); + +promise_test(async t => { + let [container, div] = createTargetWithStuff(t, 'block-content'); + container.style.animation = "anim 10s linear"; + container.style.animationTimeline = "scroll(self)"; + + await scrollTop(container, 50); + assert_equals(getComputedStyle(container).translate, '100px'); +}, 'animation-timeline: scroll(self)'); + +promise_test(async t => { + let [container, div] = createTargetWithStuff(t, 'block-content'); + div.style.animation = "anim 10s linear"; + div.style.animationTimeline = "scroll(self)"; + + await scrollTop(container, 50); + assert_equals(getComputedStyle(div).translate, 'none'); +}, 'animation-timeline: scroll(self), on non-scroller'); + +promise_test(async t => { + let [container, div] = createTargetWithStuff(t, 'inline-content'); + div.style.animation = "anim 10s linear"; + div.style.animationTimeline = "scroll(inline)"; + + await scrollLeft(container, 50); + assert_equals(getComputedStyle(div).translate, '100px'); +}, 'animation-timeline: scroll(inline)'); + +promise_test(async t => { + let [container, div] = createTargetWithStuff(t, 'block-content'); + container.style.writingMode = 'vertical-lr'; + div.style.animation = "anim 10s linear"; + div.style.animationTimeline = "scroll(horizontal)"; + + await scrollLeft(container, 50); + assert_equals(getComputedStyle(div).translate, '100px'); +}, 'animation-timeline: scroll(horizontal)'); + +promise_test(async t => { + let [container, div] = createTargetWithStuff(t, 'inline-content'); + container.style.writingMode = 'vertical-lr'; + div.style.animation = "anim 10s linear"; + div.style.animationTimeline = "scroll(vertical)"; + + await scrollTop(container, 50); + assert_equals(getComputedStyle(div).translate, '100px'); +}, 'animation-timeline: scroll(vertical)'); + +// TODO: Add more tests which change the overflow property of the container for +// scroll(nearest) + +</script> +</body> diff --git a/testing/web-platform/tests/scroll-animations/css/animation-timeline-view-functional-notation.tentative.html b/testing/web-platform/tests/scroll-animations/css/animation-timeline-view-functional-notation.tentative.html new file mode 100644 index 0000000000..745d76c729 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/animation-timeline-view-functional-notation.tentative.html @@ -0,0 +1,474 @@ +<!DOCTYPE html> +<title>The animation-timeline: view() notation</title> +<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1"> +<link rel="help" src="https://w3c.github.io/csswg-drafts/scroll-animations-1/#view-notation"> +<link rel="help" src="https://github.com/w3c/csswg-drafts/issues/7587"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="support/testcommon.js"></script> +<style> + @keyframes fade-in-out-without-timeline-range { + 0% { opacity: 0; } + 40% { opacity: 1; } + 60% { opacity: 1; } + 100% { opacity: 0; } + } + @keyframes fade-out-without-timeline-range { + 0% { opacity: 1; } + 100% { opacity: 0; } + } + @keyframes change-font-size-without-timeline-range { + 0% { font-size: 10px; } + 100% { font-size: 30px; } + } + @keyframes fade-in-out { + entry 0% { opacity: 0; } + entry 100% { opacity: 1; } + exit 0% { opacity: 1; } + exit 100% { opacity: 0; } + } + @keyframes fade-out { + exit 0% { opacity: 1; } + exit 100% { opacity: 0; } + } + @keyframes change-font-size { + exit 0% { font-size: 10px; } + exit 100% { font-size: 20px; } + } + #container { + width: 200px; + height: 200px; + overflow-y: scroll; + overflow-x: hidden; + } + .target { + width: 100px; + height: 100px; + background-color: red; + } + .content { + width: 400px; + height: 400px; + background-color: blue; + } +</style> + +<body> +<script> +"use strict"; + +setup(assert_implements_animation_timeline); + +const createTargetWithStuff = function(t, divClasses) { + let container = document.createElement('div'); + container.id = 'container'; + document.body.appendChild(container); + + // *** When testing inset + // <div id='container'> + // <div class='content'></div> + // <div class='target'></div> + // <div class='content'></div> + // </div> + // *** When testing axis + // <div id='container'> + // <div class='target'></div> + // <div class='content'></div> + // </div> + + let divs = []; + let target; + for(let className of divClasses) { + let div = document.createElement('div'); + div.className = className; + container.appendChild(div); + + divs.push(div); + if(className === 'target') + target = div; + } + + if (t && typeof t.add_cleanup === 'function') { + t.add_cleanup(() => { + for(let div of divs) + div.remove(); + container.remove(); + }); + } + + return [container, target]; +}; + +async function scrollLeft(element, value) { + element.scrollLeft = value; + await waitForNextFrame(); +} + +async function scrollTop(element, value) { + element.scrollTop = value; + await waitForNextFrame(); +} + +// --------------------------------- +// Tests without timeline range name +// --------------------------------- + +promise_test(async t => { + let [container, div] = createTargetWithStuff(t, ['content', 'target', 'content']); + container.style.overflow = 'hidden'; + div.style.animation = "fade-in-out-without-timeline-range 1s linear"; + div.style.animationTimeline = "view()"; + // So the range is [200px, 500px]. + + await scrollTop(container, 200); + assert_equals(getComputedStyle(div).opacity, '0', 'At 0%'); + await scrollTop(container, 260); + assert_equals(getComputedStyle(div).opacity, '0.5', 'At 20%'); + await scrollTop(container, 320); + assert_equals(getComputedStyle(div).opacity, '1', 'At 40%'); + + await scrollTop(container, 380); + assert_equals(getComputedStyle(div).opacity, '1', 'At 60%'); + await scrollTop(container, 440); + assert_equals(getComputedStyle(div).opacity, '0.5', 'At 80%'); + await scrollTop(container, 500); + assert_equals(getComputedStyle(div).opacity, '0', 'At 100%'); +}, 'animation-timeline: view() without timeline range name'); + +promise_test(async t => { + let [container, div] = createTargetWithStuff(t, ['content', 'target', 'content']); + container.style.overflow = 'hidden'; + div.style.animation = "fade-in-out-without-timeline-range 1s linear"; + div.style.animationTimeline = "view(50px)"; + // So the range is [250px, 450px]. + + await scrollTop(container, 250); + assert_equals(getComputedStyle(div).opacity, '0', 'At 0%'); + await scrollTop(container, 290); + assert_equals(getComputedStyle(div).opacity, '0.5', 'At 20%'); + await scrollTop(container, 330); + assert_equals(getComputedStyle(div).opacity, '1', 'At 40%'); + + await scrollTop(container, 370); + assert_equals(getComputedStyle(div).opacity, '1', 'At 60%'); + await scrollTop(container, 410); + assert_equals(getComputedStyle(div).opacity, '0.5', 'At 80%'); + await scrollTop(container, 450); + assert_equals(getComputedStyle(div).opacity, '0', 'At 100%'); +}, 'animation-timeline: view(50px) without timeline range name'); + +promise_test(async t => { + let [container, div] = createTargetWithStuff(t, ['content', 'target', 'content']); + container.style.overflow = 'hidden'; + div.style.animation = "fade-in-out-without-timeline-range 1s linear"; + div.style.animationTimeline = "view(auto 50px)"; + // So the range is [250px, 500px]. + + await scrollTop(container, 250); + assert_equals(getComputedStyle(div).opacity, '0', 'At 0%'); + await scrollTop(container, 300); + assert_equals(getComputedStyle(div).opacity, '0.5', 'At 20%'); + await scrollTop(container, 350); + assert_equals(getComputedStyle(div).opacity, '1', 'At 40%'); + + await scrollTop(container, 400); + assert_equals(getComputedStyle(div).opacity, '1', 'At 60%'); + await scrollTop(container, 450); + assert_equals(getComputedStyle(div).opacity, '0.5', 'At 80%'); + await scrollTop(container, 500); + assert_equals(getComputedStyle(div).opacity, '0', 'At 100%'); +}, 'animation-timeline: view(auto 50px) without timeline range name'); + +promise_test(async t => { + let [container, div] = createTargetWithStuff(t, ['target', 'content']); + container.style.overflow = 'hidden'; + div.style.animation = "fade-out-without-timeline-range 1s linear"; + div.style.animationTimeline = "view(inline)"; + // So the range is [-200px, 100px], but it is impossible to scroll to the + // negative part. + + await scrollLeft(container, 0); + assert_approx_equals(parseFloat(getComputedStyle(div).opacity), 0.33333, + 0.00001, 'At 66.7%'); + // Note: 20% for each 60px. + await scrollLeft(container, 40); + assert_equals(getComputedStyle(div).opacity, '0.2', 'At 80%'); + await scrollLeft(container, 100); + assert_equals(getComputedStyle(div).opacity, '0', 'At 100%'); +}, 'animation-timeline: view(inline) without timeline range name'); + +promise_test(async t => { + let [container, div] = createTargetWithStuff(t, ['target', 'content']); + container.style.overflow = 'hidden'; + div.style.animation = "fade-out-without-timeline-range 1s linear"; + div.style.animationTimeline = "view(horizontal)"; + // So the range is [-200px, 100px], but it is impossible to scroll to the + // negative part. + + await scrollLeft(container, 0); + assert_approx_equals(parseFloat(getComputedStyle(div).opacity), 0.33333, + 0.00001, 'At 66.7%'); + // Note: 20% for each 60px. + await scrollLeft(container, 40); + assert_equals(getComputedStyle(div).opacity, '0.2', 'At 80%'); + await scrollLeft(container, 100); + assert_equals(getComputedStyle(div).opacity, '0', 'At 100%'); +}, 'animation-timeline: view(horizontal) without timeline range name'); + +promise_test(async t => { + let [container, div] = createTargetWithStuff(t, ['target', 'content']); + div.style.animation = "fade-out-without-timeline-range 1s linear"; + div.style.animationTimeline = "view(vertical)"; + // So the range is [-200px, 100px], but it is impossible to scroll to the + // negative part. + + await scrollTop(container, 0); + assert_approx_equals(parseFloat(getComputedStyle(div).opacity), 0.33333, + 0.00001, 'At 66.7%'); + // Note: 20% for each 60px. + await scrollTop(container, 40); + assert_equals(getComputedStyle(div).opacity, '0.2', 'At 80%'); + await scrollTop(container, 100); + assert_equals(getComputedStyle(div).opacity, '0', 'At 100%'); +}, 'animation-timeline: view(vertical) without timeline range name'); + +promise_test(async t => { + let [container, div] = createTargetWithStuff(t, ['target', 'content']); + container.style.overflow = 'hidden'; + div.style.animation = "fade-out-without-timeline-range 1s linear"; + div.style.animationTimeline = "view(horizontal 50px)"; + // So the range is [-150px, 50px], but it is impossible to scroll to the + // negative part. + + // Note: 25% for each 50px. + await scrollLeft(container, 0); + assert_equals(getComputedStyle(div).opacity, '0.25', 'At 75%'); + await scrollLeft(container, 10); + assert_equals(getComputedStyle(div).opacity, '0.2', 'At 80%'); + await scrollLeft(container, 50); + assert_equals(getComputedStyle(div).opacity, '0', 'At 100%'); +}, 'animation-timeline: view(horizontal 50px) without timeline range name'); + +promise_test(async t => { + let [container, div] = createTargetWithStuff(t, ['target', 'content']); + container.style.overflow = 'hidden'; + div.style.animation = "fade-out-without-timeline-range 1s linear, " + + "change-font-size-without-timeline-range 1s linear"; + div.style.animationTimeline = "view(50px), view(inline 50px)"; + + await scrollLeft(container, 0); + assert_equals(getComputedStyle(div).fontSize, '25px', 'At 75% inline'); + await scrollLeft(container, 10); + assert_equals(getComputedStyle(div).fontSize, '26px', 'At 80% inline'); + await scrollLeft(container, 50); + assert_equals(getComputedStyle(div).fontSize, '30px', 'At 100% inline'); + + await scrollLeft(container, 0); + + await scrollTop(container, 0); + assert_equals(getComputedStyle(div).opacity, '0.25', 'At 75% block'); + await scrollTop(container, 10); + assert_equals(getComputedStyle(div).opacity, '0.2', 'At 80% block'); + await scrollTop(container, 50); + assert_equals(getComputedStyle(div).opacity, '0', 'At 100% block'); + + await scrollLeft(container, 10); + await scrollTop(container, 10); + assert_equals(getComputedStyle(div).fontSize, '26px', 'At 80% inline'); + assert_equals(getComputedStyle(div).opacity, '0.2', 'At 80% block'); +}, 'animation-timeline: view(50px), view(inline 50px) without timeline range ' + + 'name'); + +promise_test(async t => { + let [container, div] = createTargetWithStuff(t, ['target', 'content']); + container.style.overflow = 'hidden'; + div.style.animation = "fade-out-without-timeline-range 1s linear"; + + div.style.animationTimeline = "view(inline)"; + await scrollLeft(container, 0); + assert_approx_equals(parseFloat(getComputedStyle(div).opacity), 0.33333, + 0.00001, 'At 66.7%'); + await scrollLeft(container, 40); + assert_equals(getComputedStyle(div).opacity, '0.2', 'At 80%'); + await scrollLeft(container, 100); + assert_equals(getComputedStyle(div).opacity, '0', 'At 100%'); + + div.style.animationTimeline = "view(inline 50px)"; + await scrollLeft(container, 0); + assert_equals(getComputedStyle(div).opacity, '0.25', 'At 75%'); + await scrollLeft(container, 50); + assert_equals(getComputedStyle(div).opacity, '0', 'At 100%'); +}, 'animation-timeline: view(inline) changes to view(inline 50px), without' + + 'timeline range name'); + + +// --------------------------------- +// Tests with timeline range name +// --------------------------------- + +promise_test(async t => { + let [container, div] = createTargetWithStuff(t, ['content', 'target', 'content']); + div.style.animation = "fade-in-out 1s linear"; + div.style.animationTimeline = "view()"; + + await scrollTop(container, 200); + assert_equals(getComputedStyle(div).opacity, '0', 'At entry 0%'); + await scrollTop(container, 250); + assert_equals(getComputedStyle(div).opacity, '0.5', 'At entry 50%'); + await scrollTop(container, 300); + assert_equals(getComputedStyle(div).opacity, '1', 'At entry 100%'); + + await scrollTop(container, 400); + assert_equals(getComputedStyle(div).opacity, '1', 'At exit 0%'); + await scrollTop(container, 450); + assert_equals(getComputedStyle(div).opacity, '0.5', 'At exit 50%'); + await scrollTop(container, 500); + assert_equals(getComputedStyle(div).opacity, '0', 'At exit 100%'); +}, 'animation-timeline: view()'); + +promise_test(async t => { + let [container, div] = createTargetWithStuff(t, ['content', 'target', 'content']); + div.style.animation = "fade-in-out 1s linear"; + div.style.animationTimeline = "view(50px)"; + + await scrollTop(container, 250); + assert_equals(getComputedStyle(div).opacity, '0', 'At entry 0%'); + await scrollTop(container, 300); + assert_equals(getComputedStyle(div).opacity, '0.5', 'At entry 50%'); + + await scrollTop(container, 350); + assert_equals(getComputedStyle(div).opacity, '1', 'At entry 100% & exit 0%'); + + await scrollTop(container, 400); + assert_equals(getComputedStyle(div).opacity, '0.5', 'At exit 50%'); + await scrollTop(container, 450); + assert_equals(getComputedStyle(div).opacity, '0', 'At exit 100%'); +}, 'animation-timeline: view(50px)'); + +promise_test(async t => { + let [container, div] = createTargetWithStuff(t, ['content', 'target', 'content']); + div.style.animation = "fade-in-out 1s linear"; + div.style.animationTimeline = "view(auto 50px)"; + + await scrollTop(container, 250); + assert_equals(getComputedStyle(div).opacity, '0', 'At entry 0%'); + await scrollTop(container, 300); + assert_equals(getComputedStyle(div).opacity, '0.5', 'At entry 50%'); + await scrollTop(container, 350); + assert_equals(getComputedStyle(div).opacity, '1', 'At entry 100%'); + + await scrollTop(container, 400); + assert_equals(getComputedStyle(div).opacity, '1', 'At exit 0%'); + await scrollTop(container, 450); + assert_equals(getComputedStyle(div).opacity, '0.5', 'At exit 50%'); + await scrollTop(container, 500); + assert_equals(getComputedStyle(div).opacity, '0', 'At exit 100%'); +}, 'animation-timeline: view(auto 50px)'); + +promise_test(async t => { + let [container, div] = createTargetWithStuff(t, ['target', 'content']); + container.style.overflow = 'scroll'; + div.style.animation = "fade-out 1s linear"; + div.style.animationTimeline = "view(inline)"; + + await scrollLeft(container, 0); + assert_equals(getComputedStyle(div).opacity, '1', 'At exit 0%'); + await scrollLeft(container, 50); + assert_equals(getComputedStyle(div).opacity, '0.5', 'At exit 50%'); + await scrollLeft(container, 100); + assert_equals(getComputedStyle(div).opacity, '0', 'At exit 100%'); +}, 'animation-timeline: view(inline)'); + +promise_test(async t => { + let [container, div] = createTargetWithStuff(t, ['target', 'content']); + container.style.overflow = 'scroll'; + div.style.animation = "fade-out 1s linear"; + div.style.animationTimeline = "view(horizontal)"; + + await scrollLeft(container, 0); + assert_equals(getComputedStyle(div).opacity, '1', 'At exit 0%'); + await scrollLeft(container, 50); + assert_equals(getComputedStyle(div).opacity, '0.5', 'At exit 50%'); + await scrollLeft(container, 100); + assert_equals(getComputedStyle(div).opacity, '0', 'At exit 100%'); +}, 'animation-timeline: view(horizontal)'); + +promise_test(async t => { + let [container, div] = createTargetWithStuff(t, ['target', 'content']); + container.style.overflow = 'scroll'; + div.style.animation = "fade-out 1s linear"; + div.style.animationTimeline = "view(vertical)"; + + await scrollTop(container, 0); + assert_equals(getComputedStyle(div).opacity, '1', 'At exit 0%'); + await scrollTop(container, 50); + assert_equals(getComputedStyle(div).opacity, '0.5', 'At exit 50%'); + await scrollTop(container, 100); + assert_equals(getComputedStyle(div).opacity, '0', 'At exit 100%'); +}, 'animation-timeline: view(vertical)'); + +promise_test(async t => { + let [container, div] = createTargetWithStuff(t, ['target', 'content']); + container.style.overflowY = 'hidden'; + container.style.overflowX = 'scroll'; + div.style.animation = "fade-out 1s linear"; + div.style.animationTimeline = "view(horizontal 50px)"; + + await scrollLeft(container, 0); + assert_equals(getComputedStyle(div).opacity, '0.5', 'At exit 50%'); + await scrollLeft(container, 50); + assert_equals(getComputedStyle(div).opacity, '0', 'At exit 100%'); +}, 'animation-timeline: view(horizontal 50px)'); + +promise_test(async t => { + let [container, div] = createTargetWithStuff(t, ['target', 'content']); + container.style.overflow = 'scroll'; + div.style.animation = "fade-out 1s linear, change-font-size 1s linear"; + div.style.animationTimeline = "view(), view(inline)"; + + await scrollLeft(container, 0); + assert_equals(getComputedStyle(div).fontSize, '10px', 'At exit 0% inline'); + await scrollLeft(container, 50); + assert_equals(getComputedStyle(div).fontSize, '15px', 'At exit 50% inline'); + await scrollLeft(container, 100); + assert_equals(getComputedStyle(div).fontSize, '20px', 'At exit 100% inline'); + + await scrollLeft(container, 0); + + await scrollTop(container, 0); + assert_equals(getComputedStyle(div).opacity, '1', 'At exit 0% block'); + await scrollTop(container, 50); + assert_equals(getComputedStyle(div).opacity, '0.5', 'At exit 50% block'); + await scrollTop(container, 100); + assert_equals(getComputedStyle(div).opacity, '0', 'At exit 100% block'); + + await scrollLeft(container, 50); + await scrollTop(container, 50); + assert_equals(getComputedStyle(div).fontSize, '15px', 'At exit 50% inline'); + assert_equals(getComputedStyle(div).opacity, '0.5', 'At exit 50% block'); +}, 'animation-timeline: view(), view(inline)'); + +promise_test(async t => { + let [container, div] = createTargetWithStuff(t, ['target', 'content']); + container.style.overflowY = 'hidden'; + container.style.overflowX = 'scroll'; + div.style.animation = "fade-out 1s linear"; + + div.style.animationTimeline = "view(inline)"; + await scrollLeft(container, 0); + assert_equals(getComputedStyle(div).opacity, '1', 'At exit 0%'); + await scrollLeft(container, 50); + assert_equals(getComputedStyle(div).opacity, '0.5', 'At exit 50%'); + await scrollLeft(container, 100); + assert_equals(getComputedStyle(div).opacity, '0', 'At exit 100%'); + + div.style.animationTimeline = "view(inline 50px)"; + await scrollLeft(container, 0); + assert_equals(getComputedStyle(div).opacity, '0.5', 'At exit 50%'); + await scrollLeft(container, 50); + assert_equals(getComputedStyle(div).opacity, '0', 'At exit 100%'); +}, 'animation-timeline: view(inline) changes to view(inline 50px)'); + +</script> +</body> diff --git a/testing/web-platform/tests/scroll-animations/css/animation-update-ref.html b/testing/web-platform/tests/scroll-animations/css/animation-update-ref.html new file mode 100644 index 0000000000..7e375a1df7 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/animation-update-ref.html @@ -0,0 +1,55 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<title>Reference file for various tests that update an animation with a scroll timeline</title> +<script src="/web-animations/testcommon.js"></script> +</head> +<style type="text/css"> + #scroller { + border: 1px solid black; + overflow: hidden; + width: 300px; + height: 200px; + } + #target { + margin-bottom: 800px; + margin-top: 800px; + margin-left: 10px; + margin-right: 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"> + document.documentElement.addEventListener('TestRendered', async () => { + runTest(); + }, { once: true }); + + async function runTest() { + // Defaults to exit 60% if using a view timeline with subject = target. + const DEFAULT_SCROLL_POS = 860; + await waitForCompositorReady(); + + const urlParams = new URLSearchParams(window.location.search); + target.style.transform = + `translateX(${urlParams.get('translate') || "0px"}`; + + scroller.scrollTop = urlParams.get('scroll') || DEFAULT_SCROLL_POS; + await waitForNextFrame(); + await waitForNextFrame(); + + // Make sure change to animation range was properly picked up. + document.documentElement.classList.remove("reftest-wait"); + } +</script> +</body> +</html> diff --git a/testing/web-platform/tests/scroll-animations/css/get-animations-inactive-timeline.html b/testing/web-platform/tests/scroll-animations/css/get-animations-inactive-timeline.html new file mode 100644 index 0000000000..10bf00fbbc --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/get-animations-inactive-timeline.html @@ -0,0 +1,87 @@ +<!DOCTYPE html> +<html> +<meta charset="utf-8"> +<title>getAnimations for scroll-linked animations</title> +<link rel="help" + href="https://www.w3.org/TR/web-animations-1/#animation-effect-phases-and-states"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="support/testcommon.js"></script> +<style> + @keyframes slide { + from { transform: translateX(100px); } + to { transform: translateX(100px); } + } + + #container { + border: 10px solid lightgray; + overflow-x: scroll; + height: 200px; + width: 200px; + scroll-timeline-name: timeline; + } + #spacer { + height: 200vh; + } + #target { + background-color: green; + height: 100px; + width: 100px; + animation: slide 1s linear; + animation-timeline: timeline; + } +</style> +<body> + <div id="container"> + <div id="spacer"></div> + <div id="target"></div> + </div> +</body> +<script type="text/javascript"> + setup(assert_implements_animation_timeline); + + promise_test(async t => { + // Newly created timeline is inactive, + let animations = document.getAnimations(); + assert_equals(animations.length, 1, + 'Single running animation'); + assert_true(animations[0].timeline instanceof ScrollTimeline, + 'Animation associated with a scroll timeline'); + assert_equals(animations[0].timeline.currentTime, null, + 'Timeline is initially inactive'); + + // Canceled animation is no longer current. + const anim = animations[0]; + animations[0].cancel(); + + assert_equals( + document.getAnimations().length, 0, + 'A canceled animation is no longer returned by getAnimations'); + + // Replaying an animation makes it current. + anim.play(); + assert_equals( + document.getAnimations().length, 1, + 'A play-pending animation is return by getAnimations'); + + // Animation effect is still current even if the timeline's source element + // cannot be scrolled. + spacer.style = 'display: none'; + t.add_cleanup(() => { + spacer.style = ''; + }); + + animations = document.getAnimations(); + assert_equals( + animations.length, 1, + 'Running animation is included in getAnimations list even if ' + + 'currentTime is null'); + assert_true(animations[0].timeline instanceof ScrollTimeline, + 'Animation has timeline associated with an element that ' + + 'cannot be scrolled'); + assert_equals(animations[0].timeline.currentTime, null, + 'Inactive timeline when timeline\'s source element cannot ' + + 'be scrolled'); + }, 'getAnimations includes inactive scroll-linked animations that have not ' + + 'been canceled'); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/merge-timeline-offset-keyframes.html b/testing/web-platform/tests/scroll-animations/css/merge-timeline-offset-keyframes.html new file mode 100644 index 0000000000..c6d384fce5 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/merge-timeline-offset-keyframes.html @@ -0,0 +1,135 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>Merge timeline offset keyframes</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="support/testcommon.js"></script> +<script src="/web-animations/resources/keyframe-utils.js"></script> +</head> +<style> + @keyframes anim-1 { + entry 0% { opacity: 0 } + entry 100% { opacity: 1 } + contain 0% { opacity: 0.8 } + entry 100% { opacity: 0.5 } + } + @keyframes anim-2 { + entry 0% { opacity: 0 } + entry 100% { opacity: 1 } + contain 0% { opacity: 0.8 } + entry 100% { opacity: 0.5; animation-timing-function: ease } + } + + #scroller { + border: 10px solid lightgray; + overflow-y: scroll; + overflow-x: hidden; + width: 300px; + height: 200px; + } + #target { + margin-bottom: 800px; + margin-top: 800px; + margin-left: 10px; + margin-right: 10px; + width: 100px; + height: 100px; + z-index: -1; + background-color: green; + animation-duration: auto; + animation-fill-mode: both; + animation-timing-function: linear; + view-timeline: target; + animation-timeline: target; + } + #target.anim-1 { + animation-name: anim-1; + } + #target.anim-2 { + animation-name: anim-2; + } +</style> +<body> + <div id="scroller"> + <div id="target"></div> + </div> +</body> +<script> + async function runTests() { + promise_test(async t => { + target.classList.add('anim-1'); + const anim = target.getAnimations()[0]; + await anim.ready; + t.add_cleanup(() => { + target.classList.remove('anim-1'); + }); + const keyframes = anim.effect.getKeyframes(); + const expected = [ + { + offset: 1, easing: "linear", composite: "replace", opacity: "1", + computedOffset: 1 + }, + { + offset: { rangeName: "entry", offset: CSS.percent(0) }, + easing: "linear", composite: "auto", opacity: "0", + computedOffset: 0 + }, + { + offset: { rangeName: "contain", offset: CSS.percent(0) }, + easing: "linear", composite: "auto", opacity: "0.8", + computedOffset: 1/3 + }, + { + offset: { rangeName: "entry", offset: CSS.percent(100) }, + easing: "linear", composite: "auto", opacity: "0.5", + computedOffset : 1/3 + }]; + assert_frame_lists_equal(keyframes, expected); + }, 'Keyframes with same easing and timeline offset are merged.'); + + promise_test(async t => { + target.classList.add('anim-2'); + const anim = target.getAnimations()[0]; + await anim.ready; + + t.add_cleanup(() => { + target.classList.remove('anim-2'); + }); + + const keyframes = anim.effect.getKeyframes(); + const expected = [ + { + offset: 1, easing: "linear", composite: "replace", opacity: "1", + computedOffset: 1 + }, + { + offset: { rangeName: "entry", offset: CSS.percent(0) }, + easing: "linear", composite: "auto", opacity: "0", + computedOffset: 0 + }, + { + offset: { rangeName: "entry", offset: CSS.percent(100) }, + easing: "linear", composite: "auto", opacity: "1", + computedOffset: 1/3 + }, + { + offset: { rangeName: "contain", offset: CSS.percent(0) }, + easing: "linear", composite: "auto", opacity: "0.8", + computedOffset: 1/3 + }, + { + offset: { rangeName: "entry", offset: CSS.percent(100) }, + easing: "ease", composite: "auto", opacity: "0.5", + computedOffset : 1/3 + }]; + assert_frame_lists_equal(keyframes, expected); + }, 'Keyframes with same timeline offset but different easing function ' + + 'are not merged.'); + } + + window.onload = runTests(); +</script> +</html> diff --git a/testing/web-platform/tests/scroll-animations/css/named-range-keyframes-with-document-timeline.tentative.html b/testing/web-platform/tests/scroll-animations/css/named-range-keyframes-with-document-timeline.tentative.html new file mode 100644 index 0000000000..a0094d3220 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/named-range-keyframes-with-document-timeline.tentative.html @@ -0,0 +1,54 @@ +<!DOCTYPE html> +<html> +<meta charset="utf-8"> +<title>Named range keyframe offset when you have a document timeline</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="support/testcommon.js"></script> +<style> + @keyframes fade-in-animation { + from { opacity: 0 } + + enter 0% { opacity: 0 } + enter 100% { opacity: 1 } + exit 0% { opacity: 1 } + exit 100% { opacity: 0 } + + to { opacity: 1 } + } + + #subject { + background-color: blue; + height: 200px; + width: 200px; + animation: linear both fade-in-animation; + animation-duration: 0.1s; + animation-play-state: paused; + } +</style> +<body onload="runTests()"> + <div id="subject"></div> +</body> + +<script type="text/javascript"> + setup(assert_implements_animation_timeline); + + function runTests() { + promise_test(async t => { + const anim = subject.getAnimations()[0]; + anim.currentTime = -1; + assert_equals(getComputedStyle(subject).opacity, "0", + 'unexpected value in the before phase'); + + anim.currentTime = 50; + assert_equals(getComputedStyle(subject).opacity, "0.5", + 'unexpected value in the middle of the animation'); + + anim.currentTime = 100; + assert_equals(getComputedStyle(subject).opacity, "1", + 'unexpected value in the after phase'); + }); + } +</script> +</html> diff --git a/testing/web-platform/tests/scroll-animations/css/printing/animation-timeline-none-with-progress-print.tentative.html b/testing/web-platform/tests/scroll-animations/css/printing/animation-timeline-none-with-progress-print.tentative.html new file mode 100644 index 0000000000..3939a1df48 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/printing/animation-timeline-none-with-progress-print.tentative.html @@ -0,0 +1,56 @@ +<!DOCTYPE HTML> +<html class="reftest-wait"> +<title>The animation-timeline:none with preserved progress for print</title> +<link rel="help" href="https://drafts.csswg.org/css-animations-2/#animation-timeline"> +<meta name="assert" content="print correctly for an animation with animation-timeline:none with preserved progress"> +<link rel="match" href="animation-timeline-none-with-progress-ref.html"> + +<style> + @keyframes anim { + from { transform: translateX(0px); } + to { transform: translateX(100px); } + } + + #scroller { + scroll-timeline: timeline; + overflow: scroll; + width: 100px; + height: 100px; + scrollbar-width: none; + } + + #contents { + height: 200px; + } + + #box { + width: 100px; + height: 100px; + background-color: green; + animation: anim 1s linear timeline; + } +</style> + +<div id="scroller"> + <div id="contents"></div> +</div> +<div id="box"></div> + +<script> + window.addEventListener('load', function() { + const scroller = document.getElementById("scroller"); + // Move the scroller to the halfway point. + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.5 * maxScroll; + + window.requestAnimationFrame(() => { + let box = document.getElementById("box"); + box.style.animationTimeline = "none"; + getComputedStyle(box).marginLeft; + + window.requestAnimationFrame(() => { + document.documentElement.classList.remove("reftest-wait"); + }); + }); + }); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/printing/animation-timeline-none-with-progress-ref.html b/testing/web-platform/tests/scroll-animations/css/printing/animation-timeline-none-with-progress-ref.html new file mode 100644 index 0000000000..09bcba2fd4 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/printing/animation-timeline-none-with-progress-ref.html @@ -0,0 +1,38 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<title>Reference for none animation-timeline</title> +<style> + #scroller { + overflow: scroll; + width: 100px; + height: 100px; + scrollbar-width: none; + } + + #contents { + height: 200px; + } + + #box { + width: 100px; + height: 100px; + background-color: green; + transform: translateX(50px); + } +</style> + +<div id="scroller"> + <div id="contents"></div> +</div> +<div id="box"></div> + +<script> + window.addEventListener('load', function() { + const scroller = document.getElementById("scroller"); + // Move the scroller to the halfway point. + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.5 * maxScroll; + + document.documentElement.classList.remove("reftest-wait"); + }); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/printing/scroll-timeline-default-iframe-print.html b/testing/web-platform/tests/scroll-animations/css/printing/scroll-timeline-default-iframe-print.html new file mode 100644 index 0000000000..d732ca141a --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/printing/scroll-timeline-default-iframe-print.html @@ -0,0 +1,65 @@ +<!DOCTYPE HTML> +<html class="reftest-wait"> +<title>The default scroll() timeline in the iframe for print</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#scroll-notation"> +<link rel="help" href="https://drafts.csswg.org/css-animations-2/#animation-timeline"> +<meta name="assert" content="CSS animation correctly updates values when using the default scroll() timeline"> +<link rel="match" href="../scroll-timeline-default-iframe-ref.html"> +<meta name="fuzzy" content="25;100"> + +<iframe id="target" width="400" height="400" srcdoc=' + <html> + <style> + @keyframes update { + from { transform: translateY(0px); } + to { transform: translateY(200px); } + } + html { + min-height: 100%; + padding-bottom: 100px; + } + #box { + width: 100px; + height: 100px; + background-color: green; + animation: update 1s linear; + animation-timeline: scroll(); + } + #covered { + width: 100px; + height: 100px; + background-color: red; + } + + * { + margin-top: 0px; + margin-bottom: 0px; + } + </style> + <script> + window.addEventListener("load", function() { + const scroller = document.scrollingElement; + + // Move the scroller to the halfway point. + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.5 * maxScroll; + + window.requestAnimationFrame(() => { + window.parent.postMessage("ready", "*"); + }); + }); + </script> + <body> + <div id="box"></div> + <div id="covered"></div> + </body> + </html> +'></iframe> + +<script> + window.addEventListener("message", event => { + if (event.data == "ready") { + document.documentElement.classList.remove("reftest-wait"); + } + }, false); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/printing/scroll-timeline-default-print-ref.html b/testing/web-platform/tests/scroll-animations/css/printing/scroll-timeline-default-print-ref.html new file mode 100644 index 0000000000..6610f7a5a7 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/printing/scroll-timeline-default-print-ref.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<title>Reference for default scroll() timeline</title> +<style> + html { + min-height: 100%; + padding-bottom: 100px; + } + + #box { + width: 100px; + height: 100px; + background-color: green; + } + + * { + margin-top: 0px; + margin-bottom: 0px; + } +</style> + +<div id="box"></div> diff --git a/testing/web-platform/tests/scroll-animations/css/printing/scroll-timeline-default-print.tentative.html b/testing/web-platform/tests/scroll-animations/css/printing/scroll-timeline-default-print.tentative.html new file mode 100644 index 0000000000..3f25cc93db --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/printing/scroll-timeline-default-print.tentative.html @@ -0,0 +1,59 @@ +<!DOCTYPE HTML> +<html class="reftest-wait"> +<title>The default scroll() timeline for print</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#scroll-notation"> +<link rel="help" href="https://drafts.csswg.org/css-animations-2/#animation-timeline"> +<meta name="assert" content="CSS animation correctly updates values when using the default scroll() timeline"> +<link rel="match" href="scroll-timeline-default-print-ref.html"> + +<style> + @keyframes update { + from { transform: translateY(0px); } + to { transform: translateY(200px); } + } + + html { + min-height: 100%; + padding-bottom: 100px; + } + + #box { + width: 100px; + height: 100px; + background-color: green; + animation: update 1s linear; + animation-timeline: scroll(); + } + + * { + margin-top: 0px; + margin-bottom: 0px; + } +</style> + +<div id="box"></div> +<script src="/web-animations/testcommon.js"></script> +<script> + document.documentElement.addEventListener('TestRendered', async () => { + runTest(); + }, { once: true }); + + async function runTest() { + const scroller = document.scrollingElement; + + await waitForCompositorReady(); + + // Move the scroller to the halfway point. + // When printing, a timeline associated with the document's scrolling + // element will become inactive. The root scroller is considered to be + // fully in view with a scroll range of zero. + // https://github.com/w3c/csswg-drafts/issues/8226 + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.5 * maxScroll; + + await waitForNextFrame(); + await waitForNextFrame(); + + document.documentElement.classList.remove("reftest-wait"); + } +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/printing/scroll-timeline-specified-scroller-print.html b/testing/web-platform/tests/scroll-animations/css/printing/scroll-timeline-specified-scroller-print.html new file mode 100644 index 0000000000..05fab3e46a --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/printing/scroll-timeline-specified-scroller-print.html @@ -0,0 +1,58 @@ +<!DOCTYPE HTML> +<html class="reftest-wait"> +<title>A scroll timeline with a specified scroller for print</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#scroll-timelines"> +<link rel="help" href="https://drafts.csswg.org/css-animations-2/#animation-timeline"> +<meta name="assert" content="CSS animation correctly updates values when using a specified scroller"> +<link rel="match" href="scroll-timeline-specified-scroller-ref.html"> + +<style> + @keyframes anim { + from { transform: translateX(0px); } + to { transform: translateX(100px); } + } + + #scroller { + scroll-timeline: timeline; + overflow: scroll; + width: 100px; + height: 100px; + scrollbar-width: none; + } + + #contents { + height: 200px; + } + + #box { + width: 100px; + height: 100px; + background-color: green; + animation: anim 1s linear; + animation-timeline: timeline; + } + + @supports not (animation-timeline:timeline) { + #box { + animation-play-state: paused; + } + } +</style> + +<div id="scroller"> + <div id="contents"></div> + <div id="box"></div> +</div> + +<script> + window.addEventListener('load', function() { + const scroller = document.getElementById("scroller"); + // Move the scroller to the halfway point. + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.5 * maxScroll; + + window.requestAnimationFrame(() => { + document.documentElement.classList.remove("reftest-wait"); + }); + }); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/printing/scroll-timeline-specified-scroller-ref.html b/testing/web-platform/tests/scroll-animations/css/printing/scroll-timeline-specified-scroller-ref.html new file mode 100644 index 0000000000..d2f2d8f73d --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/printing/scroll-timeline-specified-scroller-ref.html @@ -0,0 +1,38 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<title>Reference for scroll timeline with a specified scroller</title> +<style> + #scroller { + overflow: scroll; + width: 100px; + height: 100px; + scrollbar-width: none; + } + + #contents { + height: 200px; + } + + #box { + width: 100px; + height: 100px; + background-color: green; + transform: translateX(50px); + } +</style> + +<div id="scroller"> + <div id="contents"></div> + <div id="box"></div> +</div> + +<script> + window.addEventListener('load', function() { + const scroller = document.getElementById("scroller"); + // Move the scroller to the halfway point. + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.5 * maxScroll; + + document.documentElement.classList.remove("reftest-wait"); + }); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/progress-based-animation-animation-longhand-properties.tentative.html b/testing/web-platform/tests/scroll-animations/css/progress-based-animation-animation-longhand-properties.tentative.html new file mode 100644 index 0000000000..f4f9a669f3 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/progress-based-animation-animation-longhand-properties.tentative.html @@ -0,0 +1,255 @@ +<!DOCTYPE html> +<title>The various animation longhands with progress based animations</title> +<link rel="help" src="https://drafts.csswg.org/css-animations-2"> +<link rel="help" src="https://github.com/w3c/csswg-drafts/issues/4862"> +<link rel="help" src="https://github.com/w3c/csswg-drafts/issues/6674"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="support/testcommon.js"></script> +<style> + @keyframes anim { + from { translate: 0px; } + to { translate: 100px; } + } + #container { + width: 300px; + height: 300px; + overflow: scroll; + } + #target { + width: 100px; + height: 100px; + translate: none; + } +</style> +<body> +<div id="log"></div> +<script> +"use strict"; + +setup(assert_implements_animation_timeline); + +const createTargetAndScroller = function(t) { + let container = document.createElement('div'); + container.id = 'container'; + let target = document.createElement('div'); + target.id = 'target'; + let content = document.createElement('div'); + content.style.blockSize = '100%'; + + // The height of target is 100px and the content is 100%, so the scroll range + // is [0, 100]. + + // <div id='container'> + // <div id='target'></div> + // <div style='block-size: 100%;'></div> + // </div> + document.body.appendChild(container); + container.appendChild(target); + container.appendChild(content); + + if (t && typeof t.add_cleanup === 'function') { + t.add_cleanup(() => { + content.remove(); + target.remove(); + container.remove(); + }); + } + + return [target, container]; +}; + +async function scrollTop(element, value) { + element.scrollTop = value; + await waitForNextFrame(); +} + +// ------------------------------ +// Test animation-duration +// ------------------------------ + +promise_test(async t => { + let [target, scroller] = createTargetAndScroller(t); + target.style.animation = '10s linear anim'; + target.style.animationTimeline = 'scroll(nearest)'; + + await scrollTop(scroller, 25); // [0, 100]. + assert_equals(getComputedStyle(target).translate, '25px'); +}, 'animation-duration'); + +promise_test(async t => { + let [target, scroller] = createTargetAndScroller(t); + target.style.animation = '0s linear anim forwards'; + target.style.animationTimeline = 'scroll(nearest)'; + + await scrollTop(scroller, 25); // [0, 100]. + assert_equals(getComputedStyle(target).translate, '100px'); +}, 'animation-duration: 0s'); + + +// ------------------------------ +// Test animation-iteration-count +// ------------------------------ + +promise_test(async t => { + let [target, scroller] = createTargetAndScroller(t); + target.style.animation = '10s linear anim'; + target.style.animationTimeline = 'scroll(nearest)'; + + await scrollTop(scroller, 25); // [0, 100]. + assert_equals(getComputedStyle(target).translate, '25px'); + + // Let animation become 50% in the 1st iteration. + target.style.animationIterationCount = '2'; + await waitForCSSScrollTimelineStyle(); + assert_equals(getComputedStyle(target).translate, '50px'); + + // Let animation become 0% in the 2nd iteration. + target.style.animationIterationCount = '4'; + await waitForCSSScrollTimelineStyle(); + assert_equals(getComputedStyle(target).translate, '0px'); +}, 'animation-iteration-count'); + +promise_test(async t => { + let [target, scroller] = createTargetAndScroller(t); + target.style.animation = '10s linear anim forwards'; + target.style.animationTimeline = 'scroll(nearest)'; + target.style.animationIterationCount = '0'; + + await scrollTop(scroller, 25); // [0, 100]. + assert_equals(getComputedStyle(target).translate, '0px'); +}, 'animation-iteration-count: 0'); + +promise_test(async t => { + let [target, scroller] = createTargetAndScroller(t); + target.style.animation = '10s linear anim forwards'; + target.style.animationTimeline = 'scroll(nearest)'; + target.style.animationIterationCount = 'infinite'; + + await scrollTop(scroller, 25); // [0, 100]. + assert_equals(getComputedStyle(target).translate, '100px'); +}, 'animation-iteration-count: infinite'); + + +// ------------------------------ +// Test animation-direction +// ------------------------------ + +promise_test(async t => { + let [target, scroller] = createTargetAndScroller(t); + target.style.animation = '10s linear anim'; + target.style.animationTimeline = 'scroll(nearest)'; + + await scrollTop(scroller, 25) // [0, 100]. + assert_equals(getComputedStyle(target).translate, '25px'); +}, 'animation-direction: normal'); + +promise_test(async t => { + let [target, scroller] = createTargetAndScroller(t); + target.style.animation = '10s linear anim'; + target.style.animationTimeline = 'scroll(nearest)'; + target.style.animationDirection = 'reverse'; + + await scrollTop(scroller, 25); // 25% in the reversing direction. + assert_equals(getComputedStyle(target).translate, '75px'); +}, 'animation-direction: reverse'); + +promise_test(async t => { + let [target, scroller] = createTargetAndScroller(t); + target.style.animation = '10s linear anim'; + target.style.animationTimeline = 'scroll(nearest)'; + target.style.animationIterationCount = '2'; + target.style.animationDirection = 'alternate'; + + await scrollTop(scroller, 10); // 20% in the 1st iteration. + assert_equals(getComputedStyle(target).translate, '20px'); + + await scrollTop(scroller, 60); // 20% in the 2nd iteration (reversing direction). + assert_equals(getComputedStyle(target).translate, '80px'); +}, 'animation-direction: alternate'); + +promise_test(async t => { + let [target, scroller] = createTargetAndScroller(t); + target.style.animation = '10s linear anim'; + target.style.animationTimeline = 'scroll(nearest)'; + target.style.animationIterationCount = '2'; + target.style.animationDirection = 'alternate-reverse'; + + await scrollTop(scroller, 10); // 20% in the 1st iteration (reversing direction). + assert_equals(getComputedStyle(target).translate, '80px'); + + await scrollTop(scroller, 60); // 20% in the 2nd iteration. + assert_equals(getComputedStyle(target).translate, '20px'); +}, 'animation-direction: alternate-reverse'); + + +// ------------------------------ +// Test animation-delay +// ------------------------------ + +promise_test(async t => { + let [target, scroller] = createTargetAndScroller(t); + target.style.animation = '10s linear anim'; + target.style.animationTimeline = 'scroll(nearest)'; + + await scrollTop(scroller, 25); // [0, 100]. + assert_equals(getComputedStyle(target).translate, '25px'); + + // (start delay: 10s) (duration: 10s) + // before active + // |--------------------|--------------------| + // 0px 50px 100px (The scroller) + // 0% 100% (The iteration progress) + + // Let animation be in before phase. + target.style.animationDelay = '10s'; + target.style.animationDelayStart = '10s'; // crbug.com/1375994 + assert_equals(getComputedStyle(target).translate, 'none'); + + await scrollTop(scroller, 50); // The animation enters active phase. + assert_equals(getComputedStyle(target).translate, '0px'); + + await scrollTop(scroller, 75); // The ieration progress is 50%. + assert_equals(getComputedStyle(target).translate, '50px'); +}, 'animation-delay with a positive value'); + +promise_test(async t => { + let [target, scroller] = createTargetAndScroller(t); + target.style.animation = '10s linear anim'; + target.style.animationTimeline = 'scroll(nearest)'; + + // active + // |--------------------| + // 0px 100px (The scroller) + // 50% 100% (The iteration progress) + + await scrollTop(scroller, 20); // [0, 100]. + target.style.animationDelay = '-5s'; + target.style.animationDelayStart = '-5s'; // crbug.com/1375994 + await waitForCSSScrollTimelineStyle(); + assert_equals(getComputedStyle(target).translate, '60px'); +}, 'animation-delay with a negative value'); + + +// ------------------------------ +// Test animation-fill-mode +// ------------------------------ + +promise_test(async t => { + let [target, scroller] = createTargetAndScroller(t); + target.style.animation = '10s linear anim'; + target.style.animationTimeline = 'scroll(nearest)'; + target.style.animationDelay = '10s'; + target.style.animationDelayStart = '10s'; // crbug.com/1375994 + + await scrollTop(scroller, 25); + assert_equals(getComputedStyle(target).translate, 'none'); + + target.style.animationFillMode = 'backwards'; + await waitForCSSScrollTimelineStyle(); + assert_equals(getComputedStyle(target).translate, '0px'); +}, 'animation-fill-mode'); + +</script> +</body> diff --git a/testing/web-platform/tests/scroll-animations/css/progress-based-animation-timeline.html b/testing/web-platform/tests/scroll-animations/css/progress-based-animation-timeline.html new file mode 100644 index 0000000000..eeb1e548e5 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/progress-based-animation-timeline.html @@ -0,0 +1,56 @@ +<!DOCTYPE html> +<title>CSS Animation using progress based timeline</title> +<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1"> +<link rel="help" src="https://drafts.csswg.org/css-animations-2/#animation-timeline"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<style> + main > div { + overflow: hidden; + width: 100px; + height: 100px; + } + main > div > div { + height: 200px; + } + + @keyframes top { + from { top: 100px; } + to { top: 200px; } + } + + #scroller1 { + scroll-timeline: top_timeline; + } + + #element { + animation-name: top; + animation-duration: 10s; + animation-timing-function: linear; + animation-timeline: top_timeline; + position: absolute; + } + /* Ensure stable expectations if feature is not supported */ + @supports not (animation-timeline:foo) { + #element { animation-play-state: paused; } + } +</style> +<main> + <div id=scroller1> + <div></div> + <div id=element></div> + </div> +</main> +<script> + window.onload = async () => { + promise_test(async (t) => { + await waitForNextFrame(); + const anim = document.getAnimations()[0]; + await anim.ready; + scroller1.scrollTop = 20; + await waitForNextFrame(); + assert_equals(getComputedStyle(element).top, '120px'); + }, 'progress based animation timeline works'); + }; +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-animation-initial-offset-ref.html b/testing/web-platform/tests/scroll-animations/css/scroll-animation-initial-offset-ref.html new file mode 100644 index 0000000000..8e6907860b --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/scroll-animation-initial-offset-ref.html @@ -0,0 +1,44 @@ +<!DOCTYPE html> +<style> + +#scroller { + overflow-y: auto; + height: 200px; + border: 2px solid green; + position: relative; + background: gray; +} + +.spacer { + height: 1000px; +} + +#align { + box-sizing: border-box; + width: 100%; + height: 50px; + background: rgba(0, 0, 200, 0.2); + color: white; + position: absolute; + border: 1px solid white; + transform: translateY(200px); + will-change: transform; +} + +#marker { + width: 100%; + height: 50px; + background: #640; + position: absolute; + top: 350px; +} + +</style> +<div id="scroller"> + <div id="align">TOP</div> + <div class="spacer"></div> + <div id="marker">BOTTOM</div> +</div> +<script> + scroller.scrollTo(0, 200); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-animation-initial-offset.html b/testing/web-platform/tests/scroll-animations/css/scroll-animation-initial-offset.html new file mode 100644 index 0000000000..34ae52d479 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/scroll-animation-initial-offset.html @@ -0,0 +1,73 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<title>Composited scroll-linked animation with initial scroll offset</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/"> +<link rel="match" href="scroll-animation-initial-offset-ref.html"> +<style> + +#scroller { + overflow-y: auto; + height: 200px; + border: 2px solid green; + position: relative; + background: gray; +} + +.spacer { + height: 1000px; +} + +@keyframes anim { + 0% { transform: translateY(0); } + 100% { transform: translateY(800px); } +} + +#align { + box-sizing: border-box; + width: 100%; + height: 50px; + background: rgba(0, 0, 200, 0.2); + color: white; + position: absolute; + border: 1px solid white; + animation: anim linear 10s; + animation-timeline: scroll(); + will-change: transform; +} + +#marker { + width: 100%; + height: 50px; + background: #640; + position: absolute; + top: 350px; +} + +</style> +<div id="scroller"> + <div id="align">TOP</div> + <div class="spacer"></div> + <div id="marker">BOTTOM</div> +</div> +<script> + + // Test that a scroll-linked animation of a composited property reacts + // correctly to a programmatic scroll early during the page load. + // + // The scroll offset will change before the animation is "started" on the + // compositor, so it needs to be able to handle a non-zero initial offset. + // + scroller.scrollTo(0, 200); + +</script> +<script src="/web-animations/testcommon.js"></script> +<script> + + document.documentElement.addEventListener('TestRendered', async () => { + await waitForCompositorReady(); + await waitForNextFrame(); + await waitForNextFrame(); + document.documentElement.classList.remove("reftest-wait"); + }, { once: true }); + +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-attachment-computed-tentative.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-attachment-computed-tentative.html new file mode 100644 index 0000000000..3ec18a0eb9 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-attachment-computed-tentative.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/css/support/computed-testcommon.js"></script> +<style> + #outer { scroll-timeline-attachment: defer; } + #target { scroll-timeline-attachment: ancestor; } +</style> +<div id="outer"> + <div id="target"></div> +</div> +<script> +test_computed_value('scroll-timeline-attachment', 'initial', 'local'); +test_computed_value('scroll-timeline-attachment', 'inherit', 'defer'); +test_computed_value('scroll-timeline-attachment', 'unset', 'local'); +test_computed_value('scroll-timeline-attachment', 'revert', 'local'); +test_computed_value('scroll-timeline-attachment', 'local'); +test_computed_value('scroll-timeline-attachment', 'defer'); +test_computed_value('scroll-timeline-attachment', 'ancestor'); +test_computed_value('scroll-timeline-attachment', 'local, defer'); +test_computed_value('scroll-timeline-attachment', 'defer, ancestor'); +test_computed_value('scroll-timeline-attachment', 'local, defer, ancestor'); +test_computed_value('scroll-timeline-attachment', 'local, local, local, local'); + +test(() => { + let style = getComputedStyle(document.getElementById('target')); + assert_not_equals(Array.from(style).indexOf('scroll-timeline-attachment'), -1); +}, 'The scroll-timeline-attachment property shows up in CSSStyleDeclaration enumeration'); + +test(() => { + let style = document.getElementById('target').style; + assert_not_equals(style.cssText.indexOf('scroll-timeline-attachment'), -1); +}, 'The scroll-timeline-attachment property shows up in CSSStyleDeclaration.cssText'); + +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-attachment-parsing-tentative.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-attachment-parsing-tentative.html new file mode 100644 index 0000000000..3235292d20 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-attachment-parsing-tentative.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/css/support/parsing-testcommon.js"></script> +<div id="target"></div> + +<script> + +test_valid_value('scroll-timeline-attachment', 'initial'); +test_valid_value('scroll-timeline-attachment', 'inherit'); +test_valid_value('scroll-timeline-attachment', 'unset'); +test_valid_value('scroll-timeline-attachment', 'revert'); + +test_valid_value('scroll-timeline-attachment', 'local'); +test_valid_value('scroll-timeline-attachment', 'defer'); +test_valid_value('scroll-timeline-attachment', 'ancestor'); +test_valid_value('scroll-timeline-attachment', 'local, defer'); +test_valid_value('scroll-timeline-attachment', 'defer, ancestor'); +test_valid_value('scroll-timeline-attachment', 'local, defer, ancestor, local'); +test_valid_value('scroll-timeline-attachment', 'local, local, local, local'); + +test_invalid_value('scroll-timeline-attachment', 'abc'); +test_invalid_value('scroll-timeline-attachment', '10px'); +test_invalid_value('scroll-timeline-attachment', 'auto'); +test_invalid_value('scroll-timeline-attachment', 'none'); +test_invalid_value('scroll-timeline-attachment', 'local defer'); +test_invalid_value('scroll-timeline-attachment', 'local / defer'); + +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-attachment.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-attachment.html new file mode 100644 index 0000000000..7996e48cea --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-attachment.html @@ -0,0 +1,417 @@ +<!DOCTYPE html> +<title>Scroll Timeline Attachment</title> +<link rel="help" src="https://github.com/w3c/csswg-drafts/issues/7759"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> + +<main id=main></main> +<script> + function inflate(t, template) { + t.add_cleanup(() => main.replaceChildren()); + main.append(template.content.cloneNode(true)); + main.offsetTop; + } + + async function scrollTop(e, value) { + e.scrollTop = value; + await waitForNextFrame(); + } +</script> +<style> + @keyframes anim { + from { width: 0px; --applied:true; } + to { width: 200px; --applied:true; } + } + + .scroller { + overflow-y: hidden; + width: 200px; + height: 200px; + } + .scroller > .content { + margin: 400px 0px; + width: 100px; + height: 100px; + background-color: green; + } + .target { + background-color: coral; + width: 0px; + animation: anim auto linear; + animation-timeline: t1; + } + .timeline { + scroll-timeline-name: t1; + } + .local { + scroll-timeline-attachment: local; + } + .defer { + scroll-timeline-attachment: defer; + } + .ancestor { + scroll-timeline-attachment: ancestor; + } + +</style> + + +<!-- Basic Behavior --> + +<template id=scroll_timeline_defer> + <div class="timeline defer"> + <div class=target>Test</div> + <div class="scroller timeline ancestor"> + <div class=content></div> + </div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, scroll_timeline_defer); + let scroller = main.querySelector('.scroller'); + let target = main.querySelector('.target'); + await scrollTop(scroller, 350); // 50% + assert_equals(getComputedStyle(target).width, '100px'); // 0px => 200px, 50% + }, 'Descendant can attach to deferred timeline'); +</script> + +<template id=scroll_timeline_defer_no_attach> + <div class="timeline defer"> + <div class=target>Test</div> + <div class="scroller timeline"> + <div class=content></div> + </div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, scroll_timeline_defer_no_attach); + let scroller = main.querySelector('.scroller'); + let target = main.querySelector('.target'); + await scrollTop(scroller, 350); // 50% + assert_equals(getComputedStyle(target).width, '0px'); + assert_equals(getComputedStyle(target).getPropertyValue('--applied'), ''); + }, 'Deferred timeline with no attachments'); +</script> + +<template id=scroll_timeline_defer_no_attach_to_prev_sibling> + <div class="timeline defer"> + <div class="scroller timeline"> + <div class=content></div> + </div> + <div class=target>Test</div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, scroll_timeline_defer_no_attach_to_prev_sibling); + let scroller = main.querySelector('.scroller'); + let target = main.querySelector('.target'); + await scrollTop(scroller, 350); // 50% + assert_equals(getComputedStyle(target).width, '0px'); + assert_equals(getComputedStyle(target).getPropertyValue('--applied'), ''); + }, 'Deferred timeline with no attachments to previous sibling'); +</script> + +<template id=scroll_timeline_local_ancestor> + <div class="scroller timeline local"> + <div class=content> + <div class=target>Test</div> + <div class="scroller timeline ancestor"> + <div class=content></div> + </div> + </div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, scroll_timeline_local_ancestor); + let scroller = main.querySelector('.scroller'); + let target = main.querySelector('.target'); + await scrollTop(scroller, 350); // 50% + assert_equals(getComputedStyle(target).width, '100px'); // 0px => 200px, 50% + }, 'Timeline with ancestor attachment does not attach to local'); +</script> + +<template id=scroll_timeline_defer_two_attachments> + <div class="timeline defer"> + <div class=target>Test</div> + <div class="scroller timeline ancestor"> + <div class=content></div> + </div> + <!-- Extra attachment --> + <div class="timeline ancestor"></div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, scroll_timeline_defer_two_attachments); + let scroller = main.querySelector('.scroller'); + let target = main.querySelector('.target'); + await scrollTop(scroller, 350); // 50% + assert_equals(getComputedStyle(target).width, '0px'); + assert_equals(getComputedStyle(target).getPropertyValue('--applied'), ''); + }, 'Deferred timeline with two attachments'); +</script> + +<!-- Effective Axis of ScrollTimeline --> + +<template id=scroll_timeline_defer_axis> + <div class="timeline defer" style="scroll-timeline-axis:inline"> + <div class=target>Test</div> + <div class="scroller timeline ancestor" style="scroll-timeline-axis:vertical"> + <div class=content></div> + </div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, scroll_timeline_defer_axis); + let target = main.querySelector('.target'); + assert_equals(target.getAnimations().length, 1); + let anim = target.getAnimations()[0]; + assert_not_equals(anim.timeline, null); + assert_equals(anim.timeline.axis, 'vertical'); + }, 'Axis of deferred timeline is taken from attached timeline'); +</script> + + +<template id=scroll_timeline_defer_axis_multiple> + <div class="timeline defer" style="scroll-timeline-axis:inline"> + <div class=target>Test</div> + <div class="scroller timeline ancestor" style="scroll-timeline-axis:vertical"> + <div class=content></div> + </div> + <!-- Extra attachment --> + <div class="timeline ancestor"></div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, scroll_timeline_defer_axis_multiple); + let target = main.querySelector('.target'); + assert_equals(target.getAnimations().length, 1); + let anim = target.getAnimations()[0]; + assert_not_equals(anim.timeline, null); + assert_equals(anim.timeline.axis, 'block'); + }, 'Axis of deferred timeline with multiple attachments'); +</script> + + +<!-- Dynamic Reattachment --> + + +<template id=scroll_timeline_reattach> + <div class="timeline defer"> + <div class=target>Test</div> + <div class="scroller timeline ancestor"> + <div class=content></div> + </div> + <div class="scroller timeline"> + <div class=content></div> + </div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, scroll_timeline_reattach); + let scrollers = main.querySelectorAll('.scroller'); + assert_equals(scrollers.length, 2); + let target = main.querySelector('.target'); + await scrollTop(scrollers[0], 350); // 50% + await scrollTop(scrollers[1], 175); // 25% + + // Attached to scrollers[0]. + assert_equals(getComputedStyle(target).width, '100px'); // 0px => 200px, 50% + + // Reattach to scrollers[1]. + scrollers[0].classList.remove('ancestor'); + scrollers[1].classList.add('ancestor'); + + await waitForNextFrame(); + assert_equals(getComputedStyle(target).width, '50px'); // 0px => 200px, 25% + }, 'Dynamically re-attaching'); +</script> + + +<template id=scroll_timeline_dynamic_attach_second> + <div class="timeline defer"> + <div class=target>Test</div> + <div class="scroller timeline"> + <div class=content></div> + </div> + <div class="scroller timeline"> + <div class=content></div> + </div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, scroll_timeline_dynamic_attach_second); + let scrollers = main.querySelectorAll('.scroller'); + assert_equals(scrollers.length, 2); + let target = main.querySelector('.target'); + await scrollTop(scrollers[0], 350); // 50% + await scrollTop(scrollers[1], 175); // 25% + + // Attached to no timelines initially: + assert_equals(getComputedStyle(target).width, '0px'); + assert_equals(getComputedStyle(target).getPropertyValue('--applied'), ''); + + // Attach to scrollers[0]. + scrollers[0].classList.add('ancestor'); + await waitForNextFrame(); + assert_equals(getComputedStyle(target).width, '100px'); // 0px => 200px, 50% + + // Also attach scrollers[1]. + scrollers[1].classList.add('ancestor'); + + await waitForNextFrame(); + assert_equals(getComputedStyle(target).width, '0px'); + assert_equals(getComputedStyle(target).getPropertyValue('--applied'), ''); + }, 'Dynamically attaching'); +</script> + + +<template id=scroll_timeline_dynamic_detach_second> + <div class="timeline defer"> + <div class=target>Test</div> + <div class="scroller timeline ancestor"> + <div class=content></div> + </div> + <div class="scroller timeline ancestor"> + <div class=content></div> + </div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, scroll_timeline_dynamic_detach_second); + let scrollers = main.querySelectorAll('.scroller'); + assert_equals(scrollers.length, 2); + let target = main.querySelector('.target'); + await scrollTop(scrollers[0], 350); // 50% + await scrollTop(scrollers[1], 175); // 25% + + // Attached to two timelines initially: + assert_equals(getComputedStyle(target).width, '0px'); + assert_equals(getComputedStyle(target).getPropertyValue('--applied'), ''); + + // Detach scrollers[1]. + scrollers[1].classList.remove('ancestor'); + + await waitForNextFrame(); + assert_equals(getComputedStyle(target).width, '100px'); // 0px => 200px, 50% + + // Also detach scrollers[0]. + scrollers[0].classList.remove('ancestor'); + + await waitForNextFrame(); + assert_equals(getComputedStyle(target).width, '0px'); + assert_equals(getComputedStyle(target).getPropertyValue('--applied'), ''); + }, 'Dynamically detaching'); +</script> + +<template id=scroll_timeline_ancestor_attached_removed> + <div class="timeline defer"> + <div class=target>Test</div> + <div class="scroller timeline ancestor"> + <div class=content></div> + </div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, scroll_timeline_ancestor_attached_removed); + let scroller = main.querySelector('.scroller'); + let target = main.querySelector('.target'); + await scrollTop(scroller, 350); // 50% + assert_equals(getComputedStyle(target).width, '100px'); // 0px => 200px, 50% + + let scroller_parent = scroller.parentElement; + scroller.remove(); + await waitForNextFrame(); + assert_equals(getComputedStyle(target).width, '0px'); + assert_equals(getComputedStyle(target).getPropertyValue('--applied'), ''); + + scroller_parent.append(scroller); + await scrollTop(scroller, 350); // 50% + assert_equals(getComputedStyle(target).width, '100px'); // 0px => 200px, 50% + }, 'Removing/inserting ancestor attached element'); +</script> + +<template id=scroll_timeline_ancestor_attached_display_none> + <div class="timeline defer"> + <div class=target>Test</div> + <div class="scroller timeline ancestor"> + <div class=content></div> + </div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, scroll_timeline_ancestor_attached_display_none); + let scroller = main.querySelector('.scroller'); + let target = main.querySelector('.target'); + await scrollTop(scroller, 350); // 50% + assert_equals(getComputedStyle(target).width, '100px'); // 0px => 200px, 50% + + scroller.style.display = 'none'; + await waitForNextFrame(); + assert_equals(getComputedStyle(target).width, '0px'); + assert_equals(getComputedStyle(target).getPropertyValue('--applied'), ''); + + scroller.style.display = 'block'; + await scrollTop(scroller, 350); // 50% + assert_equals(getComputedStyle(target).width, '100px'); // 0px => 200px, 50% + }, 'Ancestor attached element becoming display:none/block'); +</script> + +<template id=scroll_timeline_dynamic_defer> + <style> + .inner-scroller { + overflow-y: hidden; + width: 50px; + height: 50px; + } + .inner-scroller > .content { + margin: 100px 0px; + width: 20px; + height: 20px; + background-color: red; + } + </style> + <div class="scroller timeline"> + <div class="target content"> + <div class="inner-scroller timeline ancestor"> + <div class=content></div> + </div> + </div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, scroll_timeline_dynamic_defer); + let target = main.querySelector('.target'); + let outer_scroller = main.querySelector('.scroller'); + let inner_scroller = main.querySelector('.inner-scroller'); + + await scrollTop(outer_scroller, 350); // 50% + await scrollTop(inner_scroller, 17); // 10% + + // Attached to outer_scroller (local). + assert_equals(getComputedStyle(target).width, '100px'); // 0px => 200px, 50% + + // Effectively attached to inner_scroller. + outer_scroller.classList.add('defer'); + await waitForNextFrame(); + assert_equals(getComputedStyle(target).width, '20px'); // 0px => 200px, 10% + + // Attached to outer_scroller again. + outer_scroller.classList.remove('defer'); + await waitForNextFrame(); + assert_equals(getComputedStyle(target).width, '100px'); // 0px => 200px, 50% + }, 'Dynamically becoming a deferred timeline'); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-axis-computed.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-axis-computed.html new file mode 100644 index 0000000000..b971aba6c0 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-axis-computed.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#scroll-timeline-axis"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/css/support/computed-testcommon.js"></script> +<style> + #outer { scroll-timeline-axis: inline; } + #target { scroll-timeline-axis: vertical; } +</style> +<div id="outer"> + <div id="target"></div> +</div> +<script> +test_computed_value('scroll-timeline-axis', 'initial', 'block'); +test_computed_value('scroll-timeline-axis', 'inherit', 'inline'); +test_computed_value('scroll-timeline-axis', 'unset', 'block'); +test_computed_value('scroll-timeline-axis', 'revert', 'block'); +test_computed_value('scroll-timeline-axis', 'block'); +test_computed_value('scroll-timeline-axis', 'inline'); +test_computed_value('scroll-timeline-axis', 'vertical'); +test_computed_value('scroll-timeline-axis', 'horizontal'); +test_computed_value('scroll-timeline-axis', 'block, inline'); +test_computed_value('scroll-timeline-axis', 'inline, block'); +test_computed_value('scroll-timeline-axis', 'block, vertical, horizontal, inline'); +test_computed_value('scroll-timeline-axis', 'inline, inline, inline, inline'); + +test(() => { + let style = getComputedStyle(document.getElementById('target')); + assert_not_equals(Array.from(style).indexOf('scroll-timeline-axis'), -1); +}, 'The scroll-timeline-axis property shows up in CSSStyleDeclaration enumeration'); + +test(() => { + let style = document.getElementById('target').style; + assert_not_equals(style.cssText.indexOf('scroll-timeline-axis'), -1); +}, 'The scroll-timeline-axis property shows up in CSSStyleDeclaration.cssText'); + +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-axis-parsing.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-axis-parsing.html new file mode 100644 index 0000000000..25f48f0c70 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-axis-parsing.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#scroll-timeline-axis"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/css/support/parsing-testcommon.js"></script> +<div id="target"></div> + +<script> + +test_valid_value('scroll-timeline-axis', 'initial'); +test_valid_value('scroll-timeline-axis', 'inherit'); +test_valid_value('scroll-timeline-axis', 'unset'); +test_valid_value('scroll-timeline-axis', 'revert'); + +test_valid_value('scroll-timeline-axis', 'block'); +test_valid_value('scroll-timeline-axis', 'inline'); +test_valid_value('scroll-timeline-axis', 'vertical'); +test_valid_value('scroll-timeline-axis', 'horizontal'); +test_valid_value('scroll-timeline-axis', 'block, inline'); +test_valid_value('scroll-timeline-axis', 'inline, block'); +test_valid_value('scroll-timeline-axis', 'block, vertical, horizontal, inline'); +test_valid_value('scroll-timeline-axis', 'inline, inline, inline, inline'); + +test_invalid_value('scroll-timeline-axis', 'abc'); +test_invalid_value('scroll-timeline-axis', '10px'); +test_invalid_value('scroll-timeline-axis', 'auto'); +test_invalid_value('scroll-timeline-axis', 'none'); +test_invalid_value('scroll-timeline-axis', 'block inline'); +test_invalid_value('scroll-timeline-axis', 'block / inline'); + +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-axis-writing-mode.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-axis-writing-mode.html new file mode 100644 index 0000000000..958ce4964e --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-axis-writing-mode.html @@ -0,0 +1,139 @@ +<!DOCTYPE html> +<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1"> +<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#scroll-timeline-axis"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<style> + .scroller { + overflow: hidden; + width: 100px; + height: 100px; + } + .contents { + height: 200px; + width: 200px; + } + @keyframes expand { + from { width: 100px; } + to { width: 200px; } + } + #timeline_initial_axis { + scroll-timeline: timeline_initial_axis; + } + #timeline_vertical { + scroll-timeline: timeline_vertical vertical; + } + #timeline_horizontal { + scroll-timeline: timeline_horizontal horizontal; + } + #timeline_block_in_horizontal { + scroll-timeline: timeline_block_in_horizontal block; + } + #timeline_inline_in_horizontal { + scroll-timeline: timeline_inline_in_horizontal inline; + } + #timeline_block_in_vertical { + scroll-timeline: timeline_block_in_vertical block; + writing-mode: vertical-lr; + } + #timeline_inline_in_vertical { + scroll-timeline: timeline_inline_in_vertical inline; + writing-mode: vertical-lr; + } + .target { + width: 0px; + animation-name: expand; + animation-duration: 10s; + animation-timing-function: linear; + position: absolute; + } + /* Ensure stable expectations if feature is not supported */ + @supports not (animation-timeline:foo) { + .target { animation-play-state: paused; } + } + #element_initial_axis { animation-timeline: timeline_initial_axis; } + #element_vertical { animation-timeline: timeline_vertical; } + #element_horizontal { animation-timeline: timeline_horizontal; } + #element_block_in_horizontal { animation-timeline: timeline_block_in_horizontal; } + #element_inline_in_horizontal { animation-timeline: timeline_inline_in_horizontal; } + #element_block_in_vertical { animation-timeline: timeline_block_in_vertical; } + #element_inline_in_vertical { animation-timeline: timeline_inline_in_vertical; } +</style> +<div class=scroller id=timeline_initial_axis> + <div class=contents></div> + <div class=target id=element_initial_axis></div> +</div> +<div class=scroller id=timeline_vertical> + <div class=contents></div> + <div class=target id=element_vertical></div> +</div> +<div class=scroller id=timeline_horizontal> + <div class=contents></div> + <div class=target id=element_horizontal></div> +</div> +<div class=scroller id=timeline_block_in_horizontal> + <div class=contents></div> + <div class=target id=element_block_in_horizontal></div> +</div> +<div class=scroller id=timeline_inline_in_horizontal> + <div class=contents></div> + <div class=target id=element_inline_in_horizontal></div> +</div> +<div class=scroller id=timeline_block_in_vertical> + <div class=contents></div> + <div class=target id=element_block_in_vertical></div> +</div> +<div class=scroller id=timeline_inline_in_vertical> + <div class=contents></div> + <div class=target id=element_inline_in_vertical></div> +</div> +<script> + // Animations linked to vertical scroll-timelines are at 75% progress. + timeline_initial_axis.scrollTop = 75; + timeline_vertical.scrollTop = 75; + timeline_block_in_horizontal.scrollTop = 75; + timeline_inline_in_vertical.scrollTop = 75; + // Animations linked to horizontal scroll-timelines are at 25% progress. + timeline_horizontal.scrollLeft = 25; + timeline_block_in_vertical.scrollLeft = 25; + timeline_inline_in_horizontal.scrollLeft = 25; + + promise_test(async (t) => { + await waitForNextFrame(); + assert_equals(getComputedStyle(element_initial_axis).width, '175px'); + }, 'Initial axis'); + + promise_test(async (t) => { + await waitForNextFrame(); + assert_equals(getComputedStyle(element_vertical).width, '175px'); + }, 'Vertical axis'); + + promise_test(async (t) => { + await waitForNextFrame(); + assert_equals(getComputedStyle(element_horizontal).width, '125px'); + }, 'Horizontal axis'); + + promise_test(async (t) => { + await waitForNextFrame(); + assert_equals(getComputedStyle(element_block_in_horizontal).width, '175px'); + }, 'Block axis in horizontal writing-mode'); + + promise_test(async (t) => { + await waitForNextFrame(); + assert_equals(getComputedStyle(element_inline_in_horizontal).width, '125px'); + }, 'Inline axis in horizontal writing-mode'); + + promise_test(async (t) => { + await waitForNextFrame(); + assert_equals(getComputedStyle(timeline_block_in_vertical).writingMode, 'vertical-lr'); + assert_equals(getComputedStyle(element_block_in_vertical).width, '125px'); + }, 'Block axis in vertical writing-mode'); + + promise_test(async (t) => { + await waitForNextFrame(); + assert_equals(getComputedStyle(timeline_inline_in_vertical).writingMode, 'vertical-lr'); + assert_equals(getComputedStyle(element_inline_in_vertical).width, '175px'); + }, 'Inline axis in vertical writing-mode'); + +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-default-iframe-ref.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-default-iframe-ref.html new file mode 100644 index 0000000000..1ab5646c8b --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-default-iframe-ref.html @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<title>Reference for default scroll() timeline</title> +<iframe width="400" height="400" srcdoc=' + <html> + <style> + html { + min-height: 100%; + padding-bottom: 100px; + } + + #box { + width: 100px; + height: 100px; + background-color: green; + transform: translateY(100px); + } + + * { + margin-top: 0px; + margin-bottom: 0px; + } + </style> + <script> + window.addEventListener("load", function() { + // Move the scroller to halfway. + const scroller = document.scrollingElement; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.5 * maxScroll; + }); + </script> + <div id="box"></div> + </html> +'></iframe> diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-default-iframe.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-default-iframe.html new file mode 100644 index 0000000000..dbcf5941a8 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-default-iframe.html @@ -0,0 +1,73 @@ +<!DOCTYPE HTML> +<html class="reftest-wait"> +<title>The default scroll() timeline in the iframe</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#scroll-notation"> +<link rel="help" href="https://drafts.csswg.org/css-animations-2/#animation-timeline"> +<meta name="assert" content="CSS animation correctly updates values when using the default scroll() timeline"> +<link rel="match" href="scroll-timeline-default-iframe-ref.html"> + +<iframe id="target" width="400" height="400" srcdoc=' + <html> + <style> + @keyframes update { + from { transform: translateY(0px); } + to { transform: translateY(200px); } + } + html { + min-height: 100%; + padding-bottom: 100px; + } + #box { + width: 100px; + height: 100px; + background-color: green; + animation: update 1s linear; + animation-timeline: scroll(); + } + #covered { + width: 100px; + height: 100px; + background-color: red; + } + + * { + margin-top: 0px; + margin-bottom: 0px; + } + </style> + <script src="/web-animations/testcommon.js"></script> + <script> + window.addEventListener("load", async function() { + const scroller = document.scrollingElement; + + // Move the scroller to the halfway point. + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.5 * maxScroll; + + await waitForCompositorReady(); + await waitForNextFrame(); + await waitForNextFrame(); + + window.parent.postMessage("success", "*"); + }); + </script> + <body> + <div id="box"></div> + <div id="covered"></div> + </body> + </html> +'></iframe> +<script src="/web-animations/testcommon.js"></script> +<script> + async function finishTest() { + await waitForCompositorReady(); + await waitForNextFrame(); + await waitForNextFrame(); + document.documentElement.classList.remove("reftest-wait"); + } + window.addEventListener("message", event => { + if (event.data == "success") { + finishTest(); + } + }, false); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-default-quirks-mode.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-default-quirks-mode.html new file mode 100644 index 0000000000..d2c28d86b6 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-default-quirks-mode.html @@ -0,0 +1,63 @@ +<html class="reftest-wait"> +<title>The default scroll() timeline in quirks mode</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#scroll-notation"> +<link rel="help" href="https://drafts.csswg.org/css-animations-2/#animation-timeline"> +<meta name="assert" content="CSS animation correctly updates values when using the default scroll() timeline"> +<link rel="match" href="scroll-timeline-default-ref.html"> + +<style> + @keyframes update { + from { transform: translateY(0px); } + to { transform: translateY(200px); } + } + + html { + min-height: 100%; + padding-bottom: 100px; + } + + #box { + width: 100px; + height: 100px; + background-color: green; + animation: update 1s linear; + animation-timeline: scroll(); + } + + #covered { + width: 100px; + height: 100px; + background-color: red; + } + + * { + margin-top: 0px; + margin-bottom: 0px; + } +</style> + +<div id="box"></div> +<div id="covered"></div> +<script src="/web-animations/testcommon.js"></script> +<script> +document.documentElement.addEventListener('TestRendered', async () => { + runTest(); +}, { once: true }); + +async function runTest() { + const scroller = document.scrollingElement; + + await waitForCompositorReady(); + + // Move the scroller to the halfway point. Then advance to the next frame + // to pick up the new timeline time. + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.5 * maxScroll; + + await waitForNextFrame(); + await waitForNextFrame(); + + document.documentElement.classList.remove("reftest-wait"); +} + +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-default-ref.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-default-ref.html new file mode 100644 index 0000000000..cb3b60e4bd --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-default-ref.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<title>Reference for default scroll() timeline</title> +<style> + html { + min-height: 100%; + padding-bottom: 100px; + } + + #box { + width: 100px; + height: 100px; + background-color: green; + transform: translateY(100px); + } + + * { + margin-top: 0px; + margin-bottom: 0px; + } +</style> + +<div id="box"></div> + +<script> + window.addEventListener('load', function() { + // Move the scroller to halfway. + const scroller = document.scrollingElement; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.5 * maxScroll; + }); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-default-writing-mode-rl-ref.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-default-writing-mode-rl-ref.html new file mode 100644 index 0000000000..3c072829e6 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-default-writing-mode-rl-ref.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<title>Reference for default scroll() timeline with vertical-rl</title> +<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1"> +<style> + html { + min-block-size: 100%; + padding-block-end: 100px; + writing-mode: vertical-rl + } + + #box { + width: 100px; + height: 100px; + background-color: green; + transform: translateX(-100px); + } + + * { + margin-block: 0px; + } +</style> + +<div id="box"></div> + +<script> + window.addEventListener('load', function() { + // Move the scroller to halfway. + const scroller = document.scrollingElement; + const maxScroll = scroller.scrollWidth - scroller.clientWidth; + scroller.scrollLeft = -0.5 * maxScroll; + }); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-default-writing-mode-rl.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-default-writing-mode-rl.html new file mode 100644 index 0000000000..27e6ec196b --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-default-writing-mode-rl.html @@ -0,0 +1,65 @@ +<!DOCTYPE HTML> +<html class="reftest-wait"> +<title>The default scroll() timeline with writing-mode:vertical-rl</title> +<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1"> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#scroll-notation"> +<link rel="help" href="https://drafts.csswg.org/css-animations-2/#animation-timeline"> +<meta name="assert" content="CSS animation correctly updates values when using + the default scroll() timeline with writing-mode:vertical-rl"> +<link rel="match" href="scroll-timeline-default-writing-mode-rl-ref.html"> + +<style> + @keyframes update { + from { transform: translateX(0px); } + to { transform: translateX(-200px); } + } + + html { + min-block-size: 100%; + padding-block-end: 100px; + writing-mode: vertical-rl; + } + + #box { + width: 100px; + height: 100px; + background-color: green; + animation: update 1s linear; + animation-timeline: scroll(); + } + + #covered { + width: 100px; + height: 100px; + background-color: red; + } + + * { + margin-block: 0px; + } +</style> + +<div id="box"></div> +<div id="covered"></div> + +<script src="/web-animations/testcommon.js"></script> +<script> + document.documentElement.addEventListener('TestRendered', async () => { + runTest(); + }, { once: true }); + + async function runTest() { + const scroller = document.scrollingElement; + + await waitForCompositorReady(); + + // Move the scroller to the halfway point. + const maxScroll = scroller.scrollWidth - scroller.clientWidth; + scroller.scrollLeft = -0.5 * maxScroll; + + await waitForNextFrame(); + await waitForNextFrame(); + + document.documentElement.classList.remove("reftest-wait"); + } +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-default.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-default.html new file mode 100644 index 0000000000..07eda33fd0 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-default.html @@ -0,0 +1,63 @@ +<!DOCTYPE HTML> +<html class="reftest-wait"> +<title>The default scroll() timeline</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#scroll-notation"> +<link rel="help" href="https://drafts.csswg.org/css-animations-2/#animation-timeline"> +<meta name="assert" content="CSS animation correctly updates values when using the default scroll() timeline"> +<link rel="match" href="scroll-timeline-default-ref.html"> + +<style> + @keyframes update { + from { transform: translateY(0px); } + to { transform: translateY(200px); } + } + + html { + min-height: 100%; + padding-bottom: 100px; + } + + #box { + width: 100px; + height: 100px; + background-color: green; + animation: update 1s linear; + animation-timeline: scroll(); + } + + #covered { + width: 100px; + height: 100px; + background-color: red; + } + + * { + margin-top: 0px; + margin-bottom: 0px; + } +</style> + +<div id="box"></div> +<div id="covered"></div> + +<script src="/web-animations/testcommon.js"></script> +<script> + document.documentElement.addEventListener('TestRendered', async () => { + runTest(); + }, { once: true }); + + async function runTest() { + const scroller = document.scrollingElement; + + await waitForCompositorReady(); + + // Move the scroller to the halfway point. + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.5 * maxScroll; + + await waitForNextFrame(); + await waitForNextFrame(); + + document.documentElement.classList.remove("reftest-wait"); + } +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-document-scroller-quirks.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-document-scroller-quirks.html new file mode 100644 index 0000000000..809a658a15 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-document-scroller-quirks.html @@ -0,0 +1,36 @@ +<!-- Quirks mode --> +<title>Tests the document scroller in quirks mode</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#scroll-notation"> +<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=1180575"> +<link rel="author" href="mailto:andruud@chromium.org"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/scroll-animations/scroll-timelines/testcommon.js"></script> +<script src="/css/css-animations/support/testcommon.js"></script> +<script src="support/testcommon.js"></script> +<style> + @keyframes anim { + from { z-index: 100; } + to { z-index: 100; } + } + #element { + animation: anim forwards; + animation-timeline: scroll(root); + } + #spacer { + height: 200vh; + } +</style> +<div id=element></div> +<div id=spacer></div> + +<script> +'use strict'; + +setup(assert_implements_animation_timeline); + +promise_test(async () => { + await waitForCSSScrollTimelineStyle(); + assert_equals(getComputedStyle(element).zIndex, "100"); +}); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-dynamic.tentative.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-dynamic.tentative.html new file mode 100644 index 0000000000..744639f663 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-dynamic.tentative.html @@ -0,0 +1,271 @@ +<!DOCTYPE html> +<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1"> +<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#scroll-timelines"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="support/testcommon.js"></script> +<style> + main { + scroll-timeline-attachment: defer; + scroll-timeline-name: timeline; + } + + .scroller { + scroll-timeline-attachment: ancestor; + } + + main > div { + overflow: hidden; + width: 100px; + height: 100px; + } + main > div > div { + height: 200px; + } + @keyframes expand { + from { width: 100px; } + to { width: 200px; } + } + #element { + width: 0px; + height: 20px; + animation-name: expand; + /* Some of the tests in this file assume animations attached to the + DocumentTimeline are "stopped" without actually being paused. + Using 600s + steps(10, end) achieves this for one minute.*/ + animation-duration: 600s; + animation-timing-function: steps(10, end); + } +</style> +<main id=main> + <div id=scroller1 class=scroller> + <div></div> + </div> + <div id=scroller2 class=scroller> + <div></div> + </div> + <div id=container></div> +</main> +<script> + // Force layout of scrollers. + scroller1.offsetTop; + scroller2.offsetTop; + + // Note the steps(10, end) timing function and height:100px. (10px scroll + // resolution). + scroller1.scrollTop = 20; + scroller2.scrollTop = 40; + + function insertElement() { + let element = document.createElement('div'); + element.id = 'element'; + container.append(element); + return element; + } + + // Runs a test with dynamically added/removed elements or CSS rules. + // Each test is instantiated twice: once for the initial style resolve where + // the DOM change was effectuated, and once after scrolling. + function dynamic_rule_test(func, description) { + // assert_width is an async function which verifies that the computed value + // of 'width' is as expected. + const instantiate = (assert_width, description) => { + promise_test(async (t) => { + try { + await func(t, assert_width); + } finally { + while (container.firstChild) + container.firstChild.remove(); + main.style = ''; + scroller1.style = ''; + scroller2.style = ''; + } + }, description); + }; + + // Verify that the computed style is as expected after a full frame update + // following the rule change took place. + instantiate(async (element, expected) => { + await waitForCSSScrollTimelineStyle(); + assert_equals(getComputedStyle(element).width, expected); + }, description + ' [immediate]'); + + // Verify that the computed style after scrolling a bit. + instantiate(async (element, expected) => { + scroller1.scrollTop = scroller1.scrollTop + 10; + scroller2.scrollTop = scroller2.scrollTop + 10; + await waitForNextFrame(); + scroller1.scrollTop = scroller1.scrollTop - 10; + scroller2.scrollTop = scroller2.scrollTop - 10; + await waitForNextFrame(); + assert_equals(getComputedStyle(element).width, expected); + }, description + ' [scroll]'); + } + + dynamic_rule_test(async (t, assert_width) => { + let element = insertElement(); + + // This element initially has a DocumentTimeline. + await assert_width(element, '100px'); + + // Switch to scroll timeline. + scroller1.style.scrollTimelineName = 'timeline'; + element.style.animationTimeline = 'timeline'; + await assert_width(element, '120px'); + + // Switching from ScrollTimeline -> DocumentTimeline should preserve + // current time. + scroller1.style = ''; + element.style = ''; + await assert_width(element, '120px'); + }, 'Switching between document and scroll timelines'); + + dynamic_rule_test(async (t, assert_width) => { + let element = insertElement(); + + // Flush style and create the animation with play pending. + getComputedStyle(element).animation; + + let anim = element.getAnimations()[0]; + assert_true(anim.pending, "The animation is in play pending"); + + // Switch to scroll timeline for a pending animation. + scroller1.style.scrollTimelineName = 'timeline'; + element.style.animationTimeline = 'timeline'; + + await anim.ready; + assert_false(anim.pending, "The animation is not pending"); + + await assert_width(element, '120px'); + }, 'Switching pending animation from document to scroll timelines'); + + dynamic_rule_test(async (t, assert_width) => { + let element = insertElement(); + + // Note: #scroller1 is at 20%, and #scroller2 is at 40%. + scroller1.style.scrollTimelineName = 'timeline1'; + scroller2.style.scrollTimelineName = 'timeline2'; + main.style.scrollTimelineName = "timeline1, timeline2"; + + await assert_width(element, '100px'); + + element.style.animationTimeline = 'timeline1'; + await assert_width(element, '120px'); + + element.style.animationTimeline = 'timeline2'; + await assert_width(element, '140px'); + + element.style.animationTimeline = 'timeline1'; + await assert_width(element, '120px'); + + // Switching from ScrollTimeline -> DocumentTimeline should preserve + // current time. + element.style.animationTimeline = ''; + await assert_width(element, '120px'); + + }, 'Changing computed value of animation-timeline changes effective timeline'); + + dynamic_rule_test(async (t, assert_width) => { + let element = insertElement(); + + scroller1.style.scrollTimelineName = 'timeline'; + + // DocumentTimeline applies by default. + await assert_width(element, '100px'); + + // Wait for the animation to be ready so that we a start time and no hold + // time. + await element.getAnimations()[0].ready; + + // DocumentTimeline -> none + element.style.animationTimeline = 'none'; + await assert_width(element, '0px'); + + // none -> DocumentTimeline + element.style.animationTimeline = ''; + await assert_width(element, '100px'); + + // DocumentTimeline -> ScrollTimeline + element.style.animationTimeline = 'timeline'; + await assert_width(element, '120px'); + + // ScrollTimeline -> none + element.style.animationTimeline = 'none'; + await assert_width(element, '120px'); + + // none -> ScrollTimeline + element.style.animationTimeline = 'timeline'; + await assert_width(element, '120px'); + }, 'Changing to/from animation-timeline:none'); + + + dynamic_rule_test(async (t, assert_width) => { + let element = insertElement(); + + element.style.animationDirection = 'reverse'; + element.style.animationTimeline = 'timeline'; + + // Inactive animation-timeline. Animation is inactive. + await assert_width(element, '0px'); + + // Note: #scroller1 is at 20%. + scroller1.style.scrollTimelineName = 'timeline'; + await assert_width(element, '180px'); + + // Note: #scroller2 is at 40%. + scroller1.style.scrollTimelineName = ''; + scroller2.style.scrollTimelineName = 'timeline'; + await assert_width(element, '160px'); + + element.style.animationDirection = ''; + await assert_width(element, '140px'); + }, 'Reverse animation direction'); + + dynamic_rule_test(async (t, assert_width) => { + let element = insertElement(); + element.style.animationTimeline = 'timeline'; + + // Inactive animation-timeline. Animation effect is inactive. + await assert_width(element, '0px'); + + // Note: #scroller1 is at 20%. + scroller1.style.scrollTimelineName = 'timeline'; + await assert_width(element, '120px'); + + element.style.animationPlayState = 'paused'; + + // We should still be at the same position after pausing. + await assert_width(element, '120px'); + + // Note: #scroller2 is at 40%. + scroller1.style.scrollTimelineName = ''; + scroller2.style.scrollTimelineName = 'timeline'; + + // Should be at the same position until we unpause. + await assert_width(element, '120px'); + + // Unpausing should synchronize to the scroll position. + element.style.animationPlayState = ''; + await assert_width(element, '140px'); + }, 'Change to timeline attachment while paused'); + + dynamic_rule_test(async (t, assert_width) => { + let element = insertElement(); + + // Note: #scroller1 is at 20%. + scroller1.style.scrollTimelineName = 'timeline'; + + await assert_width(element, '100px'); + + element.style.animationTimeline = 'timeline'; + element.style.animationPlayState = 'paused'; + + // Pausing should happen before the timeline is modified. (Tentative). + // https://github.com/w3c/csswg-drafts/issues/5653 + await assert_width(element, '100px'); + + element.style.animationPlayState = 'running'; + await assert_width(element, '120px'); + }, 'Switching timelines and pausing at the same time'); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-frame-size-changed-ref.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-frame-size-changed-ref.html new file mode 100644 index 0000000000..ea7628ac72 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-frame-size-changed-ref.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<title>Reference for the default scroll() timeline</title> +<style> + html { + min-height: 100%; + padding-bottom: 50px; + } + + #box { + width: 100px; + height: 100px; + background-color: green; + transform: translateY(100px); + } + + * { + margin-top: 0px; + margin-bottom: 0px; + } +</style> + +<div id="box"></div> + +<script> + window.addEventListener('load', function() { + // Move the scroller to halfway. + const scroller = document.scrollingElement; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.5 * maxScroll; + }); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-frame-size-changed.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-frame-size-changed.html new file mode 100644 index 0000000000..fb0eb8aa17 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-frame-size-changed.html @@ -0,0 +1,67 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<title>The default scroll() timeline when the frame size changed</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#scroll-notation"> +<link rel="help" href="https://drafts.csswg.org/css-animations-2/#animation-timeline"> +<meta name="assert" content="CSS animation correctly updates values when using + the default scroll() timeline and update the + frame size"> +<link rel="match" href="scroll-timeline-frame-size-changed-ref.html"> + +<style> + @keyframes update { + from { transform: translateY(0px); } + to { transform: translateY(200px); } + } + + html { + min-height: 100%; + padding-bottom: 100px; + } + + #box { + width: 100px; + height: 100px; + background-color: green; + animation: update 1s linear; + animation-timeline: scroll(); + } + + #covered { + width: 100px; + height: 100px; + background-color: red; + } + + * { + margin-top: 0px; + margin-bottom: 0px; + } +</style> + +<div id="box"></div> +<div id="covered"></div> + +<script src="/web-animations/testcommon.js"></script> +<script> + document.documentElement.addEventListener('TestRendered', async () => { + runTest(); + }, { once: true }); + + async function runTest() { + const scroller = document.scrollingElement; + + await waitForCompositorReady(); + + // Move the scroller to the 25% point. + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.25 * maxScroll; + await waitForNextFrame(); + + // Update scroll range to make the current position become 50% point. + scroller.style.paddingBottom = "50px"; + await waitForNextFrame(); + + document.documentElement.classList.remove("reftest-wait"); + } +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-in-container-query.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-in-container-query.html new file mode 100644 index 0000000000..38b8ffdc15 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-in-container-query.html @@ -0,0 +1,75 @@ +<!DOCTYPE html> +<title>scroll-timeline and container queries</title> +<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#scroll-timeline-shorthand"> +<link rel="help" src="https://drafts.csswg.org/css-contain-3/#container-queries"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="support/testcommon.js"></script> +<style> + #outer { + height: 100px; + width: 150px; + } + + #container { + container-type: size; + } + + #scroller { + overflow: auto; + width: auto; + height: 100px; + } + + #scroller > div { + height: 200px; + } + + /* This does not apply initially. */ + @container (width > 200px) { + #scroller { + scroll-timeline: timeline; + } + } + + @keyframes recolor { + from { background-color: rgb(100, 100, 100); } + to { background-color: rgb(200, 200, 200); } + } + + #element { + height: 10px; + width: 10px; + animation: recolor 10s linear; + animation-timeline: timeline; + background-color: rgb(0, 0, 0); + } +</style> +<div id=outer> + <div id=container> + <div id=scroller> + <div></div> + <div id=element></div> + </div> + </div> +</div> +<script> + setup(assert_implements_animation_timeline); + + promise_test(async (t) => { + element.offsetTop; + scroller.scrollTop = (scroller.scrollHeight - scroller.clientHeight) / 2; + await waitForNextFrame(); + // Unknown timeline, time held at zero. + assert_equals(getComputedStyle(element).backgroundColor, 'rgb(100, 100, 100)'); + // This causes the timeline to be created. + outer.style.width = '250px'; + // Check value with getComputedStyle immediately, which is the unanimated + // value since the scroll timeline is inactive before the next frame. + assert_equals(getComputedStyle(element).backgroundColor, 'rgb(0, 0, 0)'); + // Also check value after one frame. + await waitForNextFrame(); + assert_equals(getComputedStyle(element).backgroundColor, 'rgb(150, 150, 150)'); + }, 'Timeline appearing via container queries'); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-inactive.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-inactive.html new file mode 100644 index 0000000000..0953f1b389 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-inactive.html @@ -0,0 +1,90 @@ +<!DOCTYPE html> +<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#scroll-timelines"> +<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#avoiding-cycles"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<style> + @keyframes expand { + from { width: 100px; } + to { width: 200px; } + } + .scroller { + overflow: scroll; + width: 100px; + height: 100px; + } +</style> +<main id=main></main> +<script> + function inflate(t, template) { + t.add_cleanup(() => main.replaceChildren()); + main.append(template.content.cloneNode(true)); + main.offsetTop; + } +</script> + +<template id=basic> + <style> + #timeline { + scroll-timeline: timeline; + } + #element { + width: 0px; + animation: expand 10s linear paused; + animation-timeline: timeline; + } + </style> + <div id="container"> + <div id=timeline class=scroller><div> + <div id=element></div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, basic); + await waitForNextFrame(); + assert_equals(getComputedStyle(element).width, '0px'); + }, 'Animation does not apply when the timeline is inactive because there is ' + + 'not scroll range'); +</script> + +<template id=dynamically_change_range> + <style> + #contents { + height: 200px; + } + #element { + width: 0px; + animation: expand 10s linear paused; + animation-timeline: timeline; + } + </style> + <div id="container"> + <div id=element></div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, dynamically_change_range); + await waitForNextFrame(); + + let div = document.createElement('div'); + div.setAttribute('class', 'scroller'); + div.style.scrollTimeline = 'timeline'; + div.innerHTML = '<div id=contents></div>'; + try { + container.insertBefore(div, element); + + // The source has no layout box at the time the scroll timeline is created. + assert_equals(getComputedStyle(element).width, '0px'); + scroller.offsetTop; // Ensure a layout box for the scroller. + // Wait for an update to the timeline state: + await waitForNextFrame(); + // The timeline should now be active, and the animation should apply: + assert_equals(getComputedStyle(element).width, '100px'); + } finally { + div.remove(); + } + }, 'Animation does not apply when timeline is initially inactive'); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-inline-orientation-ref.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-inline-orientation-ref.html new file mode 100644 index 0000000000..7b87b1db39 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-inline-orientation-ref.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<title>Reference for scroll timeline with inline orientation and root scroller</title> +<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1"> +<style> + html { + min-width: 100%; + padding-right: 100px; + } + + #box { + width: 100px; + height: 100px; + background-color: green; + transform: translateX(100px); + } + + * { + margin-left: 0px; + margin-right: 0px; + } +</style> + +<div id="box"></div> + +<script> + window.addEventListener('load', function() { + // Move the scroller to halfway. + const scroller = document.scrollingElement; + const maxScroll = scroller.scrollWidth - scroller.clientWidth; + scroller.scrollLeft = 0.5 * maxScroll; + }); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-inline-orientation.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-inline-orientation.html new file mode 100644 index 0000000000..52b7427f2d --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-inline-orientation.html @@ -0,0 +1,68 @@ +<!DOCTYPE HTML> +<html class="reftest-wait"> +<title>Scroll timeline with inline orientation and root scroller</title> +<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1"> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#scroll-notation"> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#descdef-scroll-timeline-orientation"> +<link rel="help" href="https://drafts.csswg.org/css-animations-2/#animation-timeline"> +<meta name="assert" content="CSS animation correctly updates values when using the inline orientation"> +<link rel="match" href="scroll-timeline-inline-orientation-ref.html"> + +<style> + @keyframes update { + from { transform: translateX(0px); } + to { transform: translateX(200px); } + } + + html { + min-width: 100%; + padding-right: 100px; + font-size: 0; + } + + #box { + width: 100px; + height: 100px; + background-color: green; + animation: update 1s linear; + animation-timeline: scroll(inline root); + display: inline-block; + } + + #covered { + width: 100px; + height: 100px; + background-color: red; + display: inline-block; + } + + * { + margin-left: 0px; + margin-right: 0px; + } +</style> + +<div id="box"></div> +<div id="covered"></div> + +<script src="/web-animations/testcommon.js"></script> +<script> + document.documentElement.addEventListener('TestRendered', async () => { + runTest(); + }, { once: true }); + + async function runTest() { + const scroller = document.scrollingElement; + + await waitForCompositorReady(); + + // Move the scroller to the halfway point. + const maxScroll = scroller.scrollWidth - scroller.clientWidth; + scroller.scrollLeft = 0.5 * maxScroll; + + await waitForNextFrame(); + await waitForNextFrame(); + + document.documentElement.classList.remove("reftest-wait"); + } +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-multi-pass.tentative.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-multi-pass.tentative.html new file mode 100644 index 0000000000..651ba212de --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-multi-pass.tentative.html @@ -0,0 +1,110 @@ +<!DOCTYPE html> +<title>ScrollTimelines may trigger multiple style/layout passes</title> +<link rel="help" src="https://github.com/w3c/csswg-drafts/issues/5261"> +<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#avoiding-cycles"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="support/testcommon.js"></script> +<style> + @keyframes expand_width { + from { width: 100px; } + to { width: 100px; } + } + @keyframes expand_height { + from { height: 100px; } + to { height: 100px; } + } + main { + height: 0px; + overflow: hidden; + scroll-timeline: timeline1 defer, timeline2 defer; + } + .scroller { + height: 100px; + overflow: scroll; + } + .scroller > div { + height: 200px; + } + #element1 { + width: 1px; + animation: expand_width 10s; + animation-timeline: timeline1; + } + #element2 { + height: 1px; + animation: expand_height 10s; + animation-timeline: timeline2; + } +</style> +<main id=main> + <div id=element1></div> + <div> + <div id=element2></div> + </div> +</main> +<script> + setup(assert_implements_animation_timeline); + + function insertScroller(timeline_name) { + let scroller = document.createElement('div'); + scroller.classList.add('scroller'); + scroller.style.scrollTimeline = `${timeline_name} ancestor`; + scroller.append(document.createElement('div')); + main.insertBefore(scroller, element1); + } + + promise_test(async () => { + await waitForNextFrame(); + + let events1 = []; + let events2 = []; + + insertScroller('timeline1'); + // Even though the scroller was just inserted into the DOM, |timeline1| + // remains inactive until the next frame. + // + // https://drafts.csswg.org/scroll-animations-1/#avoiding-cycles + assert_equals(getComputedStyle(element1).width, '1px'); + (new ResizeObserver(entries => { + events1.push(entries); + insertScroller('timeline2'); + assert_equals(getComputedStyle(element2).height, '1px'); + })).observe(element1); + + (new ResizeObserver(entries => { + events2.push(entries); + })).observe(element2); + + await waitForNextFrame(); + + // According to the basic rules of the spec [1], the timeline is + // inactive at the time the resize observer event was delivered, because + // #scroller1 did not have a layout box at the time style recalc for + // #element1 happened. + // + // However, an additional style/layout pass should take place + // (before resize observer deliveries) if we detect new ScrollTimelines + // in this situation, hence we ultimately do expect the animation to + // apply [2]. + // + // [1] https://drafts.csswg.org/scroll-animations-1/#avoiding-cycles + // [2] https://github.com/w3c/csswg-drafts/issues/5261 + assert_equals(events1.length, 1); + assert_equals(events1[0].length, 1); + assert_equals(events1[0][0].contentBoxSize.length, 1); + assert_equals(events1[0][0].contentBoxSize[0].inlineSize, 100); + + // ScrollTimelines created during the ResizeObserver should remain + // inactive during the frame they're created, so the ResizeObserver + // event should not reflect the animated value. + assert_equals(events2.length, 1); + assert_equals(events2[0].length, 1); + assert_equals(events2[0][0].contentBoxSize.length, 1); + assert_equals(events2[0][0].contentBoxSize[0].blockSize, 1); + + assert_equals(getComputedStyle(element1).width, '100px'); + assert_equals(getComputedStyle(element2).height, '100px'); + }, 'Multiple style/layout passes occur when necessary'); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-name-computed.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-name-computed.html new file mode 100644 index 0000000000..bfffafc652 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-name-computed.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#scroll-timeline-name"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/css/support/computed-testcommon.js"></script> +</head> +<style> + #outer { scroll-timeline-name: foo; } + #target { scroll-timeline-name: bar; } +</style> +<div id="outer"> + <div id="target"></div> +</div> +<script> +test_computed_value('scroll-timeline-name', 'initial', 'none'); +test_computed_value('scroll-timeline-name', 'inherit', 'foo'); +test_computed_value('scroll-timeline-name', 'unset', 'none'); +test_computed_value('scroll-timeline-name', 'revert', 'none'); +test_computed_value('scroll-timeline-name', 'none'); +test_computed_value('scroll-timeline-name', 'test'); +test_computed_value('scroll-timeline-name', 'foo, bar'); +test_computed_value('scroll-timeline-name', 'bar, foo'); +test_computed_value('scroll-timeline-name', 'a, b, c, D, e'); +test_computed_value('scroll-timeline-name', 'none, none'); +test_computed_value('scroll-timeline-name', 'a, b, c, none, d, e'); + +test(() => { + let style = getComputedStyle(document.getElementById('target')); + assert_not_equals(Array.from(style).indexOf('scroll-timeline-name'), -1); +}, 'The scroll-timeline-name property shows up in CSSStyleDeclaration enumeration'); + +test(() => { + let style = document.getElementById('target').style; + assert_not_equals(style.cssText.indexOf('scroll-timeline-name'), -1); +}, 'The scroll-timeline-name property shows up in CSSStyleDeclaration.cssText'); + +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-name-parsing.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-name-parsing.html new file mode 100644 index 0000000000..0fb271250a --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-name-parsing.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#scroll-timeline-name"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/css/support/parsing-testcommon.js"></script> +<div id="target"></div> +<script> + +test_valid_value('scroll-timeline-name', 'initial'); +test_valid_value('scroll-timeline-name', 'inherit'); +test_valid_value('scroll-timeline-name', 'unset'); +test_valid_value('scroll-timeline-name', 'revert'); + +test_valid_value('scroll-timeline-name', 'none'); +test_valid_value('scroll-timeline-name', 'abc'); +test_valid_value('scroll-timeline-name', ' abc', 'abc'); +test_valid_value('scroll-timeline-name', 'aBc'); +test_valid_value('scroll-timeline-name', 'foo, bar'); +test_valid_value('scroll-timeline-name', 'bar, foo'); +test_valid_value('scroll-timeline-name', 'none, none'); +test_valid_value('scroll-timeline-name', 'a, none, b'); +test_valid_value('scroll-timeline-name', 'auto'); + +test_invalid_value('scroll-timeline-name', 'default'); +test_invalid_value('scroll-timeline-name', '10px'); +test_invalid_value('scroll-timeline-name', 'foo bar'); +test_invalid_value('scroll-timeline-name', '"foo" "bar"'); +test_invalid_value('scroll-timeline-name', 'rgb(1, 2, 3)'); +test_invalid_value('scroll-timeline-name', '#fefefe'); + +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-name-shadow.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-name-shadow.html new file mode 100644 index 0000000000..f5cd2ce47d --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-name-shadow.html @@ -0,0 +1,185 @@ +<!DOCTYPE html> +<title>scroll-timeline-name and tree-scoped references</title> +<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#scroll-timelines-named"> +<link rel="help" src="https://github.com/w3c/csswg-drafts/issues/8135"> +<link rel="help" src="https://github.com/w3c/csswg-drafts/issues/8192"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="/resources/declarative-shadow-dom-polyfill.js"></script> + +<main id=main></main> +<script> + function inflate(t, template) { + t.add_cleanup(() => main.replaceChildren()); + main.append(template.content.cloneNode(true)); + main.offsetTop; + } + + setup(() => { + polyfill_declarative_shadow_dom(document); + }); +</script> +<style> + @keyframes anim { + from { z-index: 100; } + to { z-index: 100; } + } +</style> + +<template id=scroll_timeline_host> + <style> + .target { + animation: anim 10s linear; + animation-timeline: timeline; + } + main > .scroller { + scroll-timeline: timeline horizontal; + } + </style> + <div class=scroller> + <div class=scroller> + <template shadowrootmode=open> + <style> + :host { + scroll-timeline: timeline vertical; + } + </style> + <slot></slot> + </template> + <div class=target></div> + </div> + </div> + <style> + </style> +</template> +<script> + promise_test(async (t) => { + inflate(t, scroll_timeline_host); + let target = main.querySelector('.target'); + assert_equals(target.getAnimations().length, 1); + let anim = target.getAnimations()[0]; + assert_not_equals(anim.timeline, null); + assert_equals(anim.timeline.axis, 'vertical'); + }, 'Outer animation can see scroll timeline defined by :host'); +</script> + + +<template id=scroll_timeline_slotted> + <style> + .target { + animation: anim 10s linear; + animation-timeline: timeline; + } + .host { + scroll-timeline: timeline horizontal; + } + </style> + <div class=host> + <template shadowrootmode=open> + <style> + ::slotted(.scroller) { + scroll-timeline: timeline vertical; + } + </style> + <slot></slot> + </template> + <div class=scroller> + <div class=target></div> + </div> + </div> + <style> + </style> +</template> +<script> + promise_test(async (t) => { + inflate(t, scroll_timeline_slotted); + let target = main.querySelector('.target'); + assert_equals(target.getAnimations().length, 1); + let anim = target.getAnimations()[0]; + assert_not_equals(anim.timeline, null); + assert_equals(anim.timeline.axis, 'vertical'); + }, 'Outer animation can see scroll timeline defined by ::slotted'); +</script> + + +<template id=scroll_timeline_part> + <style> + .host { + scroll-timeline: timeline vertical; + } + .host::part(foo) { + scroll-timeline: timeline horizontal; + } + </style> + <div class=host> + <template shadowrootmode=open> + <style> + /* Not using 'anim' at document scope, due to https://crbug.com/1334534 */ + @keyframes anim2 { + from { z-index: 100; background-color: green; } + to { z-index: 100; background-color: green; } + } + .target { + animation: anim2 10s linear; + animation-timeline: timeline; + } + </style> + <div part=foo> + <div class=target></div> + </div> + </template> + </div> + <style> + </style> +</template> +<script> + promise_test(async (t) => { + inflate(t, scroll_timeline_part); + let target = main.querySelector('.host').shadowRoot.querySelector('.target'); + assert_equals(target.getAnimations().length, 1); + let anim = target.getAnimations()[0]; + assert_not_equals(anim.timeline, null); + assert_equals(anim.timeline.axis, 'horizontal'); + }, 'Inner animation can see scroll timeline defined by ::part'); +</script> + + +<template id=scroll_timeline_shadow> + <style> + .target { + animation: anim 10s linear; + animation-timeline: timeline; + } + .host { + scroll-timeline: timeline horizontal; + } + </style> + <div class=scroller> + <div class=host> + <template shadowrootmode=open> + <style> + div { + scroll-timeline: timeline vertical; + } + </style> + <div> + <slot></slot> + </div> + </template> + <div class=target></div> + </div> + </div> + <style> + </style> +</template> +<script> + promise_test(async (t) => { + inflate(t, scroll_timeline_shadow); + let target = main.querySelector('.target'); + assert_equals(target.getAnimations().length, 1); + let anim = target.getAnimations()[0]; + assert_not_equals(anim.timeline, null); + assert_equals(anim.timeline.axis, 'vertical'); + }, 'Slotted element can see scroll timeline within the shadow'); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-nearest-dirty.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-nearest-dirty.html new file mode 100644 index 0000000000..1a79c9bb22 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-nearest-dirty.html @@ -0,0 +1,42 @@ +<!DOCTYPE html> +<title>Unrelated style mutation does not affect anonymous timeline</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#scroll-notation"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/scroll-animations/scroll-timelines/testcommon.js"></script> +<script src="/css/css-animations/support/testcommon.js"></script> +<script src="support/testcommon.js"></script> +<style> + @keyframes anim { + from { z-index: 100; } + to { z-index: 100; } + } + #scroller { + overflow: auto; + width: 100px; + height: 100px; + } + #element { + animation: anim forwards; + animation-timeline: scroll(); + } + #spacer { + height: 200px; + } +</style> +<div id=scroller> + <div id=element></div> + <div id=spacer></div> +</div> + +<script> +setup(assert_implements_animation_timeline); + +promise_test(async () => { + await waitForCSSScrollTimelineStyle(); + assert_equals(getComputedStyle(element).zIndex, '100'); + // Unrelated style mutation does not change the effect value: + element.style.color = 'green'; + assert_equals(getComputedStyle(element).zIndex, '100'); +}); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-nearest-with-absolute-positioned-element.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-nearest-with-absolute-positioned-element.html new file mode 100644 index 0000000000..7fe2d12be3 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-nearest-with-absolute-positioned-element.html @@ -0,0 +1,79 @@ +<!DOCTYPE html> +<title>The animation-timeline: scroll-timeline-name</title> +<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1"> +<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/rewrite#scroll-timelines-named"> +<link rel="help" src="https://github.com/w3c/csswg-drafts/issues/6674"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="support/testcommon.js"></script> +<style> + @keyframes grow-progress { + to { width: 300px; } + } + + .scrollcontainer { + overflow-x: scroll; + display: flex; + flex-direction: row; + scroll-timeline: timeline inline; + } + + .progress { + position: absolute; + z-index: 10; + left: 0; + top: 0; + width: 100px; + height: 1em; + background: red; + animation: auto grow-progress linear forwards; + animation-timeline: scroll(inline nearest); + } + + .entry { + min-height: 90vh; + min-width: 100vw; + } + + .entry:nth-child(even) { + background-color: #eee; + } + + .entry:nth-child(odd) { + background-color: #ddd; + } +</style> +<body> + <div class = "scrollcontainer" id = "scroller"> + <div class = "progress" id = "target"></div> + <div class = "entry"></div> + <div class = "entry"></div> + <div class = "entry"></div> + </div> +</body> +<script> +"use strict"; + +setup(assert_implements_animation_timeline); + +promise_test(async t => { + const maxScroll = scroller.scrollWidth - scroller.clientWidth; + scroller.scrollLeft = maxScroll; + + // Advance to next frame so that scroll-timeline has a valid time. + await waitForNextFrame(); + + // Flex container is not position relative and therefore not the container for + // the progress element. + assert_equals(getComputedStyle(target).width, "100px"); + + // Once the scroller is position relative, it becomes the container block for + // the progress element. + scroller.style.position = 'relative'; + await waitForNextFrame(); + + assert_equals(getComputedStyle(target).width, "300px"); +}, 'Resolving scroll(nearest) for an absolutely positioned element'); + +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-paused-animations.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-paused-animations.html new file mode 100644 index 0000000000..54518a5e87 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-paused-animations.html @@ -0,0 +1,95 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Scroll timeline with paused animations</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#scroll-notation"> +<link rel="help" href="https://drafts.csswg.org/css-animations/#animation-play-state"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/scroll-animations/scroll-timelines/testcommon.js"></script> +<script src="/css/css-animations/support/testcommon.js"></script> +<script src="support/testcommon.js"></script> +<style> + @keyframes anim { + from { width: 100px; } + to { width: 200px; } + } + + .fill-vh { + width: 100px; + height: 100vh; + } +</style> +<body> +<div id="log"></div> +<script> +'use strict'; + +setup(assert_implements_animation_timeline); + +async function resetScrollPosition() { + // Reset to 0 so we don't affect following tests. + document.scrollingElement.scrollTop = 0; + return waitForNextFrame(); +} + +promise_test(async t => { + const div = addDiv(t, { style: 'width: 50px; height: 100px;' }); + const filling = addDiv(t, { class: 'fill-vh' }); + const scroller = document.scrollingElement; + t.add_cleanup(resetScrollPosition); + + div.style.animation = 'anim 100s linear paused'; + div.style.animationTimeline = 'scroll(root)'; + await waitForCSSScrollTimelineStyle(); + + const anim = div.getAnimations()[0]; + await anim.ready; + assert_percents_equal(anim.currentTime, 0, 'timeline time reset'); + assert_equals(getComputedStyle(div).width, '100px'); + + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = maxScroll; + await waitForNextFrame(); + assert_equals(getComputedStyle(div).width, '100px'); + +}, 'Test that the scroll animation is paused'); + +promise_test(async t => { + const div = addDiv(t, { style: 'width: 50px; height: 100px;' }); + const filling = addDiv(t, { class: 'fill-vh' }); + const scroller = document.scrollingElement; + await waitForNextFrame(); + + div.style.animation = 'anim 100s linear forwards'; + div.style.animationTimeline = 'scroll(root)'; + await waitForCSSScrollTimelineStyle(); + + const anim = div.getAnimations()[0]; + await anim.ready; + assert_percents_equal(anim.currentTime, 0, 'timeline time reset'); + assert_equals(getComputedStyle(div).width, '100px'); + + await waitForNextFrame(); + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = maxScroll; + await waitForNextFrame(); + assert_equals(getComputedStyle(div).width, '200px'); + + div.style.animationPlayState = 'paused'; + assert_equals(anim.playState, 'paused'); + assert_equals(getComputedStyle(div).width, '200px', + 'Current time preserved when pause-pending.'); + assert_true(anim.pending, + 'Pending state after changing animationPlayState'); + await anim.ready; + assert_equals(getComputedStyle(div).width, '200px', + 'Current time preserved when paused.'); + assert_percents_equal(anim.timeline.currentTime, 100); + document.scrollingElement.scrollTop = 0; + await waitForNextFrame(); + assert_percents_equal(anim.timeline.currentTime, 0); + assert_equals(getComputedStyle(div).width, '200px'); +}, 'Test that the scroll animation is paused by updating animation-play-state'); + +</script> +</body> diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-responsiveness-from-endpoint.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-responsiveness-from-endpoint.html new file mode 100644 index 0000000000..71d3699077 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-responsiveness-from-endpoint.html @@ -0,0 +1,62 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Root-scrolling timeline with animation moving from end point</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#scroll-notation"> +<link rel="help" href="https://drafts.csswg.org/web-animations/#update-an-animations-finished-state"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/css/css-animations/support/testcommon.js"></script> +<script src="/scroll-animations/scroll-timelines/testcommon.js"></script> +<script src="support/testcommon.js"></script> + +<style> + @keyframes anim { + from { width: 100px; } + to { width: 200px; } + } + + .fill-vh { + width: 100px; + height: 100vh; + } +</style> +<body> +<div id="log"></div> +<script> +'use strict'; + +setup(assert_implements_animation_timeline); + +promise_test(async t => { + const div = addDiv(t, { style: 'width: 50px; height: 100px;' }); + const filling = addDiv(t, { class: 'fill-vh' }); + const scroller = document.scrollingElement; + scroller.scrollTop = 0; + await waitForNextFrame(); + + div.style.animation = 'anim 100s linear'; + div.style.animationTimeline = 'scroll(root)'; + await waitForCSSScrollTimelineStyle(); + + const anim = div.getAnimations()[0]; + await anim.ready; + assert_percents_equal(anim.timeline.currentTime, 0, + 'Timeline time when animation is ready'); + assert_equals(getComputedStyle(div).width, '100px', + 'Width at animation start'); + + await waitForNextFrame(); + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = maxScroll; + await waitForNextFrame(); + assert_equals(getComputedStyle(div).width, '200px', + 'Width at scroll limit'); + + document.scrollingElement.scrollTop = 0; + await waitForNextFrame(); + assert_equals(getComputedStyle(div).width, '100px', + 'Width after reset to scroll top'); +}, 'Test that the scroll animation is still responsive after moving from 100%'); + +</script> +</body> diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-root-dirty.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-root-dirty.html new file mode 100644 index 0000000000..1c0b73ab45 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-root-dirty.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<title>Unrelated style mutation does not affect anonymous timeline (root)</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#scroll-notation"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/scroll-animations/scroll-timelines/testcommon.js"></script> +<script src="/css/css-animations/support/testcommon.js"></script> +<script src="support/testcommon.js"></script> +<style> + @keyframes anim { + from { z-index: 100; } + to { z-index: 100; } + } + #element { + animation: anim forwards; + animation-timeline: scroll(root); + } + #spacer { + height: 200vh; + } +</style> +<div id=element></div> +<div id=spacer></div> + +<script> +setup(assert_implements_animation_timeline); + +promise_test(async () => { + await waitForCSSScrollTimelineStyle(); + assert_equals(getComputedStyle(element).zIndex, '100'); + // Unrelated style mutation does not change the effect value: + element.style.color = 'green'; + assert_equals(getComputedStyle(element).zIndex, '100'); +}); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-sampling.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-sampling.html new file mode 100644 index 0000000000..51b60e73ce --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-sampling.html @@ -0,0 +1,46 @@ +<!DOCTYPE html> +<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#avoiding-cycles"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<style> + #scroller { + overflow: hidden; + width: 100px; + height: 100px; + scroll-timeline: timeline; + } + #contents { + height: 200px; + } + @keyframes expand { + from { width: 100px; } + to { width: 200px; } + } + #element { + width: 0px; + animation: expand 10s linear; + animation-timeline: timeline; + } + /* Ensure stable expectations if feature is not supported */ + @supports not (animation-timeline:foo) { + #element { animation-play-state: paused; } + } +</style> +<div id=scroller> + <div id=contents></div> + <div id=element></div> +</div> +<script> + promise_test(async (t) => { + // The scroll timeline is initially inactive until the first frame. + assert_equals(getComputedStyle(element).width, '0px'); + await waitForNextFrame(); + scroller.scrollTop = 50; + // Scrolling position should not yet be reflected in the animation, + // since the new scroll position has not yet been sampled. + assert_equals(getComputedStyle(element).width, '100px'); + await waitForNextFrame(); + assert_equals(getComputedStyle(element).width, '150px'); + }, 'Scroll position is sampled once per frame'); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-shorthand.tentative.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-shorthand.tentative.html new file mode 100644 index 0000000000..68e1cc955f --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-shorthand.tentative.html @@ -0,0 +1,122 @@ +<!DOCTYPE html> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#scroll-timeline-shorthand"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/css/support/computed-testcommon.js"></script> +<script src="/css/support/parsing-testcommon.js"></script> +<script src="/css/support/shorthand-testcommon.js"></script> +<div id="target"></div> +<script> +test_valid_value('scroll-timeline', 'none block', 'none'); +test_valid_value('scroll-timeline', 'none inline'); +test_valid_value('scroll-timeline', 'abc horizontal'); +test_valid_value('scroll-timeline', 'abc inline'); +test_valid_value('scroll-timeline', 'aBc inline'); +test_valid_value('scroll-timeline', 'inline inline'); +test_valid_value('scroll-timeline', 'abc'); + +test_valid_value('scroll-timeline', 'inline block', 'inline'); +test_valid_value('scroll-timeline', 'block block', 'block'); +test_valid_value('scroll-timeline', 'vertical block', 'vertical'); +test_valid_value('scroll-timeline', 'horizontal block', 'horizontal'); + +test_valid_value('scroll-timeline', 'a, b, c'); +test_valid_value('scroll-timeline', 'a inline, b block, c vertical', 'a inline, b, c vertical'); +test_valid_value('scroll-timeline', 'auto'); +test_valid_value('scroll-timeline', 'abc defer vertical', 'abc vertical defer'); +test_valid_value('scroll-timeline', 'abc vertical defer'); + +test_invalid_value('scroll-timeline', ''); +test_invalid_value('scroll-timeline', 'abc abc'); +test_invalid_value('scroll-timeline', 'block none'); +test_invalid_value('scroll-timeline', 'inline abc'); +test_invalid_value('scroll-timeline', 'default'); +test_invalid_value('scroll-timeline', ','); +test_invalid_value('scroll-timeline', ',,block,,'); + +test_computed_value('scroll-timeline', 'none block', 'none'); +test_computed_value('scroll-timeline', 'abc inline'); +test_computed_value('scroll-timeline', 'none vertical'); +test_computed_value('scroll-timeline', 'abc horizontal'); +test_computed_value('scroll-timeline', 'vertical vertical'); +test_computed_value('scroll-timeline', 'abc'); +test_computed_value('scroll-timeline', 'inline block', 'inline'); +test_computed_value('scroll-timeline', 'block block', 'block'); +test_computed_value('scroll-timeline', 'vertical block', 'vertical'); +test_computed_value('scroll-timeline', 'horizontal block', 'horizontal'); +test_computed_value('scroll-timeline', 'a, b, c'); +test_computed_value('scroll-timeline', 'a inline, b block, c vertical', 'a inline, b, c vertical'); +test_computed_value('scroll-timeline', 'abc defer vertical', 'abc vertical defer'); +test_computed_value('scroll-timeline', 'abc vertical defer'); + +test_shorthand_value('scroll-timeline', 'abc vertical local', +{ + 'scroll-timeline-name': 'abc', + 'scroll-timeline-axis': 'vertical', + 'scroll-timeline-attachment': 'local', +}); +test_shorthand_value('scroll-timeline', 'inline horizontal defer', +{ + 'scroll-timeline-name': 'inline', + 'scroll-timeline-axis': 'horizontal', + 'scroll-timeline-attachment': 'defer', +}); +test_shorthand_value('scroll-timeline', 'abc vertical ancestor, def', +{ + 'scroll-timeline-name': 'abc, def', + 'scroll-timeline-axis': 'vertical, block', + 'scroll-timeline-attachment': 'ancestor, local', +}); +test_shorthand_value('scroll-timeline', 'abc, def', +{ + 'scroll-timeline-name': 'abc, def', + 'scroll-timeline-axis': 'block, block', + 'scroll-timeline-attachment': 'local, local', +}); + +function test_shorthand_contraction(shorthand, longhands, expected) { + let longhands_fmt = Object.entries(longhands).map((e) => `${e[0]}:${e[1]}:${e[2]}`).join(';'); + test((t) => { + t.add_cleanup(() => { + for (let shorthand of Object.keys(longhands)) + target.style.removeProperty(shorthand); + }); + for (let [shorthand, value] of Object.entries(longhands)) + target.style.setProperty(shorthand, value); + assert_equals(target.style.getPropertyValue(shorthand), expected, 'Declared value'); + assert_equals(getComputedStyle(target).getPropertyValue(shorthand), expected, 'Computed value'); + }, `Shorthand contraction of ${longhands_fmt}`); +} + +test_shorthand_contraction('scroll-timeline', { + 'scroll-timeline-name': 'abc', + 'scroll-timeline-axis': 'inline', + 'scroll-timeline-attachment': 'defer', +}, 'abc inline defer'); + +test_shorthand_contraction('scroll-timeline', { + 'scroll-timeline-name': 'a, b', + 'scroll-timeline-axis': 'inline, block', + 'scroll-timeline-attachment': 'ancestor, local', +}, 'a inline ancestor, b'); + +test_shorthand_contraction('scroll-timeline', { + 'scroll-timeline-name': 'none, none', + 'scroll-timeline-axis': 'block, block', + 'scroll-timeline-attachment': 'local, local', +}, 'none, none'); + +// Longhands with different lengths: + +test_shorthand_contraction('scroll-timeline', { + 'scroll-timeline-name': 'a, b, c', + 'scroll-timeline-axis': 'inline, inline', + 'scroll-timeline-attachment': 'local, local', +}, ''); + +test_shorthand_contraction('scroll-timeline', { + 'scroll-timeline-name': 'a, b', + 'scroll-timeline-axis': 'inline, inline, inline', + 'scroll-timeline-attachment': 'local, local', +}, ''); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-update-reversed-animation.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-update-reversed-animation.html new file mode 100644 index 0000000000..93ad6916ea --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-update-reversed-animation.html @@ -0,0 +1,69 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<title>Attach a scroll timeline to a reversed animation refTest</title> +<link rel="help" src="https://www.w3.org/TR/scroll-animations-1/#scroll-timeline-name"> +<link rel="match" href="./animation-update-ref.html?translate=55px&scroll=825"> +<script src="/web-animations/testcommon.js"></script> +</head> +<style type="text/css"> + @keyframes anim { + from { transform: translateX(100px) } + to { transform: translateX(0px) } + } + #scroller { + border: 1px solid black; + overflow: hidden; + width: 300px; + height: 200px; + scroll-timeline: timeline; + } + #target { + margin-bottom: 800px; + margin-top: 800px; + margin-left: 10px; + margin-right: 10px; + width: 100px; + height: 100px; + z-index: -1; + background-color: green; + animation: anim 10s linear paused; + } + #target.update { + animation-play-state: running; + animation-timeline: timeline; + animation-duration: auto; + } +</style> +<body> + <div id="scroller"> + <div id="target"></div> + </div> +</body> +<script type="text/javascript"> + document.documentElement.addEventListener('TestRendered', async () => { + runTest(); + }, { once: true }); + + async function runTest() { + await waitForCompositorReady(); + + const anim = target.getAnimations()[0]; + anim.playbackRate = -1; + await anim.ready; + + // Scroll to 55% of maximum scroll while paused. + scroller.scrollTop = 825; + await waitForNextFrame(); + + target.classList.add('update'); + await waitForNextFrame(); + + // Make sure change to animation range was properly picked up. + document.documentElement.classList.remove("reftest-wait"); + } +</script> +</body> +</html> diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-with-percent-delay.tentative.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-with-percent-delay.tentative.html new file mode 100644 index 0000000000..4f2e1761de --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-with-percent-delay.tentative.html @@ -0,0 +1,91 @@ +<!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"> + @keyframes anim { + from { opacity: 0 } + to { opacity: 1 } + } + #scroller { + border: 10px solid lightgray; + overflow-y: scroll; + width: 300px; + height: 200px; + } + #target { + margin: 800px 0px; + width: 100px; + height: 100px; + z-index: -1; + background-color: green; + animation: anim auto linear; + animation-timeline: scroll(); + /* Sentinel value when in before or after phase of the animation. */ + opacity: 0.96875; + } +</style> +<body> + <div id=scroller> + <div id=target></div> + </div> +</body> +<script type="text/javascript"> + async function runTest() { + + function assert_opacity_equals(expected, errorMessage) { + assert_approx_equals( + parseFloat(getComputedStyle(target).opacity), expected, 1e-6, + errorMessage); + } + + promise_test(async t => { + await waitForNextFrame(); + const anim = document.getAnimations()[0]; + await anim.ready; + + await waitForNextFrame(); + scroller.scrollTop = + (scroller.scrollHeight - scroller.clientHeight) / 2; + await waitForNextFrame(); + + const baseOpacity = 0.96875; + // Delays are percentages. + const testData = [ + { delay: 0, endDelay: 0, opacity: 0.5 }, + { delay: 20, endDelay: 0, opacity: 0.375 }, + { delay: 0, endDelay: 20, opacity: 0.625 }, + { delay: 20, endDelay: 20, opacity: 0.5 }, + // // Negative delays. + { delay: -25, endDelay: 0, opacity: 0.6 }, + { delay: 0, endDelay: -25, opacity: 0.4 }, + { delay: -25, endDelay: -25, opacity: 0.5 }, + // Stress tests with >= 100% total delay. Verify effect is inactive. + { delay: 100, endDelay: 0, opacity: baseOpacity }, + { delay: 0, endDelay: 100, opacity: baseOpacity }, + { delay: 100, endDelay: 100, opacity: baseOpacity } + ]; + + testData.forEach(test => { + anim.effect.updateTiming({ + delay: CSS.percent(test.delay), + endDelay: CSS.percent(test.endDelay) + }); + assert_opacity_equals( + test.opacity, + `Opacity when delay=${test.delay} and endDelay=${test.endDelay}`); + }); + }, 'ScrollTimeline with animation delays as percentages'); + } + + window.onload = runTest; + +</script> +</html> diff --git a/testing/web-platform/tests/scroll-animations/css/support/testcommon.js b/testing/web-platform/tests/scroll-animations/css/support/testcommon.js new file mode 100644 index 0000000000..66bc27bb10 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/support/testcommon.js @@ -0,0 +1,19 @@ +'use strict'; + +/** + * Returns a Promise that is resolved after a CSS scroll timeline is created (as + * the result of a style change) and a snapshot has been taken, so that the + * animation style is correctly reflected by getComputedStyle(). + * Technically, this only takes a full frame update. We implement this as two + * requestAnimationFrame callbacks because the result will be available at the + * beginning of the second frame. + */ +async function waitForCSSScrollTimelineStyle() { + await waitForNextFrame(); + await waitForNextFrame(); +} + +function assert_implements_animation_timeline() { + assert_implements(CSS.supports('animation-timeline:foo'), + 'animation-timeline not supported'); +} diff --git a/testing/web-platform/tests/scroll-animations/css/timeline-offset-in-keyframe-change-timeline.tentative.html b/testing/web-platform/tests/scroll-animations/css/timeline-offset-in-keyframe-change-timeline.tentative.html new file mode 100644 index 0000000000..eeb13150aa --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/timeline-offset-in-keyframe-change-timeline.tentative.html @@ -0,0 +1,148 @@ +<!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> +<title>Animation range and delay</title> +</head> +<style type="text/css"> + @keyframes anim { + cover 0% { + opacity: 0; + margin-left: 0px; + } + cover 100% { + opacity: 1; + margin-right: 0px; + } + } + #scroller { + border: 10px solid lightgray; + overflow-y: scroll; + overflow-x: hidden; + width: 300px; + height: 200px; + view-timeline: sibling defer; + } + #sibling { + margin-top: 800px; + margin-left: 10px; + margin-right: 10px; + width: 100px; + height: 50px; + background-color: blue; + view-timeline: sibling block ancestor; + } + #target { + margin-bottom: 800px; + margin-left: 10px; + margin-right: 10px; + width: 100px; + height: 100px; + z-index: -1; + background-color: green; + animation: anim auto both linear; + /* using document timeline by default */ + animation-range-start: contain 0%; + animation-range-end: contain 100%; + view-timeline: target block; + } + + #target.with-view-timeline { + animation-timeline: target; + } + #target.with-view-timeline.retarget { + animation-timeline: sibling; + } +</style> +<body> + <div id="scroller"> + <div id="sibling"></div> + <div id="target"></div> + </div> +</body> +<script type="text/javascript"> + async function runTest() { + promise_test(async t => { + await waitForNextFrame(); + const anim = document.getAnimations()[0]; + await anim.ready; + await waitForNextFrame(); + + // Initially using a document timeline, so the keyframes should be + // ignored. + 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); + + // Once a view-timeline is added, the kefyrames must update to reflect + // the new keyframe offsets. + target.classList.add('with-view-timeline'); + assert_equals(getComputedStyle(target).animationTimeline, 'target', + 'Switch to view timeline'); + await waitForNextFrame(); + + frames = anim.effect.getKeyframes(); + expected = [ + { offset: 0, computedOffset: 0, easing: "linear", composite: "replace", + marginRight: "10px" }, + { offset: 1, computedOffset: 1, easing: "linear", composite: "replace", + marginLeft: "10px" }, + { 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); + + target.classList.add('retarget'); + assert_equals(getComputedStyle(target).animationTimeline, 'sibling', + 'Switch to another view timeline'); + await waitForNextFrame(); + frames = anim.effect.getKeyframes(); + expected = [ + { offset: 0, computedOffset: 0, easing: "linear", composite: "replace", + marginRight: "10px" }, + { offset: 1, computedOffset: 1, easing: "linear", composite: "replace", + marginLeft: "10px" }, + { offset: { rangeName: 'cover', offset: CSS.percent(0) }, + computedOffset: -1/3, easing: "linear", + composite: "auto", marginLeft: "0px", opacity: "0" }, + { offset: { rangeName: 'cover', offset: CSS.percent(100) }, + computedOffset: 4/3, easing: "linear", composite: "auto", + marginRight: "0px", opacity: "1" }, + ]; + assert_frame_lists_equal(frames, expected); + + target.classList.toggle('with-view-timeline'); + assert_equals(getComputedStyle(target).animationTimeline, 'auto', + 'Switch back to document timeline'); + frames = anim.effect.getKeyframes(); + 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); + }, 'getKeyframes with timeline-offsets'); + } + + window.onload = runTest; +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/timeline-offset-keyframes-hidden-subject.html b/testing/web-platform/tests/scroll-animations/css/timeline-offset-keyframes-hidden-subject.html new file mode 100644 index 0000000000..bea072aaf7 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/timeline-offset-keyframes-hidden-subject.html @@ -0,0 +1,126 @@ +<!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> +<title>Animation range and delay</title> +</head> +<style type="text/css"> + @keyframes anim { + cover 0% { + margin-left: 0px; + } + 50% { + opacity: 0.5; + } + cover 100% { + margin-right: 0px; + } + } + + #scroller { + border: 10px solid lightgray; + overflow-y: scroll; + overflow-x: hidden; + width: 300px; + height: 200px; + view-timeline: t1 defer; + } + #block { + margin-top: 800px; + margin-left: 10px; + margin-right: 10px; + width: 100px; + height: 50px; + background-color: blue; + view-timeline: t1 ancestor; + } + #target { + 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-range-start: contain 0%; + animation-range-end: contain 100%; + animation-timeline: t1; + } +</style> +<body> + <div id="scroller"> + <div id="block"></div> + <div id="target"></div> + </div> +</body> +<script type="text/javascript"> + async function runTest() { + promise_test(async t => { + await waitForNextFrame(); + const anims = document.getAnimations(); + assert_equals(anims.length, 1, + "Should have one animation attached to the view-timeline"); + const anim = anims[0]; + await anim.ready; + await waitForNextFrame(); + + let frames = anim.effect.getKeyframes(); + let expected_resolved_offsets = [ + { offset: 0, computedOffset: 0, easing: "linear", composite: "replace", + marginRight: "10px", opacity: "1" }, + { offset: 1/2, computedOffset: 1/2, easing: "linear", + composite: "auto", opacity: "0.5" }, + { offset: 1, computedOffset: 1, easing: "linear", composite: "replace", + marginLeft: "10px", opacity: "1" }, + { offset: { rangeName: "cover", offset: CSS.percent(0) }, + computedOffset: -1/3, easing: "linear", + composite: "auto", marginLeft: "0px" }, + { offset: { rangeName: "cover", offset: CSS.percent(100) }, + computedOffset: 4/3, easing: "linear", composite: "auto", + marginRight: "0px" }, + ]; + assert_frame_lists_equal(frames, expected_resolved_offsets, + 'Initial keyframes with active view-timeline'); + + block.style.display = 'none'; + // View-timeline becomes inactive. Keyframes with timeline offsets must be + // ignored. + frames = anim.effect.getKeyframes(); + let expected_unresolved_offsets = [ + { offset: 0, computedOffset: 0, opacity: "1", easing: "linear", + composite: "replace" }, + { offset: 0.5, computedOffset: 0.5, opacity: "0.5", easing: "linear", + composite: "auto", }, + { offset: 1, computedOffset: 1, opacity: "1", easing: "linear", + composite: "replace" }, + { offset: { rangeName: 'cover', offset: CSS.percent(0) }, + computedOffset: null, easing: "linear", + composite: "auto", marginLeft: "0px" }, + { offset: { rangeName: 'cover', offset: CSS.percent(100) }, + computedOffset: null, easing: "linear", composite: "auto", + marginRight: "0px" } + ]; + assert_frame_lists_equal(frames, expected_unresolved_offsets, + 'Keyframes with invalid view timeline'); + + block.style.display = 'block'; + // Timeline remains inactive until next frame. + await waitForNextFrame(); + + // Ensure that keyframes with timeline-offsets are restored. + frames = anim.effect.getKeyframes(); + + assert_frame_lists_equal(frames, expected_resolved_offsets, + 'Keyframes with restored view timeline'); + }, 'Keyframes with timeline-offsets ignored when timeline is inactive'); + } + + window.onload = runTest; +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/timeline-offset-keyframes-with-document-timeline.html b/testing/web-platform/tests/scroll-animations/css/timeline-offset-keyframes-with-document-timeline.html new file mode 100644 index 0000000000..03ee381fd9 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/timeline-offset-keyframes-with-document-timeline.html @@ -0,0 +1,80 @@ +<!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> +<title>Animation range and delay</title> +</head> +<style type="text/css"> + @keyframes anim { + cover 100% { + margin-right: 0px; + } + cover 0% { + margin-left: 0px; + } + 50% { + opacity: 0.5; + } + } + #scroller { + border: 10px solid lightgray; + overflow-y: scroll; + overflow-x: hidden; + width: 300px; + height: 200px; + } + #target { + margin-bottom: 800px; + margin-top: 800px; + margin-left: 10px; + margin-right: 10px; + width: 100px; + height: 100px; + z-index: -1; + background-color: green; + animation: anim auto both linear; + /* using document timeline by default */ + } +</style> +<body> + <div id="scroller"> + <div id="target"></div> + </div> +</body> +<script type="text/javascript"> + async function runTest() { + promise_test(async t => { + await waitForNextFrame(); + const anim = document.getAnimations()[0]; + await anim.ready; + await waitForNextFrame(); + + // Using a document timeline, so only the 50% keyframe is used. + let frames = anim.effect.getKeyframes(); + let expected = [ + { offset: 0, computedOffset: 0, opacity: "1", easing: "linear", + composite: "replace" }, + { offset: 0.5, computedOffset: 0.5, opacity: "0.5", easing: "linear", + composite: "auto" }, + { offset: 1, computedOffset: 1, opacity: "1", easing: "linear", + composite: "replace" }, + { offset: { rangeName: "cover", offset: CSS.percent(100) }, + computedOffset: null, marginRight: "0px", composite: "auto", + easing: "linear" }, + { offset: { rangeName: "cover", offset: CSS.percent(0) }, + computedOffset: null, marginLeft: "0px", composite: "auto", + easing: "linear" } + ]; + assert_frame_lists_equal(frames, expected); + }, 'Keyframes with timeline-offsets reported but not reachable when ' + + 'using a document timeline'); + } + + window.onload = runTest; +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/timeline-range-name-offset-in-keyframes.tentative.html b/testing/web-platform/tests/scroll-animations/css/timeline-range-name-offset-in-keyframes.tentative.html new file mode 100644 index 0000000000..54467bc83b --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/timeline-range-name-offset-in-keyframes.tentative.html @@ -0,0 +1,109 @@ +<!DOCTYPE html> +<html> +<meta charset="utf-8"> +<title>Timeline offset in Animation Keyframes</title> +<link rel="help" href="https://w3c.github.io/csswg-drafts/scroll-animations-1/#named-range-keyframes"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="support/testcommon.js"></script> +<style> + @keyframes fade-in-out-animation { + entry 0%, exit 100% { opacity: 0 } + entry 100%, exit 0% { opacity: 1 } + } + + #subject { + background-color: rgba(0, 0, 255); + height: 200px; + width: 200px; + view-timeline-name: foo; + animation: linear 1s both fade-in-out-animation; + animation-timeline: foo; + } + + #container { + border: 5px solid black; + height: 400px; + width: 400px; + overflow-y: scroll; + resize: both; + } + + .spacer { + height: 600px; + width: 100%; + } +</style> +<body onload="runTests()"> + <div id="container"> + <div class="spacer"></div> + <div id="subject"></div> + <div class="spacer"></div> + </div> +</body> + +<script type="text/javascript"> + setup(assert_implements_animation_timeline); + + function runTests() { + promise_test(async t => { + // scrollTop=200 to 400 is the entry range + container.scrollTop = 200; + await waitForNextFrame(); + assert_equals(getComputedStyle(subject).opacity, '0', + 'Effect at entry 0%'); + + container.scrollTop = 300; + await waitForNextFrame(); + assert_equals(getComputedStyle(subject).opacity, '0.5', + 'Effect at entry 50%'); + + container.scrollTop = 400; + await waitForNextFrame(); + assert_equals(getComputedStyle(subject).opacity, '1', + 'Effect at entry 100%'); + + // scrollTop=600-800 is the exit range + container.scrollTop = 600; + await waitForNextFrame(); + assert_equals(getComputedStyle(subject).opacity, '1', + 'Effect at exit 0%'); + + container.scrollTop = 700; + await waitForNextFrame(); + assert_equals(getComputedStyle(subject).opacity, '0.5', + 'Effect at exit 50%'); + + container.scrollTop = 800; + await waitForNextFrame(); + assert_equals(getComputedStyle(subject).opacity, '0', + 'Effect at exit 100%'); + + // First change scrollTop so that you are at entry 100%, then resize the + // container in a way that scrollTop is the same, but now the animation is + // at entry 50% and check opacity. After changing the height of container, + // scrollTop=300-500 is the entry range + container.scrollTop = 400; + await waitForNextFrame(); + assert_equals(getComputedStyle(subject).opacity, '1', + 'Effect at entry 100%'); + + // Reducing the viewport by 100px, shifts the keyframe offsets. + // The entry range shifts from [200px, 400px] to [300px, 500px]. + container.style.height = '300px'; + + await waitForNextFrame(); + assert_equals(getComputedStyle(subject).opacity, '0.5', + 'Effect at entry 50% (post resize)'); + + // After changing the height of container, scrollTop=600-800 is still the + // exit range + container.scrollTop = 700; + await waitForNextFrame(); + assert_equals(getComputedStyle(subject).opacity, '0.5', + 'Effect at exit 50% (post resize)'); + }); + } +</script> +</html> diff --git a/testing/web-platform/tests/scroll-animations/css/timeline-scope-computed.tentative.html b/testing/web-platform/tests/scroll-animations/css/timeline-scope-computed.tentative.html new file mode 100644 index 0000000000..814933f726 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/timeline-scope-computed.tentative.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/7759"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/css/support/computed-testcommon.js"></script> +</head> +<style> + #outer { timeline-scope: foo; } + #target { timeline-scope: bar; } +</style> +<div id="outer"> + <div id="target"></div> +</div> +<script> +test_computed_value('timeline-scope', 'initial', 'none'); +test_computed_value('timeline-scope', 'inherit', 'foo'); +test_computed_value('timeline-scope', 'unset', 'none'); +test_computed_value('timeline-scope', 'revert', 'none'); +test_computed_value('timeline-scope', 'none'); +test_computed_value('timeline-scope', 'test'); +test_computed_value('timeline-scope', 'foo, bar'); +test_computed_value('timeline-scope', 'bar, foo'); +test_computed_value('timeline-scope', 'a, b, c, D, e'); + +test(() => { + let style = getComputedStyle(document.getElementById('target')); + assert_not_equals(Array.from(style).indexOf('timeline-scope'), -1); +}, 'The timeline-scope property shows up in CSSStyleDeclaration enumeration'); + +test(() => { + let style = document.getElementById('target').style; + assert_not_equals(style.cssText.indexOf('timeline-scope'), -1); +}, 'The timeline-scope property shows up in CSSStyleDeclaration.cssText'); + +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/timeline-scope-parsing.tentative.html b/testing/web-platform/tests/scroll-animations/css/timeline-scope-parsing.tentative.html new file mode 100644 index 0000000000..2885cb758d --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/timeline-scope-parsing.tentative.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/7759"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/css/support/parsing-testcommon.js"></script> +<div id="target"></div> +<script> + +test_valid_value('timeline-scope', 'initial'); +test_valid_value('timeline-scope', 'inherit'); +test_valid_value('timeline-scope', 'unset'); +test_valid_value('timeline-scope', 'revert'); + +test_valid_value('timeline-scope', 'none'); +test_valid_value('timeline-scope', 'abc'); +test_valid_value('timeline-scope', ' abc', 'abc'); +test_valid_value('timeline-scope', 'aBc'); +test_valid_value('timeline-scope', 'foo, bar'); +test_valid_value('timeline-scope', 'bar, foo'); +test_valid_value('timeline-scope', 'auto'); + +test_invalid_value('timeline-scope', 'none, abc'); +test_invalid_value('timeline-scope', '10px'); +test_invalid_value('timeline-scope', 'foo bar'); +test_invalid_value('timeline-scope', '"foo" "bar"'); +test_invalid_value('timeline-scope', 'rgb(1, 2, 3)'); +test_invalid_value('timeline-scope', '#fefefe'); + +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/view-timeline-animation-range-update.tentative.html b/testing/web-platform/tests/scroll-animations/css/view-timeline-animation-range-update.tentative.html new file mode 100644 index 0000000000..6c2a792aee --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-animation-range-update.tentative.html @@ -0,0 +1,78 @@ +<!DOCTYPE html> +<html> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<title>Change animation-range after creation</title> +<link rel="help" src="https://www.w3.org/TR/scroll-animations-1/#named-range-animation-declaration"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="support/testcommon.js"></script> +<style> + @keyframes anim { + from { z-index: 0; background-color: skyblue;} + to { z-index: 100; background-color: coral; } + } + #scroller { + border: 10px solid lightgray; + overflow-y: scroll; + width: 200px; + height: 200px; + } + /* Reset specificity to allow animation-range-* from .restrict-range to win. */ + :where(#target) { + margin: 800px 0px; + width: 100px; + height: 100px; + z-index: -1; + background-color: green; + animation: anim auto both linear; + animation-timeline: t1; + view-timeline: t1 block; + } + .restrict-range { + animation-range-start: contain 0%; + animation-range-end: contain 100%; + } +</style> +<body> + <div id=scroller> + <div id=target></div> + </div> +</body> +<script type="text/javascript"> + setup(assert_implements_animation_timeline); + + async function scrollTop(e, value) { + e.scrollTop = value; + await waitForNextFrame(); + } + async function waitForAnimationReady(target) { + await waitForNextFrame(); + await Promise.all(target.getAnimations().map(x => x.promise)); + } + async function assertValueAt(scroller, target, position, expected) { + await waitForAnimationReady(target); + await scrollTop(scroller, position); + assert_equals(getComputedStyle(target).zIndex, expected.toString()); + } + + promise_test(async t => { + const scroller = document.getElementById('scroller'); + const target = document.getElementById('target'); + waitForAnimationReady(target); + + await assertValueAt(scroller, target, 600, 0); + await assertValueAt(scroller, target, 700, 33); + await assertValueAt(scroller, target, 750, 50); + await assertValueAt(scroller, target, 800, 67); + + target.classList.toggle('restrict-range'); + await waitForNextFrame(); + + await assertValueAt(scroller, target, 700, 0); + await assertValueAt(scroller, target, 750, 50); + await assertValueAt(scroller, target, 800, 100); + }, 'Ensure that animation is updated on a style change'); +</script> +</html> diff --git a/testing/web-platform/tests/scroll-animations/css/view-timeline-animation.html b/testing/web-platform/tests/scroll-animations/css/view-timeline-animation.html new file mode 100644 index 0000000000..a367ef9dd8 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-animation.html @@ -0,0 +1,221 @@ +<!DOCTYPE html> +<title>Animations using view-timeline</title> +<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1"> +<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#view-timelines-named"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="support/testcommon.js"></script> +<style> + @keyframes anim { + from { z-index: 0; } + to { z-index: 100; } + } + .vertical-scroller { + overflow: auto; + width: 100px; + height: 100px; + } + .vertical-scroller > div { + height: 50px; + z-index: -1; + } + .horizontal-scroller { + overflow: auto; + width: 100px; + height: 100px; + writing-mode: vertical-lr; + } + .horizontal-scroller > div { + width: 50px; + z-index: -1; + } +</style> +<main id=main></main> +<script> + setup(assert_implements_animation_timeline); + + function inflate(t, template) { + t.add_cleanup(() => main.replaceChildren()); + main.append(template.content.cloneNode(true)); + } + async function scrollTop(e, value) { + e.scrollTop = value; + await waitForNextFrame(); + } + async function scrollLeft(e, value) { + e.scrollLeft = value; + await waitForNextFrame(); + } +</script> + +<template id=default_view_timeline> + <style> + #target { + view-timeline: t1; + animation: anim 1s linear; + animation-timeline: t1; + } + </style> + <div id=scroller class=vertical-scroller> + <div></div> <!-- [0px, 50px] --> + <div></div> <!-- [50px, 100px] --> + <div></div> <!-- [100px, 150px] --> + <div id=target></div> <!-- [150px, 200px] --> + <div></div> <!-- [200px, 250px] --> + <div></div> <!-- [250px, 300px] --> + <div></div> <!-- [300px, 350px] --> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, default_view_timeline); + assert_equals(getComputedStyle(target).zIndex, '-1'); + await scrollTop(scroller, 25); + assert_equals(getComputedStyle(target).zIndex, '-1'); + await scrollTop(scroller, 50); // 0% (enter 0%) + assert_equals(getComputedStyle(target).zIndex, '0'); + await scrollTop(scroller, 125); // 50% + assert_equals(getComputedStyle(target).zIndex, '50'); + await scrollTop(scroller, 200); // 100% (exit 100%) + assert_equals(getComputedStyle(target).zIndex, '100'); + await scrollTop(scroller, 225); + assert_equals(getComputedStyle(target).zIndex, '-1'); + }, 'Default view-timeline'); +</script> + +<template id=horizontal_timeline> + <style> + #target { + view-timeline: t1 horizontal; + animation: anim 1s linear; + animation-timeline: t1; + } + </style> + <div id=scroller class=horizontal-scroller> + <div></div> <!-- [0px, 50px] --> + <div></div> <!-- [50px, 100px] --> + <div></div> <!-- [100px, 150px] --> + <div id=target></div> <!-- [150px, 200px] --> + <div></div> <!-- [200px, 250px] --> + <div></div> <!-- [250px, 300px] --> + <div></div> <!-- [300px, 350px] --> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, horizontal_timeline); + assert_equals(getComputedStyle(target).zIndex, '-1'); + await scrollLeft(scroller, 25); + assert_equals(getComputedStyle(target).zIndex, '-1'); + await scrollLeft(scroller, 50); // 0% (enter 0%) + assert_equals(getComputedStyle(target).zIndex, '0'); + await scrollLeft(scroller, 125); // 50% + assert_equals(getComputedStyle(target).zIndex, '50'); + await scrollLeft(scroller, 200); // 100% (exit 100%) + assert_equals(getComputedStyle(target).zIndex, '100'); + await scrollLeft(scroller, 225); + assert_equals(getComputedStyle(target).zIndex, '-1'); + }, 'Horizontal view-timeline'); +</script> + +<template id=multiple_timelines> + <style> + #timelines { + view-timeline: tv vertical ancestor, th horizontal ancestor; + background-color: red; + } + #scroller { + width: 100px; + height: 100px; + overflow: hidden; + display: grid; + grid-template-columns: 50px 50px 50px 50px 50px 50px 50px; + grid-template-row: 50px 50px 50px 50px 50px 50px 50px; + view-timeline: tv defer, th defer; + } + #scroller > div { + z-index: -1; + width: 50px; + height: 50px; + } + #target_v { + animation: anim 1s linear; + animation-timeline: tv; + } + #target_h { + animation: anim 1s linear; + animation-timeline: th; + } + </style> + <div id=scroller> + <!-- Created dynamically --> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, multiple_timelines); + + // Create a 350px x 350px grid (7x7 items of 50x50px each), with the + // timelines at item [3,3], an element attached to the horizontal timeline + // at [4,3], and an element attached to the vertical timeline at [3,4]. + + // x x x x x x x + // x x x x x x x + // x x x x x x x + // x x x T H x x + // x x x V x x x + // x x x x x x x + // x x x x x x x + // x x x x x x x + + let grid_size = 7; + for (let i = 0; i < (grid_size*grid_size); ++i) { + let div = document.createElement('div'); + if (i == (3 * grid_size + 3)) + div.id = 'timelines'; + if (i == (3 * grid_size + 4)) + div.id = 'target_h'; + if (i == (4 * grid_size + 3)) + div.id = 'target_v'; + scroller.append(div); + } + + assert_equals(getComputedStyle(target_v).zIndex, '-1'); + assert_equals(getComputedStyle(target_h).zIndex, '-1'); + + // First scroll vertically. + await scrollTop(scroller, 25); + assert_equals(getComputedStyle(target_v).zIndex, '-1'); + assert_equals(getComputedStyle(target_h).zIndex, '-1'); + await scrollTop(scroller, 50); // 0% (enter 0%) + assert_equals(getComputedStyle(target_v).zIndex, '0'); + assert_equals(getComputedStyle(target_h).zIndex, '-1'); + await scrollTop(scroller, 125); // 50% + assert_equals(getComputedStyle(target_v).zIndex, '50'); + assert_equals(getComputedStyle(target_h).zIndex, '-1'); + await scrollTop(scroller, 200); // 100% (exit 100%) + assert_equals(getComputedStyle(target_v).zIndex, '100'); + assert_equals(getComputedStyle(target_h).zIndex, '-1'); + await scrollTop(scroller, 225); + assert_equals(getComputedStyle(target_v).zIndex, '-1'); + assert_equals(getComputedStyle(target_h).zIndex, '-1'); + + // Then horizontally. + await scrollLeft(scroller, 25); + assert_equals(getComputedStyle(target_v).zIndex, '-1'); + assert_equals(getComputedStyle(target_h).zIndex, '-1'); + await scrollLeft(scroller, 50); // 0% (enter 0%) + assert_equals(getComputedStyle(target_v).zIndex, '-1'); + assert_equals(getComputedStyle(target_h).zIndex, '0'); + await scrollLeft(scroller, 125); // 50% + assert_equals(getComputedStyle(target_v).zIndex, '-1'); + assert_equals(getComputedStyle(target_h).zIndex, '50'); + await scrollLeft(scroller, 200); // 100% (exit 100%) + assert_equals(getComputedStyle(target_v).zIndex, '-1'); + assert_equals(getComputedStyle(target_h).zIndex, '100'); + await scrollLeft(scroller, 225); + assert_equals(getComputedStyle(target_v).zIndex, '-1'); + assert_equals(getComputedStyle(target_h).zIndex, '-1'); + }, 'Multiple view-timelines on the same element'); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/view-timeline-attachment-computed-tentative.html b/testing/web-platform/tests/scroll-animations/css/view-timeline-attachment-computed-tentative.html new file mode 100644 index 0000000000..dd244e137b --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-attachment-computed-tentative.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/css/support/computed-testcommon.js"></script> +<style> + #outer { view-timeline-attachment: defer; } + #target { view-timeline-attachment: ancestor; } +</style> +<div id="outer"> + <div id="target"></div> +</div> +<script> +test_computed_value('view-timeline-attachment', 'initial', 'local'); +test_computed_value('view-timeline-attachment', 'inherit', 'defer'); +test_computed_value('view-timeline-attachment', 'unset', 'local'); +test_computed_value('view-timeline-attachment', 'revert', 'local'); +test_computed_value('view-timeline-attachment', 'local'); +test_computed_value('view-timeline-attachment', 'defer'); +test_computed_value('view-timeline-attachment', 'ancestor'); +test_computed_value('view-timeline-attachment', 'local, defer'); +test_computed_value('view-timeline-attachment', 'defer, ancestor'); +test_computed_value('view-timeline-attachment', 'local, defer, ancestor'); +test_computed_value('view-timeline-attachment', 'local, local, local, local'); + +test(() => { + let style = getComputedStyle(document.getElementById('target')); + assert_not_equals(Array.from(style).indexOf('view-timeline-attachment'), -1); +}, 'The view-timeline-attachment property shows up in CSSStyleDeclaration enumeration'); + +test(() => { + let style = document.getElementById('target').style; + assert_not_equals(style.cssText.indexOf('view-timeline-attachment'), -1); +}, 'The view-timeline-attachment property shows up in CSSStyleDeclaration.cssText'); + +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/view-timeline-attachment-parsing-tentative.html b/testing/web-platform/tests/scroll-animations/css/view-timeline-attachment-parsing-tentative.html new file mode 100644 index 0000000000..25e20135f1 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-attachment-parsing-tentative.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/css/support/parsing-testcommon.js"></script> +<div id="target"></div> + +<script> + +test_valid_value('view-timeline-attachment', 'initial'); +test_valid_value('view-timeline-attachment', 'inherit'); +test_valid_value('view-timeline-attachment', 'unset'); +test_valid_value('view-timeline-attachment', 'revert'); + +test_valid_value('view-timeline-attachment', 'local'); +test_valid_value('view-timeline-attachment', 'defer'); +test_valid_value('view-timeline-attachment', 'ancestor'); +test_valid_value('view-timeline-attachment', 'local, defer'); +test_valid_value('view-timeline-attachment', 'defer, ancestor'); +test_valid_value('view-timeline-attachment', 'local, defer, ancestor, local'); +test_valid_value('view-timeline-attachment', 'local, local, local, local'); + +test_invalid_value('view-timeline-attachment', 'abc'); +test_invalid_value('view-timeline-attachment', '10px'); +test_invalid_value('view-timeline-attachment', 'auto'); +test_invalid_value('view-timeline-attachment', 'none'); +test_invalid_value('view-timeline-attachment', 'local defer'); +test_invalid_value('view-timeline-attachment', 'local / defer'); + +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/view-timeline-attachment.html b/testing/web-platform/tests/scroll-animations/css/view-timeline-attachment.html new file mode 100644 index 0000000000..ff98ed7825 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-attachment.html @@ -0,0 +1,433 @@ +<!DOCTYPE html> +<title>View Timeline Attachment</title> +<link rel="help" src="https://github.com/w3c/csswg-drafts/issues/7759"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> + +<main id=main></main> +<script> + function inflate(t, template) { + t.add_cleanup(() => main.replaceChildren()); + main.append(template.content.cloneNode(true)); + main.offsetTop; + } + + async function scrollTop(e, value) { + e.scrollTop = value; + await waitForNextFrame(); + } +</script> +<style> + @keyframes anim { + from { width: 0px; --applied:true; } + to { width: 200px; --applied:true; } + } + + .scroller { + overflow-y: hidden; + width: 200px; + height: 200px; + } + .scroller > .content { + margin: 400px 0px; + width: 100px; + height: 100px; + background-color: green; + } + .target { + background-color: coral; + width: 0px; + animation: anim auto linear; + animation-timeline: t1; + } + .timeline { + view-timeline-name: t1; + } + .local { + view-timeline-attachment: local; + } + .defer { + view-timeline-attachment: defer; + } + .ancestor { + view-timeline-attachment: ancestor; + } + +</style> + +<!-- Basic Behavior --> + +<template id=view_timeline_defer> + <div class="timeline defer"> + <div class=target>Test</div> + <div class=scroller> + <div class="content timeline ancestor"></div> + </div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, view_timeline_defer); + let scroller = main.querySelector('.scroller'); + let target = main.querySelector('.target'); + await scrollTop(scroller, 350); // 50% + assert_equals(getComputedStyle(target).width, '100px'); // 0px => 200px, 50% + }, 'Descendant can attach to deferred timeline'); +</script> + +<template id=view_timeline_defer_no_attach> + <div class="timeline defer"> + <div class=target>Test</div> + <div class=scroller> + <div class="timeline content"></div> + </div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, view_timeline_defer_no_attach); + let scroller = main.querySelector('.scroller'); + let target = main.querySelector('.target'); + await scrollTop(scroller, 350); // 50% + assert_equals(getComputedStyle(target).width, '0px'); + assert_equals(getComputedStyle(target).getPropertyValue('--applied'), ''); + }, 'Deferred timeline with no attachments'); +</script> + +<template id=view_timeline_defer_two_attachments> + <div class="timeline defer"> + <div class=target>Test</div> + <div class=scroller> + <div class="content timeline ancestor"></div> + <!-- Extra attachment --> + <div class="timeline ancestor"></div> + </div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, view_timeline_defer_two_attachments); + let scroller = main.querySelector('.scroller'); + let target = main.querySelector('.target'); + await scrollTop(scroller, 350); // 50% + assert_equals(getComputedStyle(target).width, '0px'); + assert_equals(getComputedStyle(target).getPropertyValue('--applied'), ''); + }, 'Deferred timeline with two attachments'); +</script> + +<!-- Effective Axis of ViewTimeline --> + +<template id=view_timeline_defer_axis> + <div class="timeline defer" style="view-timeline-axis:inline"> + <div class=target>Test</div> + <div class=scroller> + <div class="content timeline ancestor" style="view-timeline-axis:vertical"></div> + </div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, view_timeline_defer_axis); + let target = main.querySelector('.target'); + assert_equals(target.getAnimations().length, 1); + let anim = target.getAnimations()[0]; + assert_not_equals(anim.timeline, null); + assert_equals(anim.timeline.axis, 'vertical'); + }, 'Axis of deferred timeline is taken from attached timeline'); +</script> + +<template id=view_timeline_defer_axis_multiple> + <div class="timeline defer" style="view-timeline-axis:inline"> + <div class=target>Test</div> + <div class=scroller> + <div class="content timeline ancestor" style="view-timeline-axis:vertical"></div> + <!-- Extra attachment --> + <div class="timeline ancestor"></div> + </div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, view_timeline_defer_axis_multiple); + let target = main.querySelector('.target'); + assert_equals(target.getAnimations().length, 1); + let anim = target.getAnimations()[0]; + assert_not_equals(anim.timeline, null); + assert_equals(anim.timeline.axis, 'block'); + }, 'Axis of deferred timeline with multiple attachments'); +</script> + +<!-- Effective Inset of ViewTimeline --> + +<template id=view_timeline_inset> + <div class="timeline defer" style="view-timeline-inset:0px"> + <div class=target>Test</div> + <div class=scroller> + <div class="content timeline ancestor" style="view-timeline-inset:50px"></div> + </div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, view_timeline_inset); + let scroller = main.querySelector('.scroller'); + let target = main.querySelector('.target'); + + // Range: [200, 500] + [50, -50] (inset) = [250, 450] + await scrollTop(scroller, 300); // 25% + assert_equals(getComputedStyle(target).width, '50px'); // 0px => 200px, 25% + }, 'Inset of deferred timeline is taken from attached timeline'); +</script> + +<!-- Dynamic Reattachment --> + +<template id=view_timeline_reattach> + <div class="timeline defer"> + <div class=target>Test</div> + <div class=scroller> + <div class="content timeline ancestor"></div> + </div> + <div class=scroller> + <div class="content timeline"></div> + </div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, view_timeline_reattach); + let scrollers = main.querySelectorAll('.scroller'); + let contents = main.querySelectorAll('.content'); + assert_equals(scrollers.length, 2); + let target = main.querySelector('.target'); + // Range: [200, 500] + await scrollTop(scrollers[0], 350); // 50% + await scrollTop(scrollers[1], 275); // 25% + + // Attached to contents[0]. + assert_equals(getComputedStyle(target).width, '100px'); // 0px => 200px, 50% + + // Reattach to contents[1]. + contents[0].classList.remove('ancestor'); + contents[1].classList.add('ancestor'); + + await waitForNextFrame(); + assert_equals(getComputedStyle(target).width, '50px'); // 0px => 200px, 25% + }, 'Dynamically re-attaching'); +</script> + + +<template id=view_timeline_dynamic_attach_second> + <div class="timeline defer"> + <div class=target>Test</div> + <div class=scroller> + <div class="timeline content"></div> + </div> + <div class=scroller> + <div class="timeline content"></div> + </div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, view_timeline_dynamic_attach_second); + let scrollers = main.querySelectorAll('.scroller'); + let contents = main.querySelectorAll('.content'); + assert_equals(scrollers.length, 2); + let target = main.querySelector('.target'); + // Range: [200, 500] + await scrollTop(scrollers[0], 350); // 50% + await scrollTop(scrollers[1], 275); // 25% + + // Attached to no timelines initially: + assert_equals(getComputedStyle(target).width, '0px'); + assert_equals(getComputedStyle(target).getPropertyValue('--applied'), ''); + + // Attach to contents[0]. + contents[0].classList.add('ancestor'); + await waitForNextFrame(); + assert_equals(getComputedStyle(target).width, '100px'); // 0px => 200px, 50% + + // Also attach contents[1]. + contents[1].classList.add('ancestor'); + + await waitForNextFrame(); + assert_equals(getComputedStyle(target).width, '0px'); + assert_equals(getComputedStyle(target).getPropertyValue('--applied'), ''); + }, 'Dynamically attaching'); +</script> + + +<template id=view_timeline_dynamic_detach_second> + <div class="timeline defer"> + <div class=target>Test</div> + <div class=scroller> + <div class="content timeline ancestor"></div> + </div> + <div class=scroller> + <div class="content timeline ancestor"></div> + </div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, view_timeline_dynamic_detach_second); + let scrollers = main.querySelectorAll('.scroller'); + let contents = main.querySelectorAll('.content'); + assert_equals(scrollers.length, 2); + let target = main.querySelector('.target'); + // Range: [200, 500] + await scrollTop(scrollers[0], 350); // 50% + await scrollTop(scrollers[1], 275); // 25% + + // Attached to two timelines initially: + assert_equals(getComputedStyle(target).width, '0px'); + assert_equals(getComputedStyle(target).getPropertyValue('--applied'), ''); + + // Detach contents[1]. + contents[1].classList.remove('ancestor'); + + await waitForNextFrame(); + assert_equals(getComputedStyle(target).width, '100px'); // 0px => 200px, 50% + + // Also detach contents[0]. + contents[0].classList.remove('ancestor'); + + await waitForNextFrame(); + assert_equals(getComputedStyle(target).width, '0px'); + assert_equals(getComputedStyle(target).getPropertyValue('--applied'), ''); + }, 'Dynamically detaching'); +</script> + +<template id=view_timeline_ancestor_attached_removed> + <div class="timeline defer"> + <div class=target>Test</div> + <div class=scroller> + <div class="content timeline ancestor"></div> + </div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, view_timeline_ancestor_attached_removed); + let scroller = main.querySelector('.scroller'); + let target = main.querySelector('.target'); + let content = main.querySelector('.content'); + await scrollTop(scroller, 350); // 50% + assert_equals(getComputedStyle(target).width, '100px'); // 0px => 200px, 50% + + content.remove(); + await waitForNextFrame(); + assert_equals(getComputedStyle(target).width, '0px'); + assert_equals(getComputedStyle(target).getPropertyValue('--applied'), ''); + + scroller.append(content); + await scrollTop(scroller, 350); // 50% + assert_equals(getComputedStyle(target).width, '100px'); // 0px => 200px, 50% + }, 'Removing/inserting ancestor attached element'); +</script> + +<template id=view_timeline_ancestor_attached_display_none> + <div class="timeline defer"> + <div class=target>Test</div> + <div class=scroller> + <div class="content timeline ancestor"></div> + </div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, view_timeline_ancestor_attached_display_none); + let scroller = main.querySelector('.scroller'); + let target = main.querySelector('.target'); + let content = main.querySelector('.content'); + await scrollTop(scroller, 350); // 50% + assert_equals(getComputedStyle(target).width, '100px'); // 0px => 200px, 50% + + content.style.display = 'none'; + await waitForNextFrame(); + assert_equals(getComputedStyle(target).width, '0px'); + assert_equals(getComputedStyle(target).getPropertyValue('--applied'), ''); + + content.style.display = 'block'; + await scrollTop(scroller, 350); // 50% + assert_equals(getComputedStyle(target).width, '100px'); // 0px => 200px, 50% + }, 'Ancestor attached element becoming display:none/block'); +</script> + +<template id=view_timeline_dynamic_defer> + <style> + .inner-content { + margin: 100px 0px; + width: 20px; + height: 50px; + background-color: red; + } + </style> + <div class="scroller"> + <div class="content timeline"> + <div class="target"> + <div class="inner-content timeline ancestor"></div> + </div> + </div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, view_timeline_dynamic_defer); + let target = main.querySelector('.target'); + let scroller = main.querySelector('.scroller'); + let outer = main.querySelector('.content'); + let inner = main.querySelector('.inner-content'); + + // Outer view timeline range: [200, 500] + // Inner view timeline range: [200, 450] + + await scrollTop(scroller, 275); // 25% (outer), 30% (inner) + + // Attached to outer_view timeline (local). + assert_equals(getComputedStyle(target).width, '50px'); // 0px => 200px, 25% + + // Effectively attached to inner. + outer.classList.add('defer'); + await waitForNextFrame(); + assert_equals(getComputedStyle(target).width, '60px'); // 0px => 200px, 30% + + // Attached to outer_scroller again. + outer.classList.remove('defer'); + await waitForNextFrame(); + assert_equals(getComputedStyle(target).width, '50px'); // 0px => 200px, 25% + }, 'Dynamically becoming a deferred timeline'); +</script> + +<!-- ViewTimelines and ScrollTimelines --> + +<template id=view_scroll_timeline_defer> + <div style="scroll-timeline: t1 defer"> + <div class=target>Test1</div> + <div class="timeline defer"> + <div class=target>Test2</div> + <div class=scroller style="scroll-timeline: t1 ancestor;"> + <div class="content timeline ancestor" style="view-timeline-inset: 0px 50px"></div> + </div> + </div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, view_scroll_timeline_defer); + let scroller = main.querySelector('.scroller'); + let targets = main.querySelectorAll('.target'); + await scrollTop(scroller, 350); + + // Attached to ScrollTimeline: + // Range: [0, 700] + // 350 => 50% + assert_equals(getComputedStyle(targets[0]).width, '100px'); + + // Attached to ViewTimeline: + // Range: [200, 500] + [50, 0] (inset) = [250, 500] + // 350 => 40% + assert_equals(getComputedStyle(targets[1]).width, '80px'); + }, 'Mixing deferred scroll and view-timelines'); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/view-timeline-axis-computed.html b/testing/web-platform/tests/scroll-animations/css/view-timeline-axis-computed.html new file mode 100644 index 0000000000..f4649dab04 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-axis-computed.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#view-timeline-axis"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/css/support/computed-testcommon.js"></script> +</head> +<style> + #outer { view-timeline-axis: block, inline; } + #target { view-timeline-axis: vertical; } +</style> +<div id=outer> + <div id=target></div> +</div> +<script> +test_computed_value('view-timeline-axis', 'initial', 'block'); +test_computed_value('view-timeline-axis', 'inherit', 'block, inline'); +test_computed_value('view-timeline-axis', 'unset', 'block'); +test_computed_value('view-timeline-axis', 'revert', 'block'); +test_computed_value('view-timeline-axis', 'block'); +test_computed_value('view-timeline-axis', 'inline'); +test_computed_value('view-timeline-axis', 'vertical'); +test_computed_value('view-timeline-axis', 'horizontal'); +test_computed_value('view-timeline-axis', 'block, inline'); +test_computed_value('view-timeline-axis', 'inline, block'); +test_computed_value('view-timeline-axis', 'block, vertical, horizontal, inline'); +test_computed_value('view-timeline-axis', 'inline, inline, inline, inline'); + +test(() => { + let style = getComputedStyle(document.getElementById('target')); + assert_not_equals(Array.from(style).indexOf('view-timeline-axis'), -1); +}, 'The view-timeline-axis property shows up in CSSStyleDeclaration enumeration'); + +test(() => { + let style = document.getElementById('target').style; + assert_not_equals(style.cssText.indexOf('view-timeline-axis'), -1); +}, 'The view-timeline-axis property shows up in CSSStyleDeclaration.cssText'); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/view-timeline-axis-parsing.html b/testing/web-platform/tests/scroll-animations/css/view-timeline-axis-parsing.html new file mode 100644 index 0000000000..ffcc36c320 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-axis-parsing.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#view-timeline-axis"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/css/support/parsing-testcommon.js"></script> +</head> +<div id="target"></div> +<script> +test_valid_value('view-timeline-axis', 'initial'); +test_valid_value('view-timeline-axis', 'inherit'); +test_valid_value('view-timeline-axis', 'unset'); +test_valid_value('view-timeline-axis', 'revert'); + +test_valid_value('view-timeline-axis', 'block'); +test_valid_value('view-timeline-axis', 'inline'); +test_valid_value('view-timeline-axis', 'vertical'); +test_valid_value('view-timeline-axis', 'horizontal'); +test_valid_value('view-timeline-axis', 'block, inline'); +test_valid_value('view-timeline-axis', 'inline, block'); +test_valid_value('view-timeline-axis', 'block, vertical, horizontal, inline'); +test_valid_value('view-timeline-axis', 'inline, inline, inline, inline'); + +test_invalid_value('view-timeline-axis', 'abc'); +test_invalid_value('view-timeline-axis', '10px'); +test_invalid_value('view-timeline-axis', 'auto'); +test_invalid_value('view-timeline-axis', 'none'); +test_invalid_value('view-timeline-axis', 'block inline'); +test_invalid_value('view-timeline-axis', 'block / inline'); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/view-timeline-dynamic.html b/testing/web-platform/tests/scroll-animations/css/view-timeline-dynamic.html new file mode 100644 index 0000000000..207c8c4e22 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-dynamic.html @@ -0,0 +1,193 @@ +<!DOCTYPE html> +<title>Changes to view-timeline are reflected in dependent elements</title> +<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#view-timeline-shorthand"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="support/testcommon.js"></script> +<style> + @keyframes anim { + from { z-index: 0; } + to { z-index: 100; } + } + .scroller { + overflow: hidden; + width: 100px; + height: 100px; + view-timeline: t1 defer; + } + .scroller > div { + height: 100px; + } + #target { + height: 0px; + z-index: -1; + } +</style> +<main id=main></main> +<script> + setup(assert_implements_animation_timeline); + + function inflate(t, template) { + t.add_cleanup(() => main.replaceChildren()); + main.append(template.content.cloneNode(true)); + main.offsetTop; + } + async function scrollTop(e, value) { + e.scrollTop = value; + await waitForNextFrame(); + } + async function scrollLeft(e, value) { + e.scrollLeft = value; + await waitForNextFrame(); + } +</script> + +<template id=dynamic_view_timeline_name> + <style> + .timeline { + view-timeline: t1 ancestor; + } + #target { + animation: anim 1s linear; + animation-timeline: t1; + } + </style> + <div id=scroller class=scroller> + <div id=div75></div> + <div id=div25></div> + <div id=div_before></div> + <div id=target></div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, dynamic_view_timeline_name); + + await scrollTop(scroller, 50); + + // scrollTop=50 is 75% for div75. + div75.classList.add('timeline'); + await waitForCSSScrollTimelineStyle(); + assert_equals(getComputedStyle(target).zIndex, '75', 'div75'); + + // Identical timelines in div75 and div25 creates an ambiguity. + div25.classList.add('timeline'); + await waitForCSSScrollTimelineStyle(); + assert_equals(getComputedStyle(target).zIndex, '-1', 'ambiguous'); + // Removing the timeline from div75 unambiguously links div25 to the + // timeline, making scrollTop=50 at 25% for div25. + div75.classList.remove('timeline'); + await waitForCSSScrollTimelineStyle(); + assert_equals(getComputedStyle(target).zIndex, '25', 'div25'); + + // scrollTop=50 is before the timeline start for div_before. + div25.classList.remove('timeline'); + div_before.classList.add('timeline'); + await waitForCSSScrollTimelineStyle(); + assert_equals(getComputedStyle(target).zIndex, '-1', 'ahead of div_before'); + // Scroll to 25% (for div_before) to verify that we're linked to that + // timeline. + await scrollTop(scroller, 150); + assert_equals(getComputedStyle(target).zIndex, '25', 'div_before'); + + // Linking the timeline back to div25 verifies that the new scrollTop=150 is + // actually at 75%. + div_before.classList.remove('timeline'); + div25.classList.add('timeline'); + await waitForCSSScrollTimelineStyle(); + assert_equals(getComputedStyle(target).zIndex, '75', 'div25 again'); + }, 'Dynamically changing view-timeline-name'); +</script> + +<template id=dynamic_view_timeline_axis> + <style> + #timeline { + width: 100px; + height: 100px; + margin: 100px; + view-timeline: t1 ancestor; + } + #target { + animation: anim 1s linear; + animation-timeline: t1; + } + </style> + <div id=scroller class=scroller> + <div id=timeline style="background: red;"></div> + <div id=target></div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, dynamic_view_timeline_axis); + + await scrollTop(scroller, 50); // 25% (vertical) + await scrollLeft(scroller, 20); // 10% (horizontal) + + assert_equals(getComputedStyle(target).zIndex, '25', 'vertical'); + timeline.style.viewTimelineAxis = 'horizontal'; + await waitForCSSScrollTimelineStyle(); + assert_equals(getComputedStyle(target).zIndex, '10', 'horizontal'); + }, 'Dynamically changing view-timeline-axis'); +</script> + +<template id=dynamic_view_timeline_inset> + <style> + #timeline { + width: 100px; + height: 100px; + margin: 100px; + view-timeline: t1 ancestor; + } + #target { + animation: anim 1s linear; + animation-timeline: t1; + } + </style> + <div id=scroller class=scroller> + <div id=timeline style="background: red;"></div> + <div id=target></div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, dynamic_view_timeline_inset); + + await scrollTop(scroller, 50); // 25% (without inset). + + assert_equals(getComputedStyle(target).zIndex, '25', 'without inset'); + timeline.style.viewTimelineInset = '0px 50px'; + await waitForCSSScrollTimelineStyle(); + assert_equals(getComputedStyle(target).zIndex, '0', 'with inset'); + }, 'Dynamically changing view-timeline-inset'); +</script> + +<template id=timeline_display_none> + <style> + #timeline { + view-timeline: t1 ancestor; + } + #target { + animation: anim 1s linear; + animation-timeline: t1; + } + </style> + <div id=scroller class=scroller> + <div></div> + <div id=timeline></div> + <div id=target></div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, timeline_display_none); + + await scrollTop(scroller, 50); + assert_equals(getComputedStyle(target).zIndex, '25', 'display:block'); + timeline.style.display = 'none'; + await waitForNextFrame(); + // The timeline became inactive. + assert_equals(getComputedStyle(target).zIndex, '-1', 'display:none'); + }, 'Element with view-timeline becoming display:none'); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/view-timeline-inset-animation.html b/testing/web-platform/tests/scroll-animations/css/view-timeline-inset-animation.html new file mode 100644 index 0000000000..a7e807c2e8 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-inset-animation.html @@ -0,0 +1,769 @@ +<!DOCTYPE html> +<title>Animations using view-timeline-inset</title> +<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1"> +<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#propdef-view-timeline-inset"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="support/testcommon.js"></script> +<style> + @keyframes anim { + from { z-index: 0; } + to { z-index: 100; } + } + #scroller { + overflow: hidden; + width: 80px; + height: 100px; + } + #target { + margin: 150px; + width: 50px; + height: 50px; + z-index: -1; + } +</style> +<main id=main></main> +<script> + setup(assert_implements_animation_timeline); + + function inflate(t, template) { + t.add_cleanup(() => main.replaceChildren()); + main.append(template.content.cloneNode(true)); + } + async function scrollTop(e, value) { + e.scrollTop = value; + await waitForNextFrame(); + } + async function scrollLeft(e, value) { + e.scrollLeft = value; + await waitForNextFrame(); + } + async function assertValueAt(scroller, target, args) { + if (args.scrollTop !== undefined) + await scrollTop(scroller, args.scrollTop); + if (args.scrollLeft !== undefined) + await scrollLeft(scroller, args.scrollLeft); + assert_equals(getComputedStyle(target).zIndex, args.expected.toString()); + } +</script> + +<!-- + Explanation of scroll positions + =============================== + + Please note the following: + + - The scroller has a width x height of 80x100px. + - The content is 50x50px with a 150px margin on all sides. + In other words, the size of the scroller content is 200x200px. + + This means that, for vertical direction scrolling, assuming no insets: + + - The start offset is 50px (scroller height + 50px is 150px, which consumes + exactly the margin of the content). + - The end offset is 200px (this is where the bottom edge of the scroller has + just cleared the content). + - The halfway point is (50px + 200px) / 2 = 125px. + + For horizontal direction scrolling, assuming no insets: + + - The start offset is 70px (scroller width + 70px is 150px, which consumes + exactly the margin of the content). + - The end offset is 200px (this is where the left edge of the scroller has + just cleared the content). + - The halfway point is (70px + 200px) / 2 = 135px. + + The start and end insets will adjust the start and end offsets accordingly, + and the expectations in this file explicitly write out those adjustments. + For example, if the start offset is normally 50px, but there's an inset of + 10px, we'll expect 50px + 10px rather than 60px. + + Halfway-point expectations write out the adjustment from the "normal" + halfway-point, e.g. for start-inset:10px and end-inset:20px, we expect + "125px + 5px" since (20-10)/2 == 5. + + Finally, note that for right-to-left and bottom-to-top scrolling directions + scroll offsets go the in the negative direction. This is why some expectations + negate all the offsets. +--> + +<template id=test_one_value> + <style> + #target { + view-timeline: t1; + view-timeline-inset: 10px; + animation: anim 1s linear; + animation-timeline: t1; + } + </style> + <div id=scroller class=vertical> + <div id=target></div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, test_one_value); + await assertValueAt(scroller, target, { scrollTop:50, expected:-1 }); + await assertValueAt(scroller, target, { scrollTop:50 + 10, expected:0 }); // 0% + await assertValueAt(scroller, target, { scrollTop:125 + 0, expected:50 }); // 50% + await assertValueAt(scroller, target, { scrollTop:200 - 10, expected:100 }); // 100% + await assertValueAt(scroller, target, { scrollTop:200, expected:-1 }); + }, 'view-timeline-inset with one value'); +</script> + +<template id=test_two_values> + <style> + #target { + view-timeline: t1; + view-timeline-inset: 10px 20px; + animation: anim 1s linear; + animation-timeline: t1; + } + </style> + <div id=scroller class=vertical> + <div id=target></div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, test_two_values); + await assertValueAt(scroller, target, { scrollTop:50, expected:-1 }); + await assertValueAt(scroller, target, { scrollTop:50 + 20, expected:0 }); // 0% + await assertValueAt(scroller, target, { scrollTop:125 + 5, expected:50 }); // 50% + await assertValueAt(scroller, target, { scrollTop:200 - 10, expected:100 }); // 100% + await assertValueAt(scroller, target, { scrollTop:200, expected:-1 }); + }, 'view-timeline-inset with two values'); +</script> + +<template id=test_em_values> + <style> + #target { + font-size: 10px; + view-timeline: t1; + view-timeline-inset: 10px 2em; + animation: anim 1s linear; + animation-timeline: t1; + } + </style> + <div id=scroller class=vertical> + <div id=target></div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, test_em_values); + await assertValueAt(scroller, target, { scrollTop:50, expected:-1 }); + await assertValueAt(scroller, target, { scrollTop:50 + 20, expected:0 }); // 0% + await assertValueAt(scroller, target, { scrollTop:125 + 5, expected:50 }); // 50% + await assertValueAt(scroller, target, { scrollTop:200 - 10, expected:100 }); // 100% + await assertValueAt(scroller, target, { scrollTop:200, expected:-1 }); + }, 'view-timeline-inset with em values'); +</script> + +<template id=test_percentage_values> + <style> + #target { + font-size: 10px; + view-timeline: t1; + view-timeline-inset: calc(5px + max(1%, 5%)) 20%; + animation: anim 1s linear; + animation-timeline: t1; + } + </style> + <div id=scroller class=vertical> + <div id=target></div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, test_percentage_values); + await assertValueAt(scroller, target, { scrollTop:50, expected:-1 }); + await assertValueAt(scroller, target, { scrollTop:50 + 20, expected:0 }); // 0% + await assertValueAt(scroller, target, { scrollTop:125 + 5, expected:50 }); // 50% + await assertValueAt(scroller, target, { scrollTop:200 - 10, expected:100 }); // 100% + await assertValueAt(scroller, target, { scrollTop:200, expected:-1 }); + }, 'view-timeline-inset with percentage values'); +</script> + +<template id=test_outset> + <style> + #target { + view-timeline: t1; + view-timeline-inset: -10px -20px; + animation: anim 1s linear; + animation-timeline: t1; + } + </style> + <div id=scroller class=vertical> + <div id=target></div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, test_outset); + await assertValueAt(scroller, target, { scrollTop:20, expected:-1 }); + await assertValueAt(scroller, target, { scrollTop:50 - 20, expected:0 }); // 0% + await assertValueAt(scroller, target, { scrollTop:125 - 5, expected:50 }); // 50% + await assertValueAt(scroller, target, { scrollTop:200 + 10, expected:100 }); // 100% + await assertValueAt(scroller, target, { scrollTop:220, expected:-1 }); + }, 'view-timeline-inset with negative values'); +</script> + +<template id=test_horizontal> + <style> + #target { + view-timeline: t1 horizontal; + view-timeline-inset: 10px 20px; + animation: anim 1s linear; + animation-timeline: t1; + } + </style> + <div id=scroller> + <div id=target></div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, test_horizontal); + await assertValueAt(scroller, target, { scrollLeft:20, expected:-1 }); + await assertValueAt(scroller, target, { scrollLeft:70 + 20, expected:0 }); // 0% + await assertValueAt(scroller, target, { scrollLeft:135 + 5, expected:50 }); // 50% + await assertValueAt(scroller, target, { scrollLeft:200 - 10, expected:100 }); // 100% + await assertValueAt(scroller, target, { scrollLeft:200, expected:-1 }); + }, 'view-timeline-inset with horizontal scroller'); +</script> + +<template id=test_block> + <style> + #target { + view-timeline: t1 block; + view-timeline-inset: 10px 20px; + animation: anim 1s linear; + animation-timeline: t1; + } + </style> + <div id=scroller> + <div id=target></div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, test_block); + await assertValueAt(scroller, target, { scrollTop:50, expected:-1 }); + await assertValueAt(scroller, target, { scrollTop:50 + 20, expected:0 }); // 0% + await assertValueAt(scroller, target, { scrollTop:125 + 5, expected:50 }); // 50% + await assertValueAt(scroller, target, { scrollTop:200 - 10, expected:100 }); // 100% + await assertValueAt(scroller, target, { scrollTop:200, expected:-1 }); + }, 'view-timeline-inset with block scroller'); +</script> + +<template id=test_inline> + <style> + #target { + view-timeline: t1 inline; + view-timeline-inset: 10px 20px; + animation: anim 1s linear; + animation-timeline: t1; + } + </style> + <div id=scroller> + <div id=target></div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, test_inline); + await assertValueAt(scroller, target, { scrollLeft:20, expected:-1 }); + await assertValueAt(scroller, target, { scrollLeft:70 + 20, expected:0 }); // 0% + await assertValueAt(scroller, target, { scrollLeft:135 + 5, expected:50 }); // 50% + await assertValueAt(scroller, target, { scrollLeft:200 - 10, expected:100 }); // 100% + await assertValueAt(scroller, target, { scrollLeft:200, expected:-1 }); + }, 'view-timeline-inset with inline scroller'); +</script> + +<template id=test_auto_block> + <style> + #scroller { + scroll-padding-block: 10px 20px; + } + #target { + view-timeline: t1 block; + view-timeline-inset: auto auto; + animation: anim 1s linear; + animation-timeline: t1; + } + </style> + <div id=scroller> + <div id=target></div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, test_auto_block); + await assertValueAt(scroller, target, { scrollTop:50, expected:-1 }); + await assertValueAt(scroller, target, { scrollTop:50 + 20, expected:0 }); // 0% + await assertValueAt(scroller, target, { scrollTop:125 + 5, expected:50 }); // 50% + await assertValueAt(scroller, target, { scrollTop:200 - 10, expected:100 }); // 100% + await assertValueAt(scroller, target, { scrollTop:200, expected:-1 }); + }, 'view-timeline-inset:auto, block'); +</script> + +<template id=test_auto_block_vertical_lr> + <style> + #scroller { + scroll-padding-block: 10px 20px; + writing-mode: vertical-lr; + } + #target { + view-timeline: t1 block; + view-timeline-inset: auto auto; + animation: anim 1s linear; + animation-timeline: t1; + } + </style> + <div id=scroller> + <div id=target></div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, test_auto_block_vertical_lr); + await assertValueAt(scroller, target, { scrollLeft:20, expected:-1 }); + await assertValueAt(scroller, target, { scrollLeft:70 + 20, expected:0 }); // 0% + await assertValueAt(scroller, target, { scrollLeft:135 + 5, expected:50 }); // 50% + await assertValueAt(scroller, target, { scrollLeft:200 - 10, expected:100 }); // 100% + await assertValueAt(scroller, target, { scrollLeft:200, expected:-1 }); + }, 'view-timeline-inset:auto, block, vertical-lr'); +</script> + +<template id=test_auto_block_vertical_rl> + <style> + #scroller { + scroll-padding-block: 10px 20px; + writing-mode: vertical-rl; + } + #target { + view-timeline: t1 block; + view-timeline-inset: auto auto; + animation: anim 1s linear; + animation-timeline: t1; + } + </style> + <div id=scroller> + <div id=target></div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, test_auto_block_vertical_rl); + // Note: this represents horizontal scrolling from right to left. + await assertValueAt(scroller, target, { scrollLeft:-20, expected:-1 }); + await assertValueAt(scroller, target, { scrollLeft:-(70 + 20), expected:0 }); // 0% + await assertValueAt(scroller, target, { scrollLeft:-(135 + 5), expected:50 }); // 50% + await assertValueAt(scroller, target, { scrollLeft:-(200 - 10), expected:100 }); // 100% + await assertValueAt(scroller, target, { scrollLeft:-200, expected:-1 }); + }, 'view-timeline-inset:auto, block, vertical-rl'); +</script> + +<template id=test_auto_inline> + <style> + #scroller { + scroll-padding-inline: 10px 20px; + } + #target { + view-timeline: t1 inline; + view-timeline-inset: auto auto; + animation: anim 1s linear; + animation-timeline: t1; + } + </style> + <div id=scroller> + <div id=target></div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, test_auto_inline); + await assertValueAt(scroller, target, { scrollLeft:20, expected:-1 }); + await assertValueAt(scroller, target, { scrollLeft:70 + 20, expected:0 }); // 0% + await assertValueAt(scroller, target, { scrollLeft:135 + 5, expected:50 }); // 50% + await assertValueAt(scroller, target, { scrollLeft:200 - 10, expected:100 }); // 100% + await assertValueAt(scroller, target, { scrollLeft:200, expected:-1 }); + }, 'view-timeline-inset:auto, inline'); +</script> + +<template id=test_auto_inline_vertical_rl> + <style> + #scroller { + scroll-padding-inline: 10px 20px; + writing-mode: vertical-rl; + } + #target { + view-timeline: t1 inline; + view-timeline-inset: auto auto; + animation: anim 1s linear; + animation-timeline: t1; + } + </style> + <div id=scroller> + <div id=target></div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, test_auto_inline_vertical_rl); + await assertValueAt(scroller, target, { scrollTop:50, expected:-1 }); + await assertValueAt(scroller, target, { scrollTop:50 + 20, expected:0 }); // 0% + await assertValueAt(scroller, target, { scrollTop:125 + 5, expected:50 }); // 50% + await assertValueAt(scroller, target, { scrollTop:200 - 10, expected:100 }); // 100% + await assertValueAt(scroller, target, { scrollTop:200, expected:-1 }); + }, 'view-timeline-inset:auto, inline, vertical-rl'); +</script> + +<template id=test_auto_inline_vertical_lr> + <style> + #scroller { + scroll-padding-inline: 10px 20px; + writing-mode: vertical-lr; + } + #target { + view-timeline: t1 inline; + view-timeline-inset: auto auto; + animation: anim 1s linear; + animation-timeline: t1; + } + </style> + <div id=scroller> + <div id=target></div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, test_auto_inline_vertical_lr); + await assertValueAt(scroller, target, { scrollTop:50, expected:-1 }); + await assertValueAt(scroller, target, { scrollTop:50 + 20, expected:0 }); // 0% + await assertValueAt(scroller, target, { scrollTop:125 + 5, expected:50 }); // 50% + await assertValueAt(scroller, target, { scrollTop:200 - 10, expected:100 }); // 100% + await assertValueAt(scroller, target, { scrollTop:200, expected:-1 }); + }, 'view-timeline-inset:auto, inline, vertical-lr'); +</script> + +<template id=test_auto_inline_rtl> + <style> + #scroller { + scroll-padding-inline: 10px 20px; + direction: rtl; + } + #target { + view-timeline: t1 inline; + view-timeline-inset: auto auto; + animation: anim 1s linear; + animation-timeline: t1; + } + </style> + <div id=scroller> + <div id=target></div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, test_auto_inline_rtl); + await assertValueAt(scroller, target, { scrollLeft:-20, expected:-1 }); + await assertValueAt(scroller, target, { scrollLeft:-(70 + 20), expected:0 }); // 0% + await assertValueAt(scroller, target, { scrollLeft:-(135 + 5), expected:50 }); // 50% + await assertValueAt(scroller, target, { scrollLeft:-(200 - 10), expected:100 }); // 100% + await assertValueAt(scroller, target, { scrollLeft:-200, expected:-1 }); + }, 'view-timeline-inset:auto, inline, rtl'); +</script> + +<template id=test_auto_inline_vertical_rl_rtl> + <style> + #scroller { + scroll-padding-inline: 10px 20px; + writing-mode: vertical-rl; + direction: rtl; + } + #target { + view-timeline: t1 inline; + view-timeline-inset: auto auto; + animation: anim 1s linear; + animation-timeline: t1; + } + </style> + <div id=scroller> + <div id=target></div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, test_auto_inline_vertical_rl_rtl); + await assertValueAt(scroller, target, { scrollTop:-50, expected:-1 }); + await assertValueAt(scroller, target, { scrollTop:-(50 + 20), expected:0 }); // 0% + await assertValueAt(scroller, target, { scrollTop:-(125 + 5), expected:50 }); // 50% + await assertValueAt(scroller, target, { scrollTop:-(200 - 10), expected:100 }); // 100% + await assertValueAt(scroller, target, { scrollTop:-200, expected:-1 }); + }, 'view-timeline-inset:auto, inline, vertical-rl, rtl'); +</script> + +<template id=test_auto_inline_vertical_lr_rtl> + <style> + #scroller { + scroll-padding-inline: 10px 20px; + writing-mode: vertical-lr; + direction: rtl; + } + #target { + view-timeline: t1 inline; + view-timeline-inset: auto auto; + animation: anim 1s linear; + animation-timeline: t1; + } + </style> + <div id=scroller> + <div id=target></div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, test_auto_inline_vertical_lr_rtl); + await assertValueAt(scroller, target, { scrollTop:-50, expected:-1 }); + await assertValueAt(scroller, target, { scrollTop:-(50 + 20), expected:0 }); // 0% + await assertValueAt(scroller, target, { scrollTop:-(125 + 5), expected:50 }); // 50% + await assertValueAt(scroller, target, { scrollTop:-(200 - 10), expected:100 }); // 100% + await assertValueAt(scroller, target, { scrollTop:-200, expected:-1 }); + }, 'view-timeline-inset:auto, inline, vertical-lr, rtl'); +</script> + +<template id=test_auto_vertical> + <style> + #scroller { + scroll-padding-block: 10px 20px; + } + #target { + view-timeline: t1 vertical; + view-timeline-inset: auto auto; + animation: anim 1s linear; + animation-timeline: t1; + } + </style> + <div id=scroller> + <div id=target></div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, test_auto_vertical); + await assertValueAt(scroller, target, { scrollTop:50, expected:-1 }); + await assertValueAt(scroller, target, { scrollTop:50 + 20, expected:0 }); // 0% + await assertValueAt(scroller, target, { scrollTop:125 + 5, expected:50 }); // 50% + await assertValueAt(scroller, target, { scrollTop:200 - 10, expected:100 }); // 100% + await assertValueAt(scroller, target, { scrollTop:200, expected:-1 }); + }, 'view-timeline-inset:auto, vertical'); +</script> + +<template id=test_auto_vertical_vertical_rl> + <style> + #scroller { + scroll-padding-inline: 10px 20px; + writing-mode: vertical-rl; + } + #target { + view-timeline: t1 vertical; + view-timeline-inset: auto auto; + animation: anim 1s linear; + animation-timeline: t1; + } + </style> + <div id=scroller> + <div id=target></div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, test_auto_vertical_vertical_rl); + await assertValueAt(scroller, target, { scrollTop:50, expected:-1 }); + await assertValueAt(scroller, target, { scrollTop:50 + 20, expected:0 }); // 0% + await assertValueAt(scroller, target, { scrollTop:125 + 5, expected:50 }); // 50% + await assertValueAt(scroller, target, { scrollTop:200 - 10, expected:100 }); // 100% + await assertValueAt(scroller, target, { scrollTop:200, expected:-1 }); + }, 'view-timeline-inset:auto, vertical, vertical-rl'); +</script> + +<template id=test_auto_vertical_vertical_rl_rtl> + <style> + #scroller { + scroll-padding-inline: 10px 20px; + writing-mode: vertical-rl; + direction: rtl; + } + #target { + view-timeline: t1 vertical; + view-timeline-inset: auto auto; + animation: anim 1s linear; + animation-timeline: t1; + } + </style> + <div id=scroller> + <div id=target></div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, test_auto_vertical_vertical_rl_rtl); + await assertValueAt(scroller, target, { scrollTop:-50, expected:-1 }); + await assertValueAt(scroller, target, { scrollTop:-(50 + 20), expected:0 }); // 0% + await assertValueAt(scroller, target, { scrollTop:-(125 + 5), expected:50 }); // 50% + await assertValueAt(scroller, target, { scrollTop:-(200 - 10), expected:100 }); // 100% + await assertValueAt(scroller, target, { scrollTop:-200, expected:-1 }); + }, 'view-timeline-inset:auto, vertical, vertical-rl, rtl'); +</script> + +<template id=test_auto_horizontal> + <style> + #scroller { + scroll-padding-inline: 10px 20px; + } + #target { + view-timeline: t1 horizontal; + view-timeline-inset: auto auto; + animation: anim 1s linear; + animation-timeline: t1; + } + </style> + <div id=scroller> + <div id=target></div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, test_auto_horizontal); + await assertValueAt(scroller, target, { scrollLeft:20, expected:-1 }); + await assertValueAt(scroller, target, { scrollLeft:70 + 20, expected:0 }); // 0% + await assertValueAt(scroller, target, { scrollLeft:135 + 5, expected:50 }); // 50% + await assertValueAt(scroller, target, { scrollLeft:200 - 10, expected:100 }); // 100% + await assertValueAt(scroller, target, { scrollLeft:200, expected:-1 }); + }, 'view-timeline-inset:auto, horizontal'); +</script> + +<template id=test_auto_horizontal_rtl> + <style> + #scroller { + scroll-padding-inline: 10px 20px; + direction: rtl; + } + #target { + view-timeline: t1 horizontal; + view-timeline-inset: auto auto; + animation: anim 1s linear; + animation-timeline: t1; + } + </style> + <div id=scroller> + <div id=target></div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, test_auto_horizontal_rtl); + await assertValueAt(scroller, target, { scrollLeft:-20, expected:-1 }); + await assertValueAt(scroller, target, { scrollLeft:-(70 + 20), expected:0 }); // 0% + await assertValueAt(scroller, target, { scrollLeft:-(135 + 5), expected:50 }); // 50% + await assertValueAt(scroller, target, { scrollLeft:-(200 - 10), expected:100 }); // 100% + await assertValueAt(scroller, target, { scrollLeft:-200, expected:-1 }); + }, 'view-timeline-inset:auto, horizontal, rtl'); +</script> + +<template id=test_auto_horizontal_vertical_lr> + <style> + #scroller { + scroll-padding-block: 10px 20px; + writing-mode: vertical-lr; + } + #target { + view-timeline: t1 horizontal; + view-timeline-inset: auto auto; + animation: anim 1s linear; + animation-timeline: t1; + } + </style> + <div id=scroller> + <div id=target></div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, test_auto_horizontal_vertical_lr); + await assertValueAt(scroller, target, { scrollLeft:20, expected:-1 }); + await assertValueAt(scroller, target, { scrollLeft:70 + 20, expected:0 }); // 0% + await assertValueAt(scroller, target, { scrollLeft:135 + 5, expected:50 }); // 50% + await assertValueAt(scroller, target, { scrollLeft:200 - 10, expected:100 }); // 100% + await assertValueAt(scroller, target, { scrollLeft:200, expected:-1 }); + }, 'view-timeline-inset:auto, horizontal, vertical-lr'); +</script> + +<template id=test_auto_horizontal_vertical_rl> + <style> + #scroller { + scroll-padding-block: 10px 20px; + writing-mode: vertical-rl; + } + #target { + view-timeline: t1 horizontal; + view-timeline-inset: auto auto; + animation: anim 1s linear; + animation-timeline: t1; + } + </style> + <div id=scroller> + <div id=target></div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, test_auto_horizontal_vertical_rl); + await assertValueAt(scroller, target, { scrollLeft:-20, expected:-1 }); + await assertValueAt(scroller, target, { scrollLeft:-(70 + 20), expected:0 }); // 0% + await assertValueAt(scroller, target, { scrollLeft:-(135 + 5), expected:50 }); // 50% + await assertValueAt(scroller, target, { scrollLeft:-(200 - 10), expected:100 }); // 100% + await assertValueAt(scroller, target, { scrollLeft:-200, expected:-1 }); + }, 'view-timeline-inset:auto, horizontal, vertical-rl'); +</script> + +<template id=test_auto_mix> + <style> + #scroller { + font-size: 10px; + scroll-padding-block: 50px calc(10% + 1em); + } + #target { + view-timeline: t1; + view-timeline-inset: 10% auto; + animation: anim 1s linear; + animation-timeline: t1; + } + </style> + <div id=scroller> + <div id=target></div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, test_auto_mix); + // Note: 10% of scroller height 100px is 10px, and 1em with font-size:10px + // is also 10px. Hence we expect the end inset specified as calc(10% + 1em) + // to be 20px. + await assertValueAt(scroller, target, { scrollTop:50, expected:-1 }); + await assertValueAt(scroller, target, { scrollTop:50 + 20, expected:0 }); // 0% + await assertValueAt(scroller, target, { scrollTop:125 + 5, expected:50 }); // 50% + await assertValueAt(scroller, target, { scrollTop:200 - 10, expected:100 }); // 100% + await assertValueAt(scroller, target, { scrollTop:200, expected:-1 }); + }, 'view-timeline-inset:auto, mix'); +</script> + +<!-- + TODO: How to test view-timeline:auto + scroll-padding:auto? The UA may + in theory use any value in that case. + + https://drafts.csswg.org/css-scroll-snap-1/#valdef-scroll-padding-auto +--> diff --git a/testing/web-platform/tests/scroll-animations/css/view-timeline-inset-computed.html b/testing/web-platform/tests/scroll-animations/css/view-timeline-inset-computed.html new file mode 100644 index 0000000000..d9e1c9d790 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-inset-computed.html @@ -0,0 +1,41 @@ +<!DOCTYPE html> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#view-timeline-inset"> +<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/7243"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/css/support/computed-testcommon.js"></script> +<style> + #outer { font-size:10px; } + #outer { view-timeline-inset: 1px 2px, auto 3px; } + #target { view-timeline-inset: 42px; } +</style> +<div id=outer> + <div id=target></div> +</div> +<script> +test_computed_value('view-timeline-inset', 'initial', 'auto'); +test_computed_value('view-timeline-inset', 'inherit', '1px 2px, auto 3px'); +test_computed_value('view-timeline-inset', 'unset', 'auto'); +test_computed_value('view-timeline-inset', 'revert', 'auto'); +test_computed_value('view-timeline-inset', '1px'); +test_computed_value('view-timeline-inset', '1%'); +test_computed_value('view-timeline-inset', 'calc(1% + 1px)'); +test_computed_value('view-timeline-inset', '1px 2px'); +test_computed_value('view-timeline-inset', '1px 2em', '1px 20px'); +test_computed_value('view-timeline-inset', 'calc(1px + 1em) 2px', '11px 2px'); +test_computed_value('view-timeline-inset', '1px 2px, 3px 4px'); +test_computed_value('view-timeline-inset', '1px auto, auto 4px'); +test_computed_value('view-timeline-inset', '1px, 2px, 3px'); +test_computed_value('view-timeline-inset', '1px 1px, 2px 3px', '1px, 2px 3px'); +test_computed_value('view-timeline-inset', 'auto auto, auto auto', 'auto, auto'); + +test(() => { + let style = getComputedStyle(document.getElementById('target')); + assert_not_equals(Array.from(style).indexOf('view-timeline-inset'), -1); +}, 'The view-timeline-inset property shows up in CSSStyleDeclaration enumeration'); + +test(() => { + let style = document.getElementById('target').style; + assert_not_equals(style.cssText.indexOf('view-timeline-inset'), -1); +}, 'The view-timeline-inset property shows up in CSSStyleDeclaration.cssText'); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/view-timeline-inset-parsing.html b/testing/web-platform/tests/scroll-animations/css/view-timeline-inset-parsing.html new file mode 100644 index 0000000000..d502b13593 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-inset-parsing.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#view-timeline-inset"> +<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/7243"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/css/support/parsing-testcommon.js"></script> +<div id="target"></div> +<script> +test_valid_value('view-timeline-inset', 'initial'); +test_valid_value('view-timeline-inset', 'inherit'); +test_valid_value('view-timeline-inset', 'unset'); +test_valid_value('view-timeline-inset', 'revert'); + +test_valid_value('view-timeline-inset', '1px'); +test_valid_value('view-timeline-inset', '1px 2px'); +test_valid_value('view-timeline-inset', '1px 2em'); +test_valid_value('view-timeline-inset', 'calc(1em + 1px) 2px'); +test_valid_value('view-timeline-inset', '1px 2px, 3px 4px'); +test_valid_value('view-timeline-inset', '1px auto, auto 4px'); +test_valid_value('view-timeline-inset', '1px, 2px, 3px'); +test_valid_value('view-timeline-inset', '1px 1px, 2px 3px', '1px, 2px 3px'); +test_valid_value('view-timeline-inset', 'auto auto, auto auto', 'auto, auto'); + +test_invalid_value('view-timeline-inset', 'none'); +test_invalid_value('view-timeline-inset', 'foo bar'); +test_invalid_value('view-timeline-inset', '"foo" "bar"'); +test_invalid_value('view-timeline-inset', 'rgb(1, 2, 3)'); +test_invalid_value('view-timeline-inset', '#fefefe'); +test_invalid_value('view-timeline-inset', '1px 2px 3px'); +test_invalid_value('view-timeline-inset', '1px 2px auto'); +test_invalid_value('view-timeline-inset', 'auto 2px 3px'); +test_invalid_value('view-timeline-inset', 'auto auto auto'); +test_invalid_value('view-timeline-inset', '1px / 2px'); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/view-timeline-keyframe-boundary-interpolation.html b/testing/web-platform/tests/scroll-animations/css/view-timeline-keyframe-boundary-interpolation.html new file mode 100644 index 0000000000..04eb648949 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-keyframe-boundary-interpolation.html @@ -0,0 +1,121 @@ +<!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"> + @keyframes anim { + cover 0% { /* resolves to -100% */ + opacity: 0; + transform: none; + margin-left: 0px; + /* missing margin-right -- requires neutral keyframe at 0% */ + } + cover 100% { /* resolves to 200% */ + opacity: 1; + transform: translateX(300px); + margin-right: 0px; + /* missing margin-left -- requires neutral keyframe at 100% */ + } + } + #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; + animation: anim auto both linear; + animation-timeline: t1; + animation-range-start: contain 0%; + animation-range-end: contain 100%; + view-timeline: t1 block; + } +</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); + } + + function assert_translate_x_equals(expected, errorMessage) { + const style = getComputedStyle(target).transform; + const regex = /matrix\(([^\)]*)\)/; + const captureGroupIndex = 1; + const translateIndex = 4; + const match = style.match(regex)[captureGroupIndex]; + const translateX = parseFloat(match.split(',')[translateIndex].trim()); + assert_approx_equals(translateX, expected, 1e-6, errorMessage); + } + + function assert_property_equals(property, expected, errorMessage) { + const value = parseFloat(getComputedStyle(target)[property]); + assert_approx_equals(value, expected, 1e-6, errorMessage); + } + + promise_test(async t => { + await waitForNextFrame(); + const anims = document.getAnimations(); + assert_equals(anims.length, 1, + "Should have one animation attatched to the view-timeline"); + const anim = anims[0]; + await anim.ready; + await waitForNextFrame(); + + // @ contain 0% + scroller.scrollTop = 700; + await waitForNextFrame(); + assert_progress_equals(anim, 0, 'progress at contain 0%'); + assert_translate_x_equals(100, 'translation at contain 0%'); + assert_opacity_equals(1/3, 'opacity at contain 0%'); + assert_property_equals('margin-left', 5, 'margin-left at contain 0%'); + assert_property_equals('margin-right', 10, 'margin-right at contain 0%'); + + // @ contain 50% + scroller.scrollTop = 750; + await waitForNextFrame(); + assert_progress_equals(anim, 0.5, 'progress at contain 50%'); + assert_translate_x_equals(150, 'translation at contain 50%'); + assert_opacity_equals(0.5, 'opacity at contain 50%'); + assert_property_equals('margin-left', 7.5, 'margin-left at contain 50%'); + assert_property_equals('margin-right', 7.5, 'margin-right at contain 50%'); + + // @ contain 100% + scroller.scrollTop = 800; + await waitForNextFrame(); + assert_progress_equals(anim, 1, 'progress at contain 100%'); + assert_translate_x_equals(200, 'translation at contain 100%'); + assert_opacity_equals(2/3, 'opacity at contain 100%'); + assert_property_equals('margin-left', 10, 'margin-left at contain 100%'); + assert_property_equals('margin-right', 5, 'margin-right at contain 100%'); + }, 'ViewTimeline with timeline offset keyframes outside [0,1]'); + } + + window.onload = runTest; +</script> +</html> diff --git a/testing/web-platform/tests/scroll-animations/css/view-timeline-lookup.html b/testing/web-platform/tests/scroll-animations/css/view-timeline-lookup.html new file mode 100644 index 0000000000..6cead9dc58 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-lookup.html @@ -0,0 +1,273 @@ +<!DOCTYPE html> +<title>Named view-timeline lookup</title> +<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#view-timelines-named"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="support/testcommon.js"></script> +<style> + @keyframes anim { + from { z-index: 0; } + to { z-index: 100; } + } + .scroller { + overflow: auto; + width: 100px; + height: 100px; + } + .scroller > div { + height: 25px; + z-index: -1; + } +</style> +<main id=main></main> +<script> + function inflate(t, template) { + t.add_cleanup(() => main.replaceChildren()); + main.append(template.content.cloneNode(true)); + main.offsetTop; + } +</script> + +<template id=timeline_self> + <style> + #target { + height: 0px; + view-timeline: t1; + animation: anim 1s linear; + animation-timeline: t1; + } + </style> + <div id=scroller class=scroller> + <div id=target></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, timeline_self); + await waitForNextFrame(); + assert_equals(getComputedStyle(target).zIndex, '100'); + }, 'view-timeline on self'); +</script> + +<template id=timeline_preceding_sibling> + <style> + #scroller { + view-timeline: t1 defer; + } + #timeline { + height: 0px; + view-timeline: t1 ancestor; + } + #target { + animation: anim 1s linear; + animation-timeline: t1; + } + </style> + <div id=scroller class=scroller> + <div></div> + <div id=timeline></div> + <div></div> + <div></div> + <div id=target></div> + <div></div> + <div></div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, timeline_preceding_sibling); + await waitForNextFrame(); + assert_equals(getComputedStyle(target).zIndex, '75'); + }, 'view-timeline on preceding sibling'); +</script> + +<template id=timeline_ancestor> + <style> + #timeline { + height: 0px; + view-timeline: t1; + } + #target { + animation: anim 1s linear; + animation-timeline: t1; + } + </style> + <div id=scroller class=scroller> + <div></div> + <div></div> + <div></div> + <div id=timeline> + <div> + <div id=target></div> + </div> + </div> + <div></div> + <div></div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, timeline_ancestor); + await waitForNextFrame(); + assert_equals(getComputedStyle(target).zIndex, '25'); + }, 'view-timeline on ancestor'); +</script> + +<template id=timeline_ancestor_sibling> + <style> + #scroller { + view-timeline: t1 defer; + } + #timeline { + height: 0px; + view-timeline: t1 ancestor; + } + #target { + animation: anim 1s linear; + animation-timeline: t1; + } + </style> + <div id=scroller class=scroller> + <div></div> + <div id=timeline></div> + <div></div> + <div> + <div> + <div id=target></div> + </div> + </div> + <div></div> + <div></div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, timeline_ancestor_sibling); + await waitForNextFrame(); + assert_equals(getComputedStyle(target).zIndex, '75'); + }, 'view-timeline on ancestor sibling'); +</script> + +<template id=timeline_ancestor_sibling_conflict> + <style> + #scroller { + view-timeline: t1 defer; + } + #timeline1, #timeline2 { + height: 0px; + view-timeline: t1 ancestor; + } + #target { + animation: anim 1s linear; + animation-timeline: t1; + } + </style> + <div id=scroller class=scroller> + <div></div> + <div id=timeline1></div> + <div></div> + <div id=timeline2></div> + <div> + <div> + <div id=target></div> + </div> + </div> + <div></div> + <div></div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, timeline_ancestor_sibling_conflict); + await waitForNextFrame(); + assert_equals(getComputedStyle(target).zIndex, 'auto'); + }, 'view-timeline on ancestor sibling, conflict remains unresolved'); +</script> + +<template id=timeline_ancestor_closer_timeline_wins> + <style> + #scroller { + view-timeline: t1 defer; + } + #timeline { + height: 0px; + view-timeline: t1 ancestor; + } + #parent { + scroll-timeline: t1 defer; + } + #target { + animation: anim 1s linear; + animation-timeline: t1; + } + </style> + <div id=scroller class=scroller> + <div></div> + <div id=timeline></div> + <div></div> + <div id=parent> + <div id=target></div> + </div> + <div></div> + <div></div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, timeline_ancestor_closer_timeline_wins); + await waitForNextFrame(); + assert_equals(getComputedStyle(target).zIndex, 'auto'); + }, 'view-timeline on ancestor sibling, closer timeline wins'); +</script> + +<template id=timeline_ancestor_scroll_timeline_wins_on_same_element> + <style> + #scroller { + view-timeline: t1 defer; + scroll-timeline: t1 defer; + } + #timelines { + height: 0px; + view-timeline: t1 ancestor; + scroll-timeline: t1 ancestor; + overflow: auto; + } + #timelines > div { + height: 50px; + } + #target { + animation: anim 1s linear; + animation-timeline: t1; + } + </style> + <div id=scroller class=scroller> + <div></div> + <div id=timelines> + <div></div> + </div> + <div></div> + <div> + <div> + <div id=target></div> + </div> + </div> + <div></div> + <div></div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, timeline_ancestor_scroll_timeline_wins_on_same_element); + await waitForNextFrame(); + // In case of a name conflict on the same element, scroll progress timelines + // take precedence over view progress timelines. + // https://drafts.csswg.org/scroll-animations-1/#timeline-scope + assert_equals(getComputedStyle(target).zIndex, '0'); + }, 'view-timeline on ancestor sibling, scroll-timeline wins on same element'); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/view-timeline-name-computed.html b/testing/web-platform/tests/scroll-animations/css/view-timeline-name-computed.html new file mode 100644 index 0000000000..5657dc7817 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-name-computed.html @@ -0,0 +1,36 @@ +<!DOCTYPE html> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#view-timeline-name"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/css/support/computed-testcommon.js"></script> +</head> +<style> + #outer { view-timeline-name: foo, bar; } + #target { view-timeline-name: faz; } +</style> +<div id=outer> + <div id=target></div> +</div> +<script> +test_computed_value('view-timeline-name', 'initial', 'none'); +test_computed_value('view-timeline-name', 'inherit', 'foo, bar'); +test_computed_value('view-timeline-name', 'unset', 'none'); +test_computed_value('view-timeline-name', 'revert', 'none'); +test_computed_value('view-timeline-name', 'none'); +test_computed_value('view-timeline-name', 'foo'); +test_computed_value('view-timeline-name', 'foo, bar'); +test_computed_value('view-timeline-name', 'bar, foo'); +test_computed_value('view-timeline-name', 'a, b, c, D, e'); +test_computed_value('view-timeline-name', 'none, none'); +test_computed_value('view-timeline-name', 'a, b, c, none, d, e'); + +test(() => { + let style = getComputedStyle(document.getElementById('target')); + assert_not_equals(Array.from(style).indexOf('view-timeline-name'), -1); +}, 'The view-timeline-name property shows up in CSSStyleDeclaration enumeration'); + +test(() => { + let style = document.getElementById('target').style; + assert_not_equals(style.cssText.indexOf('view-timeline-name'), -1); +}, 'The view-timeline-name property shows up in CSSStyleDeclaration.cssText'); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/view-timeline-name-parsing.html b/testing/web-platform/tests/scroll-animations/css/view-timeline-name-parsing.html new file mode 100644 index 0000000000..3878d5c583 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-name-parsing.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#view-timeline-name"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/css/support/parsing-testcommon.js"></script> +<div id="target"></div> +<script> +test_valid_value('view-timeline-name', 'initial'); +test_valid_value('view-timeline-name', 'inherit'); +test_valid_value('view-timeline-name', 'unset'); +test_valid_value('view-timeline-name', 'revert'); + +test_valid_value('view-timeline-name', 'none'); +test_valid_value('view-timeline-name', 'abc'); +test_valid_value('view-timeline-name', ' abc', 'abc'); +test_valid_value('view-timeline-name', 'abc ', 'abc'); +test_valid_value('view-timeline-name', 'aBc'); +test_valid_value('view-timeline-name', 'foo, bar'); +test_valid_value('view-timeline-name', 'bar, foo'); +test_valid_value('view-timeline-name', 'none, none'); +test_valid_value('view-timeline-name', 'a, none, b'); +test_valid_value('view-timeline-name', 'auto'); + +test_invalid_value('view-timeline-name', 'default'); +test_invalid_value('view-timeline-name', '10px'); +test_invalid_value('view-timeline-name', 'foo bar'); +test_invalid_value('view-timeline-name', '"foo" "bar"'); +test_invalid_value('view-timeline-name', 'rgb(1, 2, 3)'); +test_invalid_value('view-timeline-name', '#fefefe'); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/view-timeline-name-shadow.html b/testing/web-platform/tests/scroll-animations/css/view-timeline-name-shadow.html new file mode 100644 index 0000000000..55240efcfb --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-name-shadow.html @@ -0,0 +1,186 @@ +<!DOCTYPE html> +<title>view-timeline-name and and shadow trees</title> +<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#view-timelines-named"> +<link rel="help" src="https://github.com/w3c/csswg-drafts/issues/8135"> +<link rel="help" src="https://github.com/w3c/csswg-drafts/issues/8192"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="/resources/declarative-shadow-dom-polyfill.js"></script> + +<main id=main></main> +<script> + function inflate(t, template) { + t.add_cleanup(() => main.replaceChildren()); + main.append(template.content.cloneNode(true)); + main.offsetTop; + } + + setup(() => { + polyfill_declarative_shadow_dom(document); + }); +</script> +<style> + @keyframes anim { + from { z-index: 100; } + to { z-index: 100; } + } +</style> + + +<template id=view_timeline_host> + <style> + .target { + animation: anim 10s linear; + animation-timeline: timeline; + } + .scroller > div { + view-timeline: timeline horizontal; + } + </style> + <div class=scroller> + <div> + <div class=target> + <template shadowrootmode=open> + <style> + :host { + view-timeline: timeline vertical; + } + </style> + </template> + </div> + </div> + </div> + <style> + </style> +</template> +<script> + promise_test(async (t) => { + inflate(t, view_timeline_host); + let target = main.querySelector('.target'); + assert_equals(target.getAnimations().length, 1); + let anim = target.getAnimations()[0]; + assert_not_equals(anim.timeline, null); + assert_equals(anim.timeline.axis, 'vertical'); + }, 'Outer animation can see view timeline defined by :host'); +</script> + + +<template id=view_timeline_slotted> + <style> + .target { + animation: anim 10s linear; + animation-timeline: timeline; + } + .host { + view-timeline: timeline horizontal; + } + </style> + <div class=scroller> + <div class=host> + <template shadowrootmode=open> + <style> + ::slotted(.target) { + view-timeline: timeline vertical; + } + </style> + <slot></slot> + </template> + <div class=target></div> + </div> + </div> + <style> + </style> +</template> +<script> + promise_test(async (t) => { + inflate(t, view_timeline_slotted); + let target = main.querySelector('.target'); + assert_equals(target.getAnimations().length, 1); + let anim = target.getAnimations()[0]; + assert_not_equals(anim.timeline, null); + assert_equals(anim.timeline.axis, 'vertical'); + }, 'Outer animation can see view timeline defined by ::slotted'); +</script> + + +<template id=view_timeline_part> + <style> + .host { + view-timeline: timeline vertical; + } + .host::part(foo) { + view-timeline: timeline horizontal; + } + </style> + <div class=host> + <template shadowrootmode=open> + <style> + /* Not using 'anim' at document scope, due to https://crbug.com/1334534 */ + @keyframes anim2 { + from { z-index: 100; } + to { z-index: 100; } + } + .target { + animation: anim2 10s linear; + animation-timeline: timeline; + } + </style> + <div part=foo> + <div class=target></div> + </div> + </template> + </div> + <style> + </style> +</template> +<script> + promise_test(async (t) => { + inflate(t, view_timeline_part); + let target = main.querySelector('.host').shadowRoot.querySelector('.target'); + assert_equals(target.getAnimations().length, 1); + let anim = target.getAnimations()[0]; + assert_not_equals(anim.timeline, null); + assert_equals(anim.timeline.axis, 'horizontal'); + }, 'Inner animation can see view timeline defined by ::part'); +</script> + + +<template id=view_timeline_shadow> + <style> + .target { + animation: anim 10s linear; + animation-timeline: timeline; + } + .host { + view-timeline: timeline horizontal; + } + </style> + <div class=scroller> + <div class=host> + <template shadowrootmode=open> + <style> + div { + view-timeline: timeline vertical; + } + </style> + <div> + <slot></slot> + </div> + </template> + <div class=target></div> + </div> + </div> + <style> + </style> +</template> +<script> + promise_test(async (t) => { + inflate(t, view_timeline_shadow); + let target = main.querySelector('.target'); + assert_equals(target.getAnimations().length, 1); + let anim = target.getAnimations()[0]; + assert_not_equals(anim.timeline, null); + assert_equals(anim.timeline.axis, 'vertical'); + }, 'Slotted element can see view timeline within the shadow'); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/view-timeline-range-animation.html b/testing/web-platform/tests/scroll-animations/css/view-timeline-range-animation.html new file mode 100644 index 0000000000..3d7593823d --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-range-animation.html @@ -0,0 +1,203 @@ +<!DOCTYPE html> +<title>Animations using named timeline ranges</title> +<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#named-timeline-range"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="support/testcommon.js"></script> +<style> + @keyframes anim { + from { z-index: 0; background-color: skyblue;} + to { z-index: 100; background-color: coral; } + } + #scroller { + border: 10px solid lightgray; + overflow-y: scroll; + width: 200px; + height: 200px; + } + #target { + margin: 800px 0px; + width: 100px; + height: 100px; + z-index: -1; + background-color: green; + font-size: 10px; + } +</style> +<main id=main> +</main> +<template> + <div id=scroller> + <div id=target></div> + </div> +</template> +<script> + setup(assert_implements_animation_timeline); + + function inflate(t, template) { + t.add_cleanup(() => main.replaceChildren()); + main.append(template.content.cloneNode(true)); + } + async function scrollTop(e, value) { + e.scrollTop = value; + await waitForNextFrame(); + } + async function waitForAnimationReady(target) { + await waitForNextFrame(); + await Promise.all(target.getAnimations().map(x => x.promise)); + } + async function assertValueAt(scroller, target, args) { + await waitForAnimationReady(target); + await scrollTop(scroller, args.scrollTop); + assert_equals(getComputedStyle(target).zIndex, args.expected.toString()); + } + function test_animation_delay(options) { + promise_test(async (t) => { + inflate(t, document.querySelector('template')); + let scroller = main.querySelector('#scroller'); + let target = main.querySelector('#target'); + + target.style.viewTimeline = 't1 block'; + // TODO(crbug.com/1375998): Create the timeline in a separate frame to + // work around a bug. + await waitForNextFrame(); + + target.style.animation = 'anim auto linear'; + target.style.animationTimeline = 't1'; + target.style.animationRangeStart = options.rangeStart; + target.style.animationRangeEnd = options.rangeEnd; + + // Accommodates floating point precision errors at the endpoints. + target.style.animationFillMode = 'both'; + + // 0% + await assertValueAt(scroller, target, + { scrollTop: options.startOffset, expected: 0 }); + // 50% + await assertValueAt(scroller, target, + { scrollTop: (options.startOffset + options.endOffset) / 2, expected: 50 }); + // 100% + await assertValueAt(scroller, target, + { scrollTop: options.endOffset, expected: 100 }); + + // Test before/after phases (need to clear the fill mode for that). + target.style.animationFillMode = 'initial'; + await assertValueAt(scroller, target, + { scrollTop: options.startOffset - 10, expected: -1 }); + await assertValueAt(scroller, target, + { scrollTop: options.endOffset + 10, expected: -1 }); + // Check 50% again without fill mode. + await assertValueAt(scroller, target, + { scrollTop: (options.startOffset + options.endOffset) / 2, expected: 50 }); + + }, `Animation with ranges [${options.rangeStart}, ${options.rangeEnd}]`); + } + + test_animation_delay({ + rangeStart: 'initial', + rangeEnd: 'initial', + startOffset: 600, + endOffset: 900 + }); + + test_animation_delay({ + rangeStart: 'cover 0%', + rangeEnd: 'cover 100%', + startOffset: 600, + endOffset: 900 + }); + + test_animation_delay({ + rangeStart: 'contain 0%', + rangeEnd: 'contain 100%', + startOffset: 700, + endOffset: 800 + }); + + + test_animation_delay({ + rangeStart: 'entry 0%', + rangeEnd: 'entry 100%', + startOffset: 600, + endOffset: 700 + }); + + test_animation_delay({ + rangeStart: 'exit 0%', + rangeEnd: 'exit 100%', + startOffset: 800, + endOffset: 900 + }); + + test_animation_delay({ + rangeStart: 'contain -50%', + rangeEnd: 'entry 200%', + startOffset: 650, + endOffset: 800 + }); + + test_animation_delay({ + rangeStart: 'entry 0%', + rangeEnd: 'exit 100%', + startOffset: 600, + endOffset: 900 + }); + + test_animation_delay({ + rangeStart: 'cover 20px', + rangeEnd: 'cover 100px', + startOffset: 620, + endOffset: 700 + }); + + test_animation_delay({ + rangeStart: 'contain 20px', + rangeEnd: 'contain 100px', + startOffset: 720, + endOffset: 800 + }); + + test_animation_delay({ + rangeStart: 'entry 20px', + rangeEnd: 'entry 100px', + startOffset: 620, + endOffset: 700 + }); + + test_animation_delay({ + rangeStart: 'entry-crossing 20px', + rangeEnd: 'entry-crossing 100px', + startOffset: 620, + endOffset: 700 + }); + + test_animation_delay({ + rangeStart: 'exit 20px', + rangeEnd: 'exit 80px', + startOffset: 820, + endOffset: 880 + }); + + test_animation_delay({ + rangeStart: 'exit-crossing 20px', + rangeEnd: 'exit-crossing 80px', + startOffset: 820, + endOffset: 880 + }); + + test_animation_delay({ + rangeStart: 'contain 20px', + rangeEnd: 'contain calc(100px - 10%)', + startOffset: 720, + endOffset: 790 + }); + + test_animation_delay({ + rangeStart: 'exit 2em', + rangeEnd: 'exit 8em', + startOffset: 820, + endOffset: 880 + }); + +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/view-timeline-range-update-reversed-animation.html b/testing/web-platform/tests/scroll-animations/css/view-timeline-range-update-reversed-animation.html new file mode 100644 index 0000000000..c719916160 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-range-update-reversed-animation.html @@ -0,0 +1,69 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<title>Update timeline range on reversed animation refTest</title> +<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#named-timeline-range"> +<link rel="match" href="./animation-update-ref.html?translate=60px"> +<script src="/web-animations/testcommon.js"></script> +</head> +<style type="text/css"> + @keyframes anim { + from { transform: translateX(100px) } + to { transform: translateX(0px) } + } + #scroller { + border: 1px solid black; + overflow: hidden; + width: 300px; + height: 200px; + } + #target { + margin-bottom: 800px; + margin-top: 800px; + margin-left: 10px; + margin-right: 10px; + width: 100px; + height: 100px; + z-index: -1; + background-color: green; + animation: anim auto linear; + animation-timeline: timeline; + view-timeline: timeline; + } + #target.exit-range { + animation-range-start: exit 0%; + animation-range-end: exit 100%; + } +</style> +<body> + <div id="scroller"> + <div id="target"></div> + </div> +</body> +<script type="text/javascript"> + document.documentElement.addEventListener('TestRendered', async () => { + runTest(); + }, { once: true }); + + async function runTest() { + await waitForCompositorReady(); + + const anim = target.getAnimations()[0]; + anim.playbackRate = -1; + + // Scroll to exit 60%. + scroller.scrollTop = 860; + await waitForNextFrame(); + + // Update the animation range. + target.classList.add('exit-range'); + await waitForNextFrame(); + + // Make sure change to animation range was properly picked up. + document.documentElement.classList.remove("reftest-wait"); + } +</script> +</body> +</html> diff --git a/testing/web-platform/tests/scroll-animations/css/view-timeline-range-update.html b/testing/web-platform/tests/scroll-animations/css/view-timeline-range-update.html new file mode 100644 index 0000000000..e8e761d86b --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-range-update.html @@ -0,0 +1,68 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<title>Update timeline range refTest</title> +<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#named-timeline-range"> +<link rel="match" href="./animation-update-ref.html?translate=40px"> +<script src="/web-animations/testcommon.js"></script> +</head> +<style type="text/css"> + @keyframes anim { + from { transform: translateX(100px) } + to { transform: translateX(0px) } + } + #scroller { + border: 1px solid black; + overflow: hidden; + width: 300px; + height: 200px; + } + #target { + margin-bottom: 800px; + margin-top: 800px; + margin-left: 10px; + margin-right: 10px; + width: 100px; + height: 100px; + z-index: -1; + background-color: green; + animation: anim auto linear; + animation-timeline: timeline; + view-timeline: timeline; + } + #target.exit-range { + animation-range-start: exit 0%; + animation-range-end: exit 100%; + } +</style> +<body> + <div id="scroller"> + <div id="target"></div> + </div> +</body> +<script type="text/javascript"> + document.documentElement.addEventListener('TestRendered', async () => { + runTest(); + }, { once: true }); + + async function runTest() { + await waitForCompositorReady(); + + const anim = target.getAnimations()[0]; + + // Scroll to exit 60%. + scroller.scrollTop = 860; + await waitForNextFrame(); + + // Update the animation range. + target.classList.add('exit-range'); + await waitForNextFrame(); + + // Make sure change to animation range was properly picked up. + document.documentElement.classList.remove("reftest-wait"); + } +</script> +</body> +</html> diff --git a/testing/web-platform/tests/scroll-animations/css/view-timeline-shorthand.tentative.html b/testing/web-platform/tests/scroll-animations/css/view-timeline-shorthand.tentative.html new file mode 100644 index 0000000000..f19b9e6ac2 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-shorthand.tentative.html @@ -0,0 +1,117 @@ +<!DOCTYPE html> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#view-timeline-shorthand"> +<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/7627"> +<link rel="help" href="https://github.com/w3c/csswg-drafts/pull/7694"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/css/support/computed-testcommon.js"></script> +<script src="/css/support/parsing-testcommon.js"></script> +<script src="/css/support/shorthand-testcommon.js"></script> +<div id="target"></div> +<script> +test_valid_value('view-timeline', 'abcd'); +test_valid_value('view-timeline', 'none block', 'none'); +test_valid_value('view-timeline', 'none inline'); + +// view-timeline-name: inline/block/horizontal/vertical. +test_valid_value('view-timeline', 'inline block', 'inline'); +test_valid_value('view-timeline', 'block block', 'block'); +test_valid_value('view-timeline', 'vertical block', 'vertical'); +test_valid_value('view-timeline', 'horizontal block', 'horizontal'); + +test_valid_value('view-timeline', 'a, b, c'); +test_valid_value('view-timeline', 'a inline, b block, c vertical', 'a inline, b, c vertical'); +test_valid_value('view-timeline', 'auto'); +test_valid_value('view-timeline', 'abc defer vertical', 'abc vertical defer'); +test_valid_value('view-timeline', 'abc vertical defer'); + +test_invalid_value('view-timeline', 'abc abc'); +test_invalid_value('view-timeline', 'block none'); +test_invalid_value('view-timeline', 'none none'); +test_invalid_value('view-timeline', 'default'); +test_invalid_value('view-timeline', ','); +test_invalid_value('view-timeline', ',,block,,'); + +test_computed_value('view-timeline', 'abcd'); +test_computed_value('view-timeline', 'none block', 'none'); +test_computed_value('view-timeline', 'none inline'); +test_computed_value('view-timeline', 'inline block', 'inline'); +test_computed_value('view-timeline', 'block block', 'block'); +test_computed_value('view-timeline', 'vertical block', 'vertical'); +test_computed_value('view-timeline', 'horizontal block', 'horizontal'); +test_computed_value('view-timeline', 'a, b, c'); +test_computed_value('view-timeline', 'a inline, b block, c vertical', 'a inline, b, c vertical'); +test_computed_value('view-timeline', 'abc defer vertical', 'abc vertical defer'); +test_computed_value('view-timeline', 'abc vertical defer'); + +test_shorthand_value('view-timeline', 'abc vertical', +{ + 'view-timeline-name': 'abc', + 'view-timeline-axis': 'vertical', + 'view-timeline-attachment': 'local', +}); +test_shorthand_value('view-timeline', 'abc vertical defer, def', +{ + 'view-timeline-name': 'abc, def', + 'view-timeline-axis': 'vertical, block', + 'view-timeline-attachment': 'defer, local', +}); +test_shorthand_value('view-timeline', 'abc, def', +{ + 'view-timeline-name': 'abc, def', + 'view-timeline-axis': 'block, block', + 'view-timeline-attachment': 'local, local', +}); +test_shorthand_value('view-timeline', 'inline horizontal ancestor', +{ + 'view-timeline-name': 'inline', + 'view-timeline-axis': 'horizontal', + 'view-timeline-attachment': 'ancestor', +}); + +function test_shorthand_contraction(shorthand, longhands, expected) { + let longhands_fmt = Object.entries(longhands).map((e) => `${e[0]}:${e[1]}:${e[2]}`).join(';'); + test((t) => { + t.add_cleanup(() => { + for (let shorthand of Object.keys(longhands)) + target.style.removeProperty(shorthand); + }); + for (let [shorthand, value] of Object.entries(longhands)) + target.style.setProperty(shorthand, value); + assert_equals(target.style.getPropertyValue(shorthand), expected, 'Declared value'); + assert_equals(getComputedStyle(target).getPropertyValue(shorthand), expected, 'Computed value'); + }, `Shorthand contraction of ${longhands_fmt}`); +} + +test_shorthand_contraction('view-timeline', { + 'view-timeline-name': 'abc', + 'view-timeline-axis': 'inline', + 'view-timeline-attachment': 'ancestor', +}, 'abc inline ancestor'); + +test_shorthand_contraction('view-timeline', { + 'view-timeline-name': 'a, b', + 'view-timeline-axis': 'inline, block', + 'view-timeline-attachment': 'defer, local', +}, 'a inline defer, b'); + +test_shorthand_contraction('view-timeline', { + 'view-timeline-name': 'none, none', + 'view-timeline-axis': 'block, block', + 'view-timeline-attachment': 'local, local', +}, 'none, none'); + +// Longhands with different lengths: + +test_shorthand_contraction('view-timeline', { + 'view-timeline-name': 'a, b, c', + 'view-timeline-axis': 'inline, inline', + 'view-timeline-attachment': 'local, local', +}, ''); + +test_shorthand_contraction('view-timeline', { + 'view-timeline-name': 'a, b', + 'view-timeline-axis': 'inline, inline, inline', + 'view-timeline-attachment': 'local, local', +}, ''); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/view-timeline-subject-bounds-update.html b/testing/web-platform/tests/scroll-animations/css/view-timeline-subject-bounds-update.html new file mode 100644 index 0000000000..7001eceeaf --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-subject-bounds-update.html @@ -0,0 +1,71 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<title>Update subject bounds refTest</title> +<link rel="help" src="https://github.com/w3c/csswg-drafts/issues/8694"> +<link rel="match" + href="./animation-update-ref.html?translate=100px&scroll=800"> +<script src="/web-animations/testcommon.js"></script> +</head> +<style type="text/css"> + @keyframes anim { + from { transform: translateX(100px) } + to { transform: translateX(0px) } + } + #scroller { + border: 1px solid black; + overflow: hidden; + width: 300px; + height: 200px; + } + #target { + margin-bottom: 800px; + margin-top: 700px; + margin-left: 10px; + margin-right: 10px; + width: 100px; + height: 200px; + z-index: -1; + background-color: green; + animation: anim auto both linear; + animation-timeline: timeline; + view-timeline: timeline; + animation-range: exit; + } + #target.bounds-update { + height: 100px; + /* Keep the scroll range the same. */ + margin-top: 800px; + } +</style> +<body> + <div id="scroller"> + <div id="target"></div> + </div> +</body> +<script type="text/javascript"> + document.documentElement.addEventListener('TestRendered', async () => { + runTest(); + }, { once: true }); + + async function runTest() { + await waitForCompositorReady(); + + const anim = target.getAnimations()[0]; + + // Scroll to exit 50%. + scroller.scrollTop = 800; + await waitForNextFrame(); + + // After the update to the animation range, the positioning is exit 0% + target.classList.add('bounds-update'); + await waitForNextFrame(); + + // Make sure change to animation range was properly picked up. + document.documentElement.classList.remove("reftest-wait"); + } +</script> +</body> +</html> diff --git a/testing/web-platform/tests/scroll-animations/css/view-timeline-used-values.html b/testing/web-platform/tests/scroll-animations/css/view-timeline-used-values.html new file mode 100644 index 0000000000..6627eeb998 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-used-values.html @@ -0,0 +1,104 @@ +<!DOCTYPE html> +<title>Used values of view-timeline properties</title> +<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#view-timeline-axis"> +<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#view-timeline-name"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="support/testcommon.js"></script> +<style> + @keyframes anim { + from { z-index: 0; } + to { z-index: 100; } + } + .scroller { + overflow: hidden; + width: 100px; + height: 100px; + } + .scroller > div { + width: 300px; + height: 300px; + z-index: -1; + } +</style> +<main id=main></main> +<script> + setup(assert_implements_animation_timeline); + + function inflate(t, template) { + t.add_cleanup(() => main.replaceChildren()); + main.append(template.content.cloneNode(true)); + } + async function scrollTop(e, value) { + e.scrollTop = value; + await waitForNextFrame(); + } + async function scrollLeft(e, value) { + e.scrollLeft = value; + await waitForNextFrame(); + } +</script> + +<template id=omitted_axis> + <style> + #target { + view-timeline-name: t1, t2; /* Two items */ + view-timeline-axis: inline; /* One item */ + animation: anim 1s linear; + animation-timeline: t2; + } + </style> + <div id=scroller class=scroller> + <div id=target></div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, omitted_axis); + assert_equals(getComputedStyle(target).zIndex, '-1'); + + // enter 0% is at scrollTop/Left = -100 + // exit 100% is at scrollTop/Left = 300 + // This means that at scrollTop/Left=0, the animation is at 25%. + + await scrollTop(scroller, 0); + await scrollLeft(scroller, 0); + assert_equals(getComputedStyle(target).zIndex, '25'); + + // The timeline should be inline-axis: + await scrollTop(scroller, 100); // 50% + await scrollLeft(scroller, 40); // 35% + assert_equals(getComputedStyle(target).zIndex, '35'); + }, 'Use the last value from view-timeline-axis if omitted'); +</script> + +<template id=omitted_inset> + <style> + #target { + view-timeline-name: t1, t2; /* Two items */ + view-timeline-inset: 100px; /* One item */ + animation: anim 1s linear; + animation-timeline: t2; + } + </style> + <div id=scroller class=scroller> + <div id=target></div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, omitted_inset); + assert_equals(getComputedStyle(target).zIndex, '-1'); + + // 0% is normally at at scrollTop = -100 + // 100% is normally at scrollTop/Left = 300 + // However, we have a 100px inset in both ends, which makes the + // range [0, 200]. + + await scrollTop(scroller, 0); + assert_equals(getComputedStyle(target).zIndex, '0'); + await scrollTop(scroller, 100); // 50% + assert_equals(getComputedStyle(target).zIndex, '50'); + }, 'Use the last value from view-timeline-inset if omitted'); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/view-timeline-with-delay-and-range.tentative.html b/testing/web-platform/tests/scroll-animations/css/view-timeline-with-delay-and-range.tentative.html new file mode 100644 index 0000000000..e8f537b188 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-with-delay-and-range.tentative.html @@ -0,0 +1,93 @@ +<!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"> + @keyframes anim { + from { opacity: 0 } + to { opacity: 1 } + } + #scroller { + border: 10px solid lightgray; + overflow-y: scroll; + width: 300px; + height: 200px; + } + #target { + margin: 800px 0px; + width: 100px; + height: 100px; + z-index: -1; + background-color: green; + animation: anim auto linear; + animation-timeline: t1; + view-timeline: t1 block; + animation-range-start: entry 0%; + animation-range-end: entry 100%; + /* Sentinel value when in before or after phase of the animation. */ + opacity: 0.96875; + } +</style> +<body> + <div id=scroller> + <div id=target></div> + </div> +</body> +<script type="text/javascript"> + async function runTest() { + + function assert_opacity_equals(expected, errorMessage) { + assert_approx_equals( + parseFloat(getComputedStyle(target).opacity), expected, 1e-6, + errorMessage); + } + + promise_test(async t => { + await waitForNextFrame(); + const anim = document.getAnimations()[0]; + await anim.ready; + + await waitForNextFrame(); + scroller.scrollTop = 650; + await waitForNextFrame(); + + const baseOpacity = 0.96875; + // Delays are percentages. + const testData = [ + { delay: 0, endDelay: 0, opacity: 0.5 }, + { delay: 20, endDelay: 0, opacity: 0.375 }, + { delay: 0, endDelay: 20, opacity: 0.625 }, + { delay: 20, endDelay: 20, opacity: 0.5 }, + // Negative delays. + { delay: -25, endDelay: 0, opacity: 0.6 }, + { delay: 0, endDelay: -25, opacity: 0.4 }, + { delay: -25, endDelay: -25, opacity: 0.5 }, + // Stress tests with >= 100% total delay. Verify effect is inactive. + { delay: 100, endDelay: 0, opacity: baseOpacity }, + { delay: 0, endDelay: 100, opacity: baseOpacity }, + { delay: 100, endDelay: 100, opacity: baseOpacity } + ]; + + testData.forEach(test => { + anim.effect.updateTiming({ + delay: CSS.percent(test.delay), + endDelay: CSS.percent(test.endDelay) + }); + assert_opacity_equals( + test.opacity, + `Opacity when delay=${test.delay} and endDelay=${test.endDelay}`); + }); + }, 'ViewTimeline with animation delays and range'); + } + + window.onload = runTest; + +</script> +</html> diff --git a/testing/web-platform/tests/scroll-animations/css/view-timeline-with-transform-on-subject.html b/testing/web-platform/tests/scroll-animations/css/view-timeline-with-transform-on-subject.html new file mode 100644 index 0000000000..e4abac7219 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-with-transform-on-subject.html @@ -0,0 +1,76 @@ +<!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"> + @keyframes anim { + from { transform: scaleX(0) translateY(0); } + to { transform: scaleX(1) translatey(50vh); } + } + #scroller { + border: 10px solid lightgray; + overflow-y: scroll; + overflow-x: hidden; + width: 300px; + height: 200px; + } + .spacer { + height: 200px; + } + #target { + height: 50px; + background-color: green; + animation: anim auto both linear; + animation-timeline: view(); + animation-range-start: contain 0%; + animation-range-end: contain 100%; + } +</style> +<body> + <div id=scroller> + <div class="spacer"></div> + <div id="target"></div> + <div class="spacer"></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); + } + + promise_test(async t => { + await waitForNextFrame(); + const anim = document.getAnimations()[0]; + await anim.ready; + await waitForNextFrame(); + + // @ contain 0% + scroller.scrollTop = 50; + await waitForNextFrame(); + assert_progress_equals(anim, 0, 'progress at contain 0%'); + + // @ contain 50% + scroller.scrollTop = 125; + await waitForNextFrame(); + assert_progress_equals(anim, 0.5, 'progress at contain 50%'); + + // @ contain 100% + scroller.scrollTop = 200; + await waitForNextFrame(); + assert_progress_equals(anim, 1, 'progress at contain 100%'); + }, 'ViewTimeline use untransformed box for range calculations'); + } + + window.onload = runTest; +</script> +</html> diff --git a/testing/web-platform/tests/scroll-animations/scroll-timelines/animation-ref.html b/testing/web-platform/tests/scroll-animations/scroll-timelines/animation-ref.html new file mode 100644 index 0000000000..9158715321 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/scroll-timelines/animation-ref.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<title>Reference for Web Animation with scroll timeline tests</title> +<style> + #box { + width: 100px; + height: 100px; + background-color: green; + transform: translate(0, 100px); + opacity: 0.5; + will-change: transform; /* force compositing */ + } + + #covered { + width: 100px; + height: 100px; + background-color: red; + } + + #scroller { + overflow: auto; + height: 100px; + width: 100px; + will-change: transform; /* force compositing */ + } + + #contents { + height: 1000px; + width: 100%; + } +</style> + +<div id="box"></div> +<div id="covered"></div> +<div id="scroller"> + <div id="contents"></div> +</div> + +<script> + window.addEventListener('load', function() { + // Move the scroller to halfway. + const scroller = document.getElementById("scroller"); + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.5 * maxScroll; + }); +</script> diff --git a/testing/web-platform/tests/scroll-animations/scroll-timelines/animation-with-animatable-interface.html b/testing/web-platform/tests/scroll-animations/scroll-timelines/animation-with-animatable-interface.html new file mode 100644 index 0000000000..b04aaf2d33 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/scroll-timelines/animation-with-animatable-interface.html @@ -0,0 +1,66 @@ +<html class="reftest-wait"> +<title>Scroll-linked animation with Animatable interface</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations/"> +<meta name="assert" content="ScrollTimeline should work with animatable +interface"> +<link rel="match" href="animation-ref.html"> + +<script src="/web-animations/testcommon.js"></script> +<script src="/common/reftest-wait.js"></script> + +<style> + #box { + width: 100px; + height: 100px; + background-color: green; + } + + #covered { + width: 100px; + height: 100px; + background-color: red; + } + + #scroller { + overflow: auto; + height: 100px; + width: 100px; + will-change: transform; + /* force compositing */ + } + + #contents { + height: 1000px; + width: 100%; + } +</style> + +<div id="box"></div> +<div id="covered"></div> +<div id="scroller"> + <div id="contents"><p>Scrolling Contents</p></div> +</div> + +<script> + const scroller = document.getElementById('scroller'); + const scroll_timeline = new ScrollTimeline({source: scroller}); + const box = document.getElementById('box'); + const animation = box.animate( + [ + { transform: 'translateY(0)', opacity: 1 }, + { transform: 'translateY(200px)', opacity: 0 } + ], { + timeline: scroll_timeline + } + ); + + animation.ready.then(() => { + // Move the scroller to the halfway point. + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.5 * maxScroll; + + waitForAnimationFrames(2).then(_ => { + takeScreenshot(); + }); + }); +</script> diff --git a/testing/web-platform/tests/scroll-animations/scroll-timelines/animation-with-delay-crash.html b/testing/web-platform/tests/scroll-animations/scroll-timelines/animation-with-delay-crash.html new file mode 100644 index 0000000000..9d821f9e20 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/scroll-timelines/animation-with-delay-crash.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<link rel="help" href="https://drafts.csswg.org/web-animations-1/#the-effecttiming-dictionaries"> +<style> +.scroller { + overflow: auto; + height: 100px; + width: 100px; + will-change: transform; +} + +.contents { + height: 1000px; + width: 100%; +} +</style> +<div class="scroller"> + <div class="contents"></div> +</div> +<script> + // Test passes if it does not crash. + // Scroll timeline animations are progress-based and not compatible with + // delays specified in milliseconds. + const timeline = new ScrollTimeline(); + const options = { + timeline: timeline, + endDelay: 200 + }; + const keyframes = { opacity: [0, 1]}; + const element = document.querySelector('.contents'); + element.animate(keyframes, options); +</script> diff --git a/testing/web-platform/tests/scroll-animations/scroll-timelines/animation-with-display-none.html b/testing/web-platform/tests/scroll-animations/scroll-timelines/animation-with-display-none.html new file mode 100644 index 0000000000..a62916833c --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/scroll-timelines/animation-with-display-none.html @@ -0,0 +1,75 @@ +<html class="reftest-wait"> +<title>Scroll timeline with Web Animation and transition from display:none to display:block</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations/"> +<meta name="assert" content="Scroll timeline should properly handle going from display:none to display:block"> +<link rel="match" href="animation-ref.html"> + +<script src="/web-animations/testcommon.js"></script> +<script src="/common/reftest-wait.js"></script> + +<style> + #box { + width: 100px; + height: 100px; + background-color: green; + } + + #covered { + width: 100px; + height: 100px; + background-color: red; + } + + #scroller { + overflow: auto; + height: 100px; + width: 100px; + will-change: transform; /* force compositing */ + } + + .removed { + display: none; + } + + #contents { + height: 1000px; + width: 100%; + } +</style> + +<div id="box"></div> +<div id="covered"></div> +<div id="scroller"> + <div id="contents"><p>Scrolling Contents</p></div> +</div> + +<script> + const box = document.getElementById('box'); + const effect = new KeyframeEffect(box, + [ + { transform: 'translateY(0)', opacity: 1 }, + { transform: 'translateY(200px)', opacity: 0 } + ], { + duration: 1000, + } + ); + + const scroller = document.getElementById('scroller'); + scroller.classList.add('removed'); + const timeline = new ScrollTimeline( + { source: scroller, orientation: 'block' }); + const animation = new Animation(effect, timeline); + animation.play(); + + waitForAnimationFrames(2).then(_ => { + scroller.classList.remove('removed'); + animation.ready.then(() => { + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.5 * maxScroll; + + waitForAnimationFrames(2).then(_ => { + takeScreenshot(); + }); + }); + }); +</script> diff --git a/testing/web-platform/tests/scroll-animations/scroll-timelines/animation-with-offsets-crash.html b/testing/web-platform/tests/scroll-animations/scroll-timelines/animation-with-offsets-crash.html new file mode 100644 index 0000000000..d4d1a55214 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/scroll-timelines/animation-with-offsets-crash.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#scrolltimeline-interface"> +<style> +.scroller { + overflow: auto; + height: 100px; + width: 100px; + will-change: transform; +} + +.contents { + height: 1000px; + width: 100%; +} +</style> +<div class="scroller"> + <div class="contents"></div> +</div> +<script> + // Test passes if it does not crash. + // Scroll timeline animations are progress-based and not compatible with + // delays specified in milliseconds. + const scroller = document.querySelector('.scroller'); + const animation = new Animation(); + const timeline = animation.timeline; + const duration = timeline.duration; + const options = { + source: scroller, + scrollOffsets: [new CSSMathInvert(duration)] + }; + const scroll_timeline = new ScrollTimeline(options); +</script> diff --git a/testing/web-platform/tests/scroll-animations/scroll-timelines/animation-with-overflow-hidden-ref.html b/testing/web-platform/tests/scroll-animations/scroll-timelines/animation-with-overflow-hidden-ref.html new file mode 100644 index 0000000000..c045f1a1c9 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/scroll-timelines/animation-with-overflow-hidden-ref.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<title>Scroll timeline with Web Animation using a scroller with overflow hidden</title> +<style> + #box { + width: 100px; + height: 100px; + background-color: green; + transform: translate(0, 100px); + opacity: 0.5; + will-change: transform; /* force compositing */ + } + + #covered { + width: 100px; + height: 100px; + background-color: red; + } + + #scroller { + overflow: hidden; + height: 100px; + width: 100px; + will-change: transform; /* force compositing */ + } + + #contents { + height: 1000px; + width: 100%; + } +</style> + +<div id="box"></div> +<div id="covered"></div> +<div id="scroller"> + <div id="contents"></div> +</div> + +<script> + window.addEventListener('load', function() { + // Move the scroller to halfway. + const scroller = document.getElementById("scroller"); + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.5 * maxScroll; + }); +</script> diff --git a/testing/web-platform/tests/scroll-animations/scroll-timelines/animation-with-overflow-hidden.html b/testing/web-platform/tests/scroll-animations/scroll-timelines/animation-with-overflow-hidden.html new file mode 100644 index 0000000000..bc7611d05a --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/scroll-timelines/animation-with-overflow-hidden.html @@ -0,0 +1,64 @@ +<html class="reftest-wait"> +<title>Scroll timeline with Web Animation using a scroller with overflow hidden</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations/"> +<meta name="assert" content="Web animation correctly updates values when using a overflow: hidden on the scroller being used as the source for the ScrollTimeline"> +<link rel="match" href="animation-with-overflow-hidden-ref.html"> + +<script src="/web-animations/testcommon.js"></script> +<script src="/common/reftest-wait.js"></script> + +<style> + #box { + width: 100px; + height: 100px; + background-color: green; + } + + #covered { + width: 100px; + height: 100px; + background-color: red; + } + + #scroller { + overflow: hidden; + height: 100px; + width: 100px; + } + + #contents { + height: 1000px; + width: 100%; + } +</style> + +<div id="box"></div> +<div id="covered"></div> +<div id="scroller"> + <div id="contents"></div> +</div> + +<script> + const box = document.getElementById('box'); + const effect = new KeyframeEffect(box, + [ + {transform: 'translateY(0)', opacity: 1}, + {transform: 'translateY(200px)', opacity: 0} + ] + ); + + const scroller = document.getElementById('scroller'); + const timeline = new ScrollTimeline( + { source: scroller, orientation: 'block' }); + const animation = new Animation(effect, timeline); + animation.play(); + + animation.ready.then(() => { + // Move the scroller to the halfway point. + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.5 * maxScroll; + waitForAnimationFrames(2).then(_ => { + takeScreenshot(); + }); + }); +</script> diff --git a/testing/web-platform/tests/scroll-animations/scroll-timelines/animation-with-root-scroller-ref.html b/testing/web-platform/tests/scroll-animations/scroll-timelines/animation-with-root-scroller-ref.html new file mode 100644 index 0000000000..58435be631 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/scroll-timelines/animation-with-root-scroller-ref.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<title>Reference for Scroll timeline with Web Animation using the root scroller</title> +<style> + html { + min-height: 100%; + min-width: 100%; + padding-bottom: 100px; + padding-right: 100px; + } + + #box { + width: 100px; + height: 100px; + background-color: green; + transform: translate(0, 100px); + opacity: 0.5; + will-change: transform; /* force compositing */ + } + + #covered { + width: 100px; + height: 100px; + background-color: red; + } +</style> + +<div id="box"></div> +<div id="covered"><p>Covered Contents</p></div> + +<script> + window.addEventListener('load', function() { + // Move the scroller to halfway. + const scroller = document.scrollingElement; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.5 * maxScroll; + }); +</script> diff --git a/testing/web-platform/tests/scroll-animations/scroll-timelines/animation-with-root-scroller.html b/testing/web-platform/tests/scroll-animations/scroll-timelines/animation-with-root-scroller.html new file mode 100644 index 0000000000..6ba1a22445 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/scroll-timelines/animation-with-root-scroller.html @@ -0,0 +1,60 @@ +<html class="reftest-wait"> +<title>Scroll timeline with Web Animation using the root scroller</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations/"> +<meta name="assert" content="Web animation correctly updates values when using the root scroller as the source for the ScrollTimeline"> +<link rel="match" href="animation-with-root-scroller-ref.html"> + +<script src="/web-animations/testcommon.js"></script> +<script src="/common/reftest-wait.js"></script> + +<style> + html { + min-height: 100%; + min-width: 100%; + padding-bottom: 100px; + padding-right: 100px; + } + + #box { + width: 100px; + height: 100px; + background-color: green; + } + + #covered { + width: 100px; + height: 100px; + background-color: red; + } +</style> + +<div id="box"></div> +<div id="covered"><p>Covered Contents</p></div> + +<script> + const box = document.getElementById('box'); + const effect = new KeyframeEffect(box, + [ + {transform: 'translateY(0)', opacity: 1}, + {transform: 'translateY(200px)', opacity: 0} + ], { + duration: 1000, + } + ); + + const scroller = document.scrollingElement; + const timeline = new ScrollTimeline( + { source: scroller, orientation: 'block' }); + const animation = new Animation(effect, timeline); + animation.play(); + + animation.ready.then(() => { + // Move the scroller to the halfway point. + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.5 * maxScroll; + + waitForAnimationFrames(2).then(_ => { + takeScreenshot(); + }); + }); +</script> diff --git a/testing/web-platform/tests/scroll-animations/scroll-timelines/animation-with-transform.html b/testing/web-platform/tests/scroll-animations/scroll-timelines/animation-with-transform.html new file mode 100644 index 0000000000..f741cc634d --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/scroll-timelines/animation-with-transform.html @@ -0,0 +1,68 @@ +<html class="reftest-wait"> +<title>Basic use of scroll timeline with Web Animation</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations/"> +<meta name="assert" content="Should be able to use the scroll timeline to drive the animation timing"> +<link rel="match" href="animation-ref.html"> + +<script src="/web-animations/testcommon.js"></script> +<script src="/common/reftest-wait.js"></script> + +<style> + #box { + width: 100px; + height: 100px; + background-color: green; + } + + #covered { + width: 100px; + height: 100px; + background-color: red; + } + + #scroller { + overflow: auto; + height: 100px; + width: 100px; + will-change: transform; /* force compositing */ + } + + #contents { + height: 1000px; + width: 100%; + } +</style> + +<div id="box"></div> +<div id="covered"></div> +<div id="scroller"> + <div id="contents"><p>Scrolling Contents</p></div> +</div> + +<script> + const box = document.getElementById('box'); + const effect = new KeyframeEffect(box, + [ + { transform: 'translateY(0)', opacity: 1}, + { transform: 'translateY(200px)', opacity: 0} + ], { + duration: 1000, + } + ); + + const scroller = document.getElementById('scroller'); + const timeline = new ScrollTimeline( + { source: scroller, orientation: 'block' }); + const animation = new Animation(effect, timeline); + animation.play(); + + animation.ready.then(() => { + // Move the scroller to the halfway point. + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.5 * maxScroll; + + waitForAnimationFrames(2).then(_ => { + takeScreenshot(); + }); + }); +</script> diff --git a/testing/web-platform/tests/scroll-animations/scroll-timelines/cancel-animation.html b/testing/web-platform/tests/scroll-animations/scroll-timelines/cancel-animation.html new file mode 100644 index 0000000000..7daf76a7a5 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/scroll-timelines/cancel-animation.html @@ -0,0 +1,214 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Canceling an animation</title> +<link rel="help" + href="https://drafts.csswg.org/web-animations/#canceling-an-animation-section"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="testcommon.js"></script> +<style> +.scroller { + overflow: auto; + height: 100px; + width: 100px; + will-change: transform; +} + +.contents { + height: 1000px; + width: 100%; +} +</style> +<body> +<script> +'use strict'; + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + animation.cancel(); + + assert_equals(animation.startTime, null, + 'The start time of a canceled animation should be unresolved'); + assert_equals(animation.currentTime, null, + 'The hold time of a canceled animation should be unresolved'); +}, 'Canceling an animation should cause its start time and hold time to be' + + ' unresolved'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + const retPromise = animation.ready.then(() => { + assert_unreached('ready promise was fulfilled'); + }).catch(err => { + assert_equals(err.name, 'AbortError', + 'ready promise is rejected with AbortError'); + }); + + animation.cancel(); + + return retPromise; +}, 'A play-pending ready promise should be rejected when the animation is' + + ' canceled'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + await animation.ready; + + // Make it pause-pending + animation.pause(); + + // We need to store the original ready promise since cancel() will + // replace it + const originalPromise = animation.ready; + animation.cancel(); + + await promise_rejects_dom(t, 'AbortError', originalPromise, + 'Cancel should abort ready promise'); +}, 'A pause-pending ready promise should be rejected when the animation is' + + ' canceled'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + animation.cancel(); + const promiseResult = await animation.ready; + assert_equals(promiseResult, animation); +}, 'When an animation is canceled, it should create a resolved Promise'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + const promise = animation.ready; + animation.cancel(); + assert_not_equals(animation.ready, promise); + promise_rejects_dom(t, 'AbortError', promise, + 'Cancel should abort ready promise'); +}, 'The ready promise should be replaced when the animation is canceled'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + assert_equals(animation.playState, 'idle', + 'The animation should be initially idle'); + + animation.finished.then(t.step_func(() => { + assert_unreached('Finished promise should not resolve'); + }), t.step_func(() => { + assert_unreached('Finished promise should not reject'); + })); + + animation.cancel(); + + return waitForAnimationFrames(3); +}, 'The finished promise should NOT be rejected if the animation is already' + + ' idle'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + assert_equals(animation.playState, 'idle', + 'The animation should be initially idle'); + + animation.oncancel = t.step_func(() => { + assert_unreached('Cancel event should not be fired'); + }); + + animation.cancel(); + + return waitForAnimationFrames(3); +}, 'The cancel event should NOT be fired if the animation is already idle'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + animation.effect.target.remove(); + + const eventWatcher = new EventWatcher(t, animation, 'cancel'); + + await animation.ready; + animation.cancel(); + + await eventWatcher.wait_for('cancel'); + + assert_equals(animation.effect.target.parentNode, null, + 'cancel event should be fired for the animation on an orphaned element'); +}, 'Canceling an animation should fire cancel event on orphaned element'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + await animation.ready; + + // Make the scroll timeline inactive. + scroller.style.overflow = 'visible'; + scroller.scrollTop; + await waitForNextFrame(); + assert_equals(animation.timeline.currentTime, null, + 'Sanity check the timeline is inactive.'); + animation.cancel(); + assert_equals(animation.startTime, null, + 'The start time of a canceled animation should be unresolved'); + assert_equals(animation.currentTime, null, + 'The current time of a canceled animation should be unresolved'); +}, 'Canceling an animation with inactive timeline should cause its start time' + + ' and hold time to be unresolved'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + await animation.ready; + + // Make the scroll timeline inactive. + scroller.style.overflow = 'visible'; + scroller.scrollTop; + await waitForNextFrame(); + assert_equals(animation.timeline.currentTime, null, + 'Sanity check the timeline is inactive.'); + + const eventWatcher = new EventWatcher(t, animation, 'cancel'); + animation.cancel(); + const cancelEvent = await eventWatcher.wait_for('cancel'); + + assert_equals(cancelEvent.currentTime, null, + 'event.currentTime should be unresolved when the timeline is inactive.'); + assert_equals(cancelEvent.timelineTime, null, + 'event.timelineTime should be unresolved when the timeline is inactive'); +}, 'oncancel event is fired when the timeline is inactive.'); + +</script> +</body> diff --git a/testing/web-platform/tests/scroll-animations/scroll-timelines/constructor-no-document.html b/testing/web-platform/tests/scroll-animations/scroll-timelines/constructor-no-document.html new file mode 100644 index 0000000000..d2cc590bc7 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/scroll-timelines/constructor-no-document.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>ScrollTimeline constructor - no document</title> +<link rel="help" href="https://wicg.github.io/scroll-animations/#scrolltimeline-interface"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<script> +'use strict'; + +test(function() { + document.documentElement.remove(); + assert_equals(document.scrollingElement, null); + + const timeline = new ScrollTimeline(); + assert_equals(timeline.source, null); + assert_equals(timeline.currentTime, null); +}, 'The source can be null if the document.scrollingElement does not exist'); +</script> diff --git a/testing/web-platform/tests/scroll-animations/scroll-timelines/constructor.html b/testing/web-platform/tests/scroll-animations/scroll-timelines/constructor.html new file mode 100644 index 0000000000..8e211efa11 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/scroll-timelines/constructor.html @@ -0,0 +1,95 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>ScrollTimeline constructor</title> + <link rel="help" href="https://wicg.github.io/scroll-animations/#scrolltimeline-interface"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<style> +.scroller { + height: 100px; + width: 100px; + overflow: scroll; +} + +.content { + height: 500px; + width: 500px; +} +</style> + +<div class="scroller"> + <div class="content"></div> +</div> + +<script> +'use strict'; + +function formatOffset(v) { + if (typeof(v) == 'object') + return `${v.constructor.name}(${v.toString()})`; + return `'${v.toString()}'`; +} + +function assert_offsets_equal(a, b) { + assert_equals(formatOffset(a), formatOffset(b)); +} + +// source + +test(t => { + const scroller = document.querySelector('.scroller'); + assert_equals( + new ScrollTimeline({source: scroller}).source, scroller); +}, 'A ScrollTimeline can be created with a source'); + +test(t => { + const div = document.createElement('div'); + assert_equals(new ScrollTimeline({source: div}).source, div); +}, 'A ScrollTimeline can be created with a non-scrolling source'); + +test(t => { + assert_equals(new ScrollTimeline({source: null}).source, null); +}, 'A ScrollTimeline created with a null source should have no source'); + +test(t => { + assert_equals(new ScrollTimeline().source, document.scrollingElement); +}, 'A ScrollTimeline created without a source should use the ' + + 'document.scrollingElement'); + +// axis + +test(t => { + assert_equals(new ScrollTimeline().axis, 'block'); +}, 'A ScrollTimeline created with the default axis should default to ' + + `'block'`); + +const gValidAxisValues = [ + 'block', + 'inline', + 'horizontal', + 'vertical', +]; + +for (let axis of gValidAxisValues) { + test(function() { + const scrollTimeline = + new ScrollTimeline({axis: axis}); + assert_equals(scrollTimeline.axis, axis); + }, `'${axis}' is a valid axis value`); +} + +test(t => { + let constructorFunc = function() { + new ScrollTimeline({axis: 'nonsense'}) + }; + assert_throws_js(TypeError, constructorFunc); + + // 'auto' for axis was previously in the spec, but was removed. Make + // sure that implementations do not support it. + constructorFunc = function() { + new ScrollTimeline({axis: 'auto'}) + }; + assert_throws_js(TypeError, constructorFunc); +}, 'Creating a ScrollTimeline with an invalid axis value should throw'); +</script> diff --git a/testing/web-platform/tests/scroll-animations/scroll-timelines/current-time-nan.html b/testing/web-platform/tests/scroll-animations/scroll-timelines/current-time-nan.html new file mode 100644 index 0000000000..440b1f413e --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/scroll-timelines/current-time-nan.html @@ -0,0 +1,80 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>ScrollTimeline current time algorithm - NaN cases</title> +<link rel="help" href="https://wicg.github.io/scroll-animations/#current-time-algorithm"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<style> +.scroller { + height: 100px; + width: 100px; + overflow: auto; +} + +.content { + height: 500px; + width: 500px; +} +</style> + +<div id='inlineScroller' class='scroller' style='display: inline;'> + <div class='content'></div> +</div> +<script> +'use strict'; + +test(function() { + const scroller = document.querySelector('#inlineScroller'); + const scrollTimeline = new ScrollTimeline( + { source: scroller, orientation: 'block' }); + + assert_equals(scrollTimeline.currentTime, null); +}, 'currentTime should be null for a display: inline source'); +</script> + +<div id='displayNoneScroller' class='scroller' style='display: none;'> + <div class='content'></div> +</div> +<script> +test(function() { + const scroller = document.querySelector('#displayNoneScroller'); + const scrollTimeline = new ScrollTimeline( + { source: scroller, orientation: 'block' }); + + assert_equals(scrollTimeline.currentTime, null); +}, 'currentTime should be null for a display: none source'); +</script> + +<script> +test(function() { + const scroller = document.createElement('div'); + const content = document.createElement('div'); + + scroller.style.overflow = 'auto'; + scroller.style.height = '100px'; + scroller.style.width = '100px'; + content.style.height = '250px'; + content.style.width = '250px'; + + scroller.appendChild(content); + + const scrollTimeline = new ScrollTimeline( + { source: scroller, orientation: 'block' }); + + assert_equals(scrollTimeline.currentTime, null); +}, 'currentTime should be null for an unattached source'); +</script> + +<div id='notAScroller' class='scroller' style='overflow: visible;'> + <div class='content'></div> +</div> +<script> +test(function() { + const scroller = document.querySelector('#notAScroller'); + const scrollTimeline = new ScrollTimeline( + { source: scroller, orientation: 'block' }); + + assert_equals(scrollTimeline.currentTime, null); +}, 'currentTime should be null when the source is not a scroller'); +</script> diff --git a/testing/web-platform/tests/scroll-animations/scroll-timelines/current-time-root-scroller.html b/testing/web-platform/tests/scroll-animations/scroll-timelines/current-time-root-scroller.html new file mode 100644 index 0000000000..be1d62bec5 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/scroll-timelines/current-time-root-scroller.html @@ -0,0 +1,49 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>ScrollTimeline current time algorithm - root scroller</title> +<link rel="help" href="https://wicg.github.io/scroll-animations/#current-time-algorithm"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="./testcommon.js"></script> + +<style> +html { + /* Ensure the document is scrollable. */ + min-height: 100%; + min-width: 100%; + padding-bottom: 100px; + padding-right: 100px; +} +</style> + +<script> +promise_test(async t => { + const scroller = document.scrollingElement; + // Allow layout to finish, otherwise the scroller isn't set up by the time + // we check the currentTime of the scroll timeline. + await waitForNextFrame(); + + const blockScrollTimeline = new ScrollTimeline( + { source: scroller, axis: 'block' }); + const inlineScrollTimeline = new ScrollTimeline( + { source: scroller, axis: 'inline' }); + + // Wait for new animation frame which allows the timeline to fully initialize + await waitForNextFrame(); + + // Unscrolled, both timelines should read a currentTime of 0. + assert_percents_equal(blockScrollTimeline.currentTime, 0); + assert_percents_equal(inlineScrollTimeline.currentTime, 0); + + // Now do some scrolling and make sure that the ScrollTimelines update. + scroller.scrollTop = 50; + scroller.scrollLeft = 75; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + + assert_percents_equal(blockScrollTimeline.currentTime, 50); + assert_percents_equal(inlineScrollTimeline.currentTime, 75); +}, 'currentTime calculates the correct time for a document.scrollingElement source'); +</script> diff --git a/testing/web-platform/tests/scroll-animations/scroll-timelines/current-time-writing-modes.html b/testing/web-platform/tests/scroll-animations/scroll-timelines/current-time-writing-modes.html new file mode 100644 index 0000000000..d18abcd48f --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/scroll-timelines/current-time-writing-modes.html @@ -0,0 +1,148 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>ScrollTimeline current time algorithm - interaction with writing modes</title> +<link rel="help" href="https://wicg.github.io/scroll-animations/#current-time-algorithm"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="./testcommon.js"></script> + +<body></body> + +<script> +'use strict'; + +promise_test(async t => { + const scrollerOverrides = new Map([['direction', 'rtl']]); + const scroller = setupScrollTimelineTest(scrollerOverrides); + const verticalScrollRange = scroller.scrollHeight - scroller.clientHeight; + const horizontalScrollRange = scroller.scrollWidth - scroller.clientWidth; + + const blockScrollTimeline = new ScrollTimeline( + {source: scroller, axis: 'block'}); + const inlineScrollTimeline = new ScrollTimeline( + {source: scroller, axis: 'inline'}); + const horizontalScrollTimeline = new ScrollTimeline( + {source: scroller, axis: 'horizontal'}); + const verticalScrollTimeline = new ScrollTimeline( + {source: scroller, axis: 'vertical'}); + + // Unscrolled, all timelines should read a current time of 0 even though the + // X-axis will have started at the right hand side for rtl. + assert_percents_equal(blockScrollTimeline.currentTime, 0, + 'Unscrolled block timeline'); + assert_percents_equal(inlineScrollTimeline.currentTime, 0, + 'Unscrolled inline timeline'); + assert_percents_equal(horizontalScrollTimeline.currentTime, 0, + 'Unscrolled horizontal timeline'); + assert_percents_equal(verticalScrollTimeline.currentTime, 0, + 'Unscrolled vertical timeline'); + + // The offset in the inline/horizontal direction should be inverted. The + // block/vertical direction should be unaffected. + scroller.scrollTop = 0.1 * verticalScrollRange; + scroller.scrollLeft = -0.8 * horizontalScrollRange; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + + assert_percents_equal(blockScrollTimeline.currentTime, 10, + 'Scrolled block timeline'); + assert_percents_equal(inlineScrollTimeline.currentTime, 80, + 'Scrolled inline timeline'); + assert_percents_equal(horizontalScrollTimeline.currentTime, 80, + 'Scrolled horizontal timeline'); + assert_percents_equal(verticalScrollTimeline.currentTime, 10, + 'Scrolled vertical timeline'); +}, 'currentTime handles direction: rtl correctly'); + +promise_test(async t => { + const scrollerOverrides = new Map([['writing-mode', 'vertical-rl']]); + const scroller = setupScrollTimelineTest(scrollerOverrides); + const verticalScrollRange = scroller.scrollHeight - scroller.clientHeight; + const horizontalScrollRange = scroller.scrollWidth - scroller.clientWidth; + + const blockScrollTimeline = new ScrollTimeline( + {source: scroller, axis: 'block'}); + const inlineScrollTimeline = new ScrollTimeline( + {source: scroller, axis: 'inline'}); + const horizontalScrollTimeline = new ScrollTimeline( + {source: scroller, axis: 'horizontal'}); + const verticalScrollTimeline = new ScrollTimeline( + {source: scroller, axis: 'vertical'}); + + // Unscrolled, all timelines should read a current time of 0 even though the + // X-axis will have started at the right hand side for vertical-rl. + assert_percents_equal(blockScrollTimeline.currentTime, 0, + 'Unscrolled block timeline'); + assert_percents_equal(inlineScrollTimeline.currentTime, 0, + 'Unscrolled inline timeline'); + assert_percents_equal(horizontalScrollTimeline.currentTime, 0, + 'Unscrolled horizontal timeline'); + assert_percents_equal(verticalScrollTimeline.currentTime, 0, + 'Unscrolled vertical timeline'); + + // For vertical-rl, the X-axis starts on the right-hand-side and is the block + // axis. The Y-axis is normal but is the inline axis. For the + // horizontal/vertical cases, horizontal starts on the right-hand-side and + // vertical is normal. + scroller.scrollTop = 0.1 * verticalScrollRange; + scroller.scrollLeft = -0.8 * horizontalScrollRange; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + + assert_percents_equal(blockScrollTimeline.currentTime, 80, + 'Scrolled block timeline'); + assert_percents_equal(inlineScrollTimeline.currentTime, 10, + 'Scrolled inline timeline'); + assert_percents_equal(horizontalScrollTimeline.currentTime, 80, + 'Scrolled horizontal timeline'); + assert_percents_equal(verticalScrollTimeline.currentTime, 10, + 'Scrolled vertical timeline'); +}, 'currentTime handles writing-mode: vertical-rl correctly'); + +promise_test(async t => { + const scrollerOverrides = new Map([['writing-mode', 'vertical-lr']]); + const scroller = setupScrollTimelineTest(scrollerOverrides); + const verticalScrollRange = scroller.scrollHeight - scroller.clientHeight; + const horizontalScrollRange = scroller.scrollWidth - scroller.clientWidth; + + const blockScrollTimeline = new ScrollTimeline( + {source: scroller, axis: 'block'}); + const inlineScrollTimeline = new ScrollTimeline( + {source: scroller, axis: 'inline'}); + const horizontalScrollTimeline = new ScrollTimeline( + {source: scroller, axis: 'horizontal'}); + const verticalScrollTimeline = new ScrollTimeline( + {source: scroller, axis: 'vertical'}); + + // Unscrolled, all timelines should read a current time of 0. + assert_percents_equal(blockScrollTimeline.currentTime, 0, + 'Unscrolled block timeline'); + assert_percents_equal(inlineScrollTimeline.currentTime, 0, + 'Unscrolled inline timeline'); + assert_percents_equal(horizontalScrollTimeline.currentTime, 0, + 'Unscrolled horizontal timeline'); + assert_percents_equal(verticalScrollTimeline.currentTime, 0, + 'Unscrolled vertical timeline'); + + // For vertical-lr, both axes start at their 'normal' positions but the X-axis + // is the block direction and the Y-axis is the inline direction. This does + // not affect horizontal/vertical. + scroller.scrollTop = 0.1 * verticalScrollRange; + scroller.scrollLeft = 0.2 * horizontalScrollRange; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + + assert_percents_equal(blockScrollTimeline.currentTime, 20, + 'Scrolled block timeline'); + assert_percents_equal(inlineScrollTimeline.currentTime, 10, + 'Scrolled inline timeline'); + assert_percents_equal(horizontalScrollTimeline.currentTime, 20, + 'Scrolled horizontal timeline'); + assert_percents_equal(verticalScrollTimeline.currentTime, 10, + 'Scrolled vertical timeline'); +}, 'currentTime handles writing-mode: vertical-lr correctly'); +</script> diff --git a/testing/web-platform/tests/scroll-animations/scroll-timelines/effect-updateTiming.html b/testing/web-platform/tests/scroll-animations/scroll-timelines/effect-updateTiming.html new file mode 100644 index 0000000000..78ec8e1de5 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/scroll-timelines/effect-updateTiming.html @@ -0,0 +1,602 @@ +<!doctype html> +<meta charset=utf-8> +<title>Scroll based animation: AnimationEffect.updateTiming</title> +<!-- Adapted to progressed based scroll animations from "wpt\web-animations\interfaces\AnimationEffect\updateTiming.html" --> +<link rel="help" href="https://drafts.csswg.org/web-animations-1/#dom-animationeffect-updatetiming"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="testcommon.js"></script> +<script src="/web-animations/resources/easing-tests.js"></script> +<script src="/web-animations/resources/timing-tests.js"></script> +<style> + .scroller { + overflow: auto; + height: 100px; + width: 100px; + will-change: transform; + } + .contents { + height: 1000px; + width: 100%; + } +</style> +<body> +<div id="log"></div> +<script> +'use strict'; + +// ------------------------------ +// delay +// ------------------------------ + +promise_test(async t => { + const anim = createScrollLinkedAnimationWithTiming(t, {duration: 1000, delay: 200}) + anim.play(); + + assert_equals(anim.effect.getTiming().delay, 200, 'initial delay 200'); + assert_equals(anim.effect.getComputedTiming().delay, 200, + 'getComputedTiming() initially delay 200'); + + anim.effect.updateTiming({ delay: 100 }); + assert_equals(anim.effect.getTiming().delay, 100, 'set delay 100'); + assert_equals(anim.effect.getComputedTiming().delay, 100, + 'getComputedTiming() after set delay 100'); +}, 'Allows setting the delay to a positive number'); + +test(t => { + const anim = createScrollLinkedAnimationWithTiming(t, {duration: 100, delay: -100}) + anim.play(); + anim.effect.updateTiming({ delay: -100 }); + assert_equals(anim.effect.getTiming().delay, -100, 'set delay -100'); + assert_equals(anim.effect.getComputedTiming().delay, -100, + 'getComputedTiming() after set delay -100'); + assert_percents_equal(anim.effect.getComputedTiming().endTime, 0, + 'getComputedTiming().endTime after set delay -100'); +}, 'Allows setting the delay to a negative number'); + +test(t => { + const anim = createScrollLinkedAnimationWithTiming(t, {duration: 100}) + anim.play(); + anim.effect.updateTiming({ delay: 100 }); + assert_equals(anim.effect.getComputedTiming().progress, null); + assert_equals(anim.effect.getComputedTiming().currentIteration, null); +}, 'Allows setting the delay of an animation in progress: positive delay that' + + ' causes the animation to be no longer in-effect'); + +test(t => { + const anim = createScrollLinkedAnimationWithTiming(t, { fill: 'both', duration: 100 }) + anim.play(); + anim.effect.updateTiming({ delay: -50 }); + assert_equals(anim.effect.getComputedTiming().progress, 0.5); +}, 'Allows setting the delay of an animation in progress: negative delay that' + + ' seeks into the active interval'); + +test(t => { + const anim = createScrollLinkedAnimationWithTiming(t, { fill: 'both', duration: 100 }) + anim.play(); + anim.effect.updateTiming({ delay: -100 }); + assert_equals(anim.effect.getComputedTiming().progress, 1); + assert_equals(anim.effect.getComputedTiming().currentIteration, 0); +}, 'Allows setting the delay of an animation in progress: large negative delay' + + ' that causes the animation to be finished'); + +for (const invalid of gBadDelayValues) { + test(t => { + const anim = createScrollLinkedAnimationWithTiming(t) + anim.play(); + assert_throws_js(TypeError, () => { + anim.effect.updateTiming({ delay: invalid }); + }); + }, `Throws when setting invalid delay value: ${invalid}`); +} + + +// ------------------------------ +// endDelay +// ------------------------------ + +test(t => { + const anim = createScrollLinkedAnimationWithTiming(t, { duration: 2000 }) + anim.play(); + anim.effect.updateTiming({ endDelay: 123.45 }); + assert_time_equals_literal(anim.effect.getTiming().endDelay, 123.45, + 'set endDelay 123.45'); + assert_time_equals_literal(anim.effect.getComputedTiming().endDelay, 123.45, + 'getComputedTiming() after set endDelay 123.45'); +}, 'Allows setting the endDelay to a positive number'); + +test(t => { + const anim = createScrollLinkedAnimationWithTiming(t, { duration: 2000 }) + anim.play(); + anim.effect.updateTiming({ endDelay: -1000 }); + assert_equals(anim.effect.getTiming().endDelay, -1000, 'set endDelay -1000'); + assert_equals(anim.effect.getComputedTiming().endDelay, -1000, + 'getComputedTiming() after set endDelay -1000'); +}, 'Allows setting the endDelay to a negative number'); + +test(t => { + const anim = createScrollLinkedAnimationWithTiming(t, { duration: 2000 }) + anim.play(); + assert_throws_js(TypeError, () => { + anim.effect.updateTiming({ endDelay: Infinity }); + }); +}, 'Throws when setting the endDelay to infinity'); + +test(t => { + const anim = createScrollLinkedAnimationWithTiming(t, { duration: 2000 }) + anim.play(); + assert_throws_js(TypeError, () => { + anim.effect.updateTiming({ endDelay: -Infinity }); + }); +}, 'Throws when setting the endDelay to negative infinity'); + + +// ------------------------------ +// fill +// ------------------------------ + +for (const fill of ['none', 'forwards', 'backwards', 'both']) { + test(t => { + const anim = createScrollLinkedAnimationWithTiming(t, { duration: 100 }) + anim.play(); + anim.effect.updateTiming({ fill }); + assert_equals(anim.effect.getTiming().fill, fill, 'set fill ' + fill); + assert_equals(anim.effect.getComputedTiming().fill, fill, + 'getComputedTiming() after set fill ' + fill); + }, `Allows setting the fill to '${fill}'`); +} + + +// ------------------------------ +// iterationStart +// ------------------------------ + +test(t => { + const anim = createScrollLinkedAnimationWithTiming(t, { iterationStart: 0.2, + iterations: 1, + fill: 'both', + duration: 100, + delay: 1 }) + anim.play(); + anim.effect.updateTiming({ iterationStart: 2.5 }); + assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.5); + assert_equals(anim.effect.getComputedTiming().currentIteration, 2); +}, 'Allows setting the iterationStart of an animation in progress:' + + ' backwards-filling'); + +test(t => { + const anim = createScrollLinkedAnimationWithTiming(t, { iterationStart: 0.2, + iterations: 1, + fill: 'both', + duration: 100, + delay: 0 }) + anim.play(); + anim.effect.updateTiming({ iterationStart: 2.5 }); + assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.5); + assert_equals(anim.effect.getComputedTiming().currentIteration, 2); +}, 'Allows setting the iterationStart of an animation in progress:' + + ' active phase'); + +test(t => { + const anim = createScrollLinkedAnimationWithTiming(t, { iterationStart: 0.3, + iterations: 1, + fill: 'both', + duration: 200, + delay: 0 }) + anim.play(); + assert_percents_equal(anim.currentTime, 0); + assert_percents_equal(anim.effect.getComputedTiming().localTime, 0, "localTime"); + assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.3); + assert_equals(anim.effect.getComputedTiming().currentIteration, 0); + + anim.finish(); + assert_percents_equal(anim.currentTime, 100); + assert_percents_equal(anim.effect.getComputedTiming().localTime, 100, "localTime"); + assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.3); + assert_equals(anim.effect.getComputedTiming().currentIteration, 1); + + anim.effect.updateTiming({ iterationStart: 2.5 }); + assert_percents_equal(anim.currentTime, 100); + assert_percents_equal(anim.effect.getComputedTiming().localTime, 100, "localTime"); + assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.5); + assert_equals(anim.effect.getComputedTiming().currentIteration, 3); +}, 'Allows setting the iterationStart of an animation in progress:' + + ' forwards-filling'); + +for (const invalid of gBadIterationStartValues) { + test(t => { + const anim = createScrollLinkedAnimationWithTiming(t) + anim.play(); + assert_throws_js(TypeError, () => { + anim.effect.updateTiming({ iterationStart: invalid }); + }, `setting ${invalid}`); + }, `Throws when setting invalid iterationStart value: ${invalid}`); +} + +// ------------------------------ +// iterations +// ------------------------------ + +test(t => { + const anim = createScrollLinkedAnimationWithTiming(t, { duration: 2000 }) + anim.play(); + anim.effect.updateTiming({ iterations: 2 }); + assert_equals(anim.effect.getTiming().iterations, 2, 'set duration 2'); + assert_equals(anim.effect.getComputedTiming().iterations, 2, + 'getComputedTiming() after set iterations 2'); +}, 'Allows setting iterations to a double value'); + +test(t => { + const anim = createScrollLinkedAnimationWithTiming(t, { duration: 2000 }) + anim.play(); + assert_throws_js(TypeError, () => { + anim.effect.updateTiming({ iterations: Infinity }); + }, "test"); +}, `Throws when setting iterations to Infinity`); + + +// progress based animations behave a bit differently than time based animations +// when changing iterations. +test(t => { + const anim = createScrollLinkedAnimationWithTiming(t, { duration: 100000, fill: 'both' }) + anim.play(); + anim.finish(); + + assert_equals(anim.effect.getComputedTiming().progress, 1, + 'progress when animation is finished'); + assert_percents_equal(anim.effect.getComputedTiming().duration, 100, + 'duration when animation is finished'); + assert_equals(anim.effect.getComputedTiming().currentIteration, 0, + 'current iteration when animation is finished'); + + anim.effect.updateTiming({ iterations: 2 }); + + assert_equals(anim.effect.getComputedTiming().progress, 1, + 'progress after adding an iteration'); + assert_percents_equal(anim.effect.getComputedTiming().duration, 50, + 'duration after adding an iteration'); + assert_equals(anim.effect.getComputedTiming().currentIteration, 1, + 'current iteration after adding an iteration'); + + anim.effect.updateTiming({ iterations: 4 }); + + assert_equals(anim.effect.getComputedTiming().progress, 1, + 'progress after setting iterations to 4'); + assert_percents_equal(anim.effect.getComputedTiming().duration, 25, + 'duration after setting iterations to 4'); + assert_equals(anim.effect.getComputedTiming().currentIteration, 3, + 'current iteration after setting iterations to 4'); + + anim.effect.updateTiming({ iterations: 0 }); + + assert_equals(anim.effect.getComputedTiming().progress, 0, + 'progress after setting iterations to zero'); + assert_percents_equal(anim.effect.getComputedTiming().duration, 0, + 'duration after setting iterations to zero'); + assert_equals(anim.effect.getComputedTiming().currentIteration, 0, + 'current iteration after setting iterations to zero'); +}, 'Allows setting the iterations of an animation in progress'); + +// Added test for checking duration "auto" +test(t => { + const anim = createScrollLinkedAnimationWithTiming(t, { fill: 'both' }) + anim.play(); + anim.finish(); + + assert_equals(anim.effect.getComputedTiming().progress, 1, + 'progress when animation is finished'); + assert_percents_equal(anim.effect.getComputedTiming().duration, 100, + 'duration when animation is finished'); + assert_equals(anim.effect.getComputedTiming().currentIteration, 0, + 'current iteration when animation is finished'); + + anim.effect.updateTiming({ iterations: 2 }); + + assert_equals(anim.effect.getComputedTiming().progress, 1, + 'progress after adding an iteration'); + assert_percents_equal(anim.effect.getComputedTiming().duration, 50, + 'duration after adding an iteration'); + assert_equals(anim.effect.getComputedTiming().currentIteration, 1, + 'current iteration after adding an iteration'); + + anim.effect.updateTiming({ iterations: 4 }); + + assert_equals(anim.effect.getComputedTiming().progress, 1, + 'progress after setting iterations to 4'); + assert_percents_equal(anim.effect.getComputedTiming().duration, 25, + 'duration after setting iterations to 4'); + assert_equals(anim.effect.getComputedTiming().currentIteration, 3, + 'current iteration after setting iterations to 4'); + + anim.effect.updateTiming({ iterations: 0 }); + + assert_equals(anim.effect.getComputedTiming().progress, 0, + 'progress after setting iterations to zero'); + assert_percents_equal(anim.effect.getComputedTiming().duration, 0, + 'duration after setting iterations to zero'); + assert_equals(anim.effect.getComputedTiming().currentIteration, 0, + 'current iteration after setting iterations to zero'); +}, 'Allows setting the iterations of an animation in progress with duration "auto"'); + + +// ------------------------------ +// duration +// ------------------------------ +// adapted for progress based animations +const gGoodDurationValuesForProgressBased = [ + // until duration returns a CSSNumberish which can handle percentages, 100% + // will be represented as 100 + { specified: 123.45, computed: 100 }, + { specified: 'auto', computed: 100 }, +]; + +for (const duration of gGoodDurationValuesForProgressBased) { + test(t => { + const anim = createScrollLinkedAnimationWithTiming(t, 2000) + anim.play(); + anim.effect.updateTiming({ duration: duration.specified }); + if (typeof duration.specified === 'number') { + assert_time_equals_literal(anim.effect.getTiming().duration, + duration.specified, + 'Updates specified duration'); + } else { + assert_equals(anim.effect.getTiming().duration, duration.specified, + 'Updates specified duration'); + } + assert_percents_equal(anim.effect.getComputedTiming().duration, + duration.computed, + 'Updates computed duration'); + }, `Allows setting the duration to ${duration.specified}`); +} + +// adapted for progress based animations +const gBadDurationValuesForProgressBased = [ + -1, NaN, Infinity, -Infinity, 'abc', '100' +]; + +for (const invalid of gBadDurationValuesForProgressBased) { + test(t => { + assert_throws_js(TypeError, () => { + const anim = createScrollLinkedAnimationWithTiming(t, { duration: invalid }) + anim.play(); + }); + }, 'Throws when setting invalid duration: ' + + (typeof invalid === 'string' ? `"${invalid}"` : invalid)); +} + +test(t => { + const anim = createScrollLinkedAnimationWithTiming(t, { duration: 100000, fill: 'both' }) + anim.play(); + anim.finish(); + assert_equals(anim.effect.getComputedTiming().progress, 1, + 'progress when animation is finished'); + anim.effect.updateTiming({ duration: anim.effect.getTiming().duration * 2 }); + assert_time_equals_literal(anim.effect.getComputedTiming().progress, 1, + 'progress after doubling the duration'); + anim.effect.updateTiming({ duration: 0 }); + assert_equals(anim.effect.getComputedTiming().progress, 1, + 'progress after setting duration to zero'); + anim.effect.updateTiming({ duration: 'auto' }); + assert_equals(anim.effect.getComputedTiming().progress, 1, + 'progress after setting duration to \'auto\''); +}, 'Allows setting the duration of an animation in progress'); + +promise_test(t => { + const anim = createScrollLinkedAnimationWithTiming(t, { duration: 100000, fill: 'both' }) + anim.play(); + return anim.ready.then(() => { + const originalStartTime = anim.startTime; + const originalCurrentTime = anim.currentTime; + assert_percents_equal( + anim.effect.getComputedTiming().duration, + 100, + 'Initial duration should be as set on KeyframeEffect' + ); + + anim.effect.updateTiming({ duration: 200000 }); + assert_percents_equal( + anim.effect.getComputedTiming().duration, + 100, + 'Effect duration should remain at 100% for progress based animations' + ); + assert_percents_equal(anim.startTime, originalStartTime, + 'startTime should be unaffected by changing effect ' + + 'duration'); + + assert_percents_equal(anim.currentTime, originalCurrentTime, + 'currentTime should be unaffected by changing ' + + 'effect duration'); + }); +}, 'Allows setting the duration of an animation in progress such that the' + + ' the start and current time do not change'); + + +// ------------------------------ +// direction +// ------------------------------ + +test(t => { + const anim = createScrollLinkedAnimationWithTiming(t, { duration: 2000 }) + anim.play(); + + const directions = ['normal', 'reverse', 'alternate', 'alternate-reverse']; + for (const direction of directions) { + anim.effect.updateTiming({ direction: direction }); + assert_equals(anim.effect.getTiming().direction, direction, + `set direction to ${direction}`); + } +}, 'Allows setting the direction to each of the possible keywords'); + +promise_test(async t => { + const anim = createScrollLinkedAnimationWithTiming(t, { duration: 10000, direction: 'normal' }); + + const scroller = anim.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + anim.play(); + await anim.ready; + scroller.scrollTop = maxScroll * 0.07 + await waitForNextFrame(); + + assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.07, + 'progress before updating direction'); + + anim.effect.updateTiming({ direction: 'reverse' }); + + assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.93, + 'progress after updating direction'); +}, 'Allows setting the direction of an animation in progress from \'normal\' to' + + ' \'reverse\''); + +test(t => { + const anim = createScrollLinkedAnimationWithTiming(t, { duration: 10000, direction: 'normal' }); + anim.play(); + assert_equals(anim.effect.getComputedTiming().progress, 0, + 'progress before updating direction'); + + anim.effect.updateTiming({ direction: 'reverse' }); + + assert_equals(anim.effect.getComputedTiming().progress, 1, + 'progress after updating direction'); +}, 'Allows setting the direction of an animation in progress from \'normal\' to' + + ' \'reverse\' while at start of active interval'); + +test(t => { + const anim = createScrollLinkedAnimationWithTiming(t, { fill: 'backwards', + duration: 10000, + delay: 10000, + direction: 'normal' }); + anim.play(); + assert_equals(anim.effect.getComputedTiming().progress, 0, + 'progress before updating direction'); + + anim.effect.updateTiming({ direction: 'reverse' }); + + assert_equals(anim.effect.getComputedTiming().progress, 1, + 'progress after updating direction'); +}, 'Allows setting the direction of an animation in progress from \'normal\' to' + + ' \'reverse\' while filling backwards'); + +promise_test(async t => { + const anim = createScrollLinkedAnimationWithTiming(t, { iterations: 2, + duration: 10000, + direction: 'normal' }); + const scroller = anim.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + anim.play(); + await anim.ready; + scroller.scrollTop = maxScroll * 0.17 // 34% through first iteration + await waitForNextFrame(); + + assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.34, + 'progress before updating direction'); + + anim.effect.updateTiming({ direction: 'alternate' }); + + assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.34, + 'progress after updating direction'); +}, 'Allows setting the direction of an animation in progress from \'normal\' to' + + ' \'alternate\''); + +promise_test(async t => { + const anim = createScrollLinkedAnimationWithTiming(t, { iterations: 2, + duration: 10000, + direction: 'alternate' }); + const scroller = anim.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + anim.play(); + await anim.ready; + scroller.scrollTop = maxScroll * 0.17 + await waitForNextFrame(); + // anim.currentTime = 17000; // 34% through first iteration + assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.34, + 'progress before updating direction'); + + anim.effect.updateTiming({ direction: 'alternate-reverse' }); + + assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.66, + 'progress after updating direction'); +}, 'Allows setting the direction of an animation in progress from \'alternate\'' + + ' to \'alternate-reverse\''); + + +// ------------------------------ +// easing +// ------------------------------ + +async function assert_progress(animation, scrollPercent, easingFunction) { + const scroller = animation.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = maxScroll * scrollPercent + await waitForNextFrame(); + assert_approx_equals(animation.effect.getComputedTiming().progress, + easingFunction(scrollPercent), + 0.01, + 'The progress of the animation should be approximately' + + ` ${easingFunction(scrollPercent)} at ${scrollPercent}%`); +} + +for (const options of gEasingTests) { + promise_test(async t => { + const anim = createScrollLinkedAnimationWithTiming(t, { duration: 100000, + fill: 'forwards' }); + anim.play(); + anim.effect.updateTiming({ easing: options.easing }); + assert_equals(anim.effect.getTiming().easing, + options.serialization || options.easing); + + const easing = options.easingFunction; + await assert_progress(anim, 0, easing); + await assert_progress(anim, 0.25, easing); + await assert_progress(anim, 0.5, easing); + await assert_progress(anim, 0.75, easing); + await assert_progress(anim, 1, easing); + }, `Allows setting the easing to a ${options.desc}`); +} + +for (const easing of gRoundtripEasings) { + test(t => { + const anim = createScrollLinkedAnimationWithTiming(t); + anim.play(); + anim.effect.updateTiming({ easing: easing }); + assert_equals(anim.effect.getTiming().easing, easing); + }, `Updates the specified value when setting the easing to '${easing}'`); +} + +// Because of the delay being so large, this progress based animation is always +// in the finished state with progress 1. Included here because it is in the +// original test file for time based animations. +promise_test(async t => { + const delay = 1000000; + + const anim = createScrollLinkedAnimationWithTiming(t, + { duration: 1000000, fill: 'both', delay: delay, easing: 'steps(2, start)' }); + + const scroller = anim.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + anim.play(); + await anim.ready; + + anim.effect.updateTiming({ easing: 'steps(2, end)' }); + assert_equals(anim.effect.getComputedTiming().progress, 0, + 'easing replace to steps(2, end) at before phase'); + + scroller.scrollTop = maxScroll * 0.875 + await waitForNextFrame(); + + assert_equals(anim.effect.getComputedTiming().progress, 0.5, + 'change currentTime to active phase'); + + anim.effect.updateTiming({ easing: 'steps(2, start)' }); + assert_equals(anim.effect.getComputedTiming().progress, 1, + 'easing replace to steps(2, start) at active phase'); + + scroller.scrollTop = maxScroll * 1.25 + await waitForNextFrame(); + + anim.effect.updateTiming({ easing: 'steps(2, end)' }); + assert_equals(anim.effect.getComputedTiming().progress, 1, + 'easing replace to steps(2, end) again at after phase'); +}, 'Allows setting the easing of an animation in progress'); +</script> +</body> diff --git a/testing/web-platform/tests/scroll-animations/scroll-timelines/finish-animation.html b/testing/web-platform/tests/scroll-animations/scroll-timelines/finish-animation.html new file mode 100644 index 0000000000..fd768651b4 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/scroll-timelines/finish-animation.html @@ -0,0 +1,449 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Finishing an animation</title> +<link rel="help" + href="https://drafts.csswg.org/web-animations/#finishing-an-animation-section"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="testcommon.js"></script> +<style> +.scroller { + overflow: auto; + height: 200px; + width: 100px; + will-change: transform; +} + +.contents { + height: 1000px; + width: 100%; +} +</style> +<body> +<div id="log"></div> +<script> +'use strict'; + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + animation.playbackRate = 0; + + assert_throws_dom('InvalidStateError', () => { + animation.finish(); + }); +}, 'Finishing an animation with a zero playback rate throws'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + animation.finish(); + + assert_percents_equal(animation.currentTime, 100, + 'After finishing, the currentTime should be set to the end of the' + + ' active duration'); +}, 'Finishing an animation seeks to the end time'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + // 1% past effect end + animation.currentTime = CSSNumericValue.parse(animation.effect.getComputedTiming().endTime.value + 1 + "%"); + animation.finish(); + + assert_percents_equal(animation.currentTime, 100, + 'After finishing, the currentTime should be set back to the end of the' + + ' active duration'); +}, 'Finishing an animation with a current time past the effect end jumps' + + ' back to the end'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + scroller.scrollTop = maxScroll; + await animation.finished; + + animation.playbackRate = -1; + animation.finish(); + + assert_percents_equal(animation.currentTime, 0, + 'After finishing a reversed animation the ' + + 'currentTime should be set to zero'); +}, 'Finishing a reversed animation jumps to zero time'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + animation.currentTime = CSSNumericValue.parse("100%"); + await animation.finished; + + animation.playbackRate = -1; + animation.currentTime = CSSNumericValue.parse("-1000%"); + animation.finish(); + + assert_percents_equal(animation.currentTime, 0, + 'After finishing a reversed animation the ' + + 'currentTime should be set back to zero'); +}, 'Finishing a reversed animation with a current time less than zero' + + ' makes it jump back to zero'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + animation.playbackRate = 0.5; + animation.finish(); + + assert_false(animation.pending); + assert_equals(animation.playState, 'finished', + 'The play state of a play-pending animation should become ' + + '"finished"'); + assert_percents_equal(animation.startTime, + animation.timeline.currentTime.value - 100 / 0.5, + 'The start time of a play-pending animation should ' + + 'be set'); +}, 'Finishing an animation while play-pending resolves the pending' + + ' task immediately'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + // Make the scroll timeline inactive. + scroller.style.overflow = 'visible'; + scroller.scrollTop; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + animation.finish(); + + await animation.finished; + + assert_true(animation.pending); + assert_equals(animation.playState, 'finished', + 'The play state of a play-pending animation should become ' + + '"finished"'); + assert_percents_equal(animation.currentTime, 100, + 'The current time of a play-pending animation should ' + + 'be set to the end of the active duration'); + assert_equals(animation.startTime, null, + 'The start time of a finished play-pending animation should ' + + 'be be unresolved'); +}, 'Finishing an animation attached to inactive timeline while play-pending ' + + 'doesn\'t resolves the pending task'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + let resolvedFinished = false; + animation.finished.then(() => { + resolvedFinished = true; + }); + + await animation.ready; + + animation.finish(); + await Promise.resolve(); + + assert_true(resolvedFinished, 'finished promise should be resolved'); +}, 'Finishing an animation resolves the finished promise synchronously'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + const promise = animation.ready; + let readyResolved = false; + + animation.finish(); + animation.ready.then(() => { readyResolved = true; }); + + const promiseResult = await animation.finished; + await animation.ready; + + assert_equals(promiseResult, animation); + assert_equals(animation.ready, promise); + assert_true(readyResolved); +}, 'A pending ready promise is resolved and not replaced when the animation' + + ' is finished'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + animation.effect.target.remove(); + + const eventWatcher = new EventWatcher(t, animation, 'finish'); + + await animation.ready; + animation.finish(); + + await eventWatcher.wait_for('finish'); + assert_equals(animation.effect.target.parentNode, null, + 'finish event should be fired for the animation on an orphaned element'); +}, 'Finishing an animation fires finish event on orphaned element'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + await animation.ready; + + const originalFinishPromise = animation.finished; + + animation.cancel(); + assert_equals(animation.startTime, null); + assert_equals(animation.currentTime, null); + + const resolvedFinishPromise = animation.finished; + assert_not_equals(originalFinishPromise, resolvedFinishPromise, + 'Canceling an animation should create a new finished promise'); + + animation.finish(); + assert_equals(animation.playState, 'finished', + 'The play state of a canceled animation should become ' + + '"finished"'); + assert_percents_equal(animation.startTime, + animation.timeline.currentTime.value - 100, + 'The start time of a finished animation should be set'); + assert_percents_equal(animation.currentTime, 100, + 'Hold time should be set to end boundary of the ' + + 'animation'); + +}, 'Finishing a canceled animation sets the current and start times'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.25 * maxScroll; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + + const eventWatcher = new EventWatcher(t, animation, 'finish'); + animation.finish(); + await animation.finished; + const finishEvent = await eventWatcher.wait_for('finish'); + assert_equals(animation.playState, 'finished', + 'Animation is finished.'); + assert_percents_equal(animation.currentTime, 100, + 'The current time is the end of the active duration in finished state.'); + assert_percents_equal(animation.startTime, -75, + 'The start time is calculated to match the current time.'); + assert_percents_equal(finishEvent.currentTime, 100, + 'event.currentTime is the animation current time.'); + assert_percents_equal(finishEvent.timelineTime, 25, + 'event.timelineTime is timeline.currentTime'); +}, 'Finishing idle animation produces correct state and fires finish event.'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + // Make the scroll timeline inactive. + scroller.style.overflow = 'visible'; + await waitForNextFrame(); + assert_equals(animation.timeline.currentTime, null, + 'Sanity check the timeline is inactive.'); + animation.finish(); + assert_equals(animation.playState, 'paused', 'Animation is paused.'); +}, 'Finishing idle animation attached to inactive timeline pauses the ' + + 'animation.'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.25 * maxScroll; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + await animation.ready; + + const eventWatcher = new EventWatcher(t, animation, 'finish'); + animation.finish(); + await animation.finished; + const finishEvent = await eventWatcher.wait_for('finish'); + assert_equals(animation.playState, 'finished', + 'Animation is finished.'); + assert_percents_equal(animation.currentTime, 100, + 'The current time is the end of active duration in finished state.'); + assert_percents_equal(animation.startTime, -75, + 'The start time is calculated to match animation current time.'); + assert_percents_equal(finishEvent.currentTime, 100, + 'event.currentTime is the animation current time.'); + assert_percents_equal(finishEvent.timelineTime, 25, + 'event.timelineTime is timeline.currentTime'); +}, 'Finishing running animation produces correct state and fires finish event.'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + await animation.ready; + + // Make the scroll timeline inactive. + scroller.style.overflow = 'visible'; + scroller.scrollTop; + await waitForNextFrame(); + assert_equals(animation.timeline.currentTime, null, + 'Sanity check the timeline is inactive.'); + assert_equals(animation.playState, 'running', + 'Sanity check the animation is running.'); + + animation.finish(); + assert_equals(animation.playState, 'paused', 'Animation is paused.'); +}, 'Finishing running animation attached to inactive timeline pauses the ' + + 'animation.'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.pause(); + await animation.ready; + + animation.finish(); + + assert_equals(animation.playState, 'finished', + 'The play state of a paused animation should become ' + + '"finished"'); + assert_percents_equal(animation.startTime, + animation.timeline.currentTime.value - 100, + 'The start time of a paused animation should be set'); +}, 'Finishing a paused animation resolves the start time'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + // Update playbackRate so we can test that the calculated startTime + // respects it + animation.playbackRate = 2; + animation.pause(); + // While animation is still pause-pending call finish() + animation.finish(); + + assert_false(animation.pending); + assert_equals(animation.playState, 'finished', + 'The play state of a pause-pending animation should become ' + + '"finished"'); + assert_percents_equal(animation.startTime, + animation.timeline.currentTime.value - 100 / 2, + 'The start time of a pause-pending animation should ' + + 'be set'); +}, 'Finishing a pause-pending animation resolves the pending task' + + ' immediately and update the start time'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + animation.playbackRate = -2; + animation.pause(); + animation.finish(); + + assert_false(animation.pending); + assert_equals(animation.playState, 'finished', + 'The play state of a pause-pending animation should become ' + + '"finished"'); + assert_percents_equal(animation.startTime, + animation.timeline.currentTime, + 'The start time of a pause-pending animation should ' + + 'be set'); +}, 'Finishing a pause-pending animation with negative playback rate' + + ' resolves the pending task immediately'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + await animation.ready; + + animation.pause(); + animation.play(); + // We are now in the unusual situation of being play-pending whilst having + // a resolved start time. Check that finish() still triggers a transition + // to the finished state immediately. + animation.finish(); + + assert_equals(animation.playState, 'finished', + 'After aborting a pause then finishing an animation its play ' + + 'state should become "finished" immediately'); +}, 'Finishing an animation during an aborted pause makes it finished' + + ' immediately'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + await animation.ready; + + animation.updatePlaybackRate(2); + assert_true(animation.pending); + + animation.finish(); + assert_false(animation.pending); + assert_equals(animation.playbackRate, 2); + assert_percents_equal(animation.currentTime, 100); +}, 'A pending playback rate should be applied immediately when an animation' + + ' is finished'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + await animation.ready; + + animation.updatePlaybackRate(0); + + assert_throws_dom('InvalidStateError', () => { + animation.finish(); + }); +}, 'An exception should be thrown if the effective playback rate is zero'); +</script> +</body> diff --git a/testing/web-platform/tests/scroll-animations/scroll-timelines/idlharness.window.js b/testing/web-platform/tests/scroll-animations/scroll-timelines/idlharness.window.js new file mode 100644 index 0000000000..90157580ce --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/scroll-timelines/idlharness.window.js @@ -0,0 +1,16 @@ +// META: script=/resources/WebIDLParser.js +// META: script=/resources/idlharness.js + +'use strict'; + +idl_test( + ['scroll-animations'], + // The css-pseudo dependency shouldn't be necessary, but is: + // https://github.com/web-platform-tests/wpt/issues/12574 + ['web-animations', 'css-pseudo', 'dom'], + idl_array => { + idl_array.add_objects({ + ScrollTimeline: ['new ScrollTimeline()'], + }); + } +); diff --git a/testing/web-platform/tests/scroll-animations/scroll-timelines/layout-changes-on-percentage-based-timeline.html b/testing/web-platform/tests/scroll-animations/scroll-timelines/layout-changes-on-percentage-based-timeline.html new file mode 100644 index 0000000000..c5a46a501e --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/scroll-timelines/layout-changes-on-percentage-based-timeline.html @@ -0,0 +1,84 @@ +<html class="reftest-wait"> +<title>Layout changes on percentage-based scroll timeline</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations/"> +<meta name="assert" content="Scroll timeline should properly handle +layout changes on percentage-based scroll offset"> +<link rel="match" href="animation-ref.html"> + +<script src="/web-animations/testcommon.js"></script> +<script src="/common/reftest-wait.js"></script> + +<style> + #box { + width: 100px; + height: 100px; + background-color: green; + } + + #covered { + width: 100px; + height: 100px; + background-color: red; + color: red; + } + + #scroller { + overflow: auto; + height: 100px; + width: 100px; + will-change: transform; + } + + #contents { + height: 500px; + width: 100%; + } + + #spacer { + height: 80px; + } + + .invisible { + display: none; + } +</style> + +<div id="box"></div> +<div id="covered">Scrolling Test</div> +<div id="scroller"> + <div id="contents"></div> + <div id="spacer" class="invisible"></div> +</div> + +<script> + const box = document.getElementById('box'); + const effect = new KeyframeEffect(box, + [ + { transform: 'translateY(0)', opacity: 1 }, + { transform: 'translateY(200px)', opacity: 0 } + ] + ); + + const scroller = document.getElementById('scroller'); + const timeline = new ScrollTimeline({ + source: scroller + }); + const animation = new Animation(effect, timeline); + animation.play(); + animation.ready.then(_ => { + // Moves the scroller to the end point (240px). + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = maxScroll * 0.6; + + // Makes sure that the animation runs on compositor with current scroll offset + waitForAnimationFrames(2).then(_ => { + // Adds 80px to scroll height which pushes scroll progress back to 50%. + const spacer = document.getElementById('spacer'); + spacer.classList.remove('invisible'); + // Makes sure that the change is propagated to the compositor. + waitForAnimationFrames(2).then(_ => { + takeScreenshot(); + }); + }); + }); +</script> diff --git a/testing/web-platform/tests/scroll-animations/scroll-timelines/null-scroll-source-crash.html b/testing/web-platform/tests/scroll-animations/scroll-timelines/null-scroll-source-crash.html new file mode 100644 index 0000000000..53ad0d9285 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/scroll-timelines/null-scroll-source-crash.html @@ -0,0 +1,24 @@ +<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=1088319"> +<meta name="assert" content="Playing animation with null scroll source should not crash."> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<style> + html { + overflow: scroll; + } + + body { + overflow: scroll; + } +</style> +<div id="box"></div> +<script> + test(() => { + const effect = new KeyframeEffect(box, []); + const timeline = new ScrollTimeline(); + const animation = new Animation(effect, timeline); + assert_equals(document.scrollingElement, null, + "This test relies on scrolling element being nil"); + animation.play(); + }, "Playing animation with null scroll source should not crash"); +</script>
\ No newline at end of file diff --git a/testing/web-platform/tests/scroll-animations/scroll-timelines/pause-animation.html b/testing/web-platform/tests/scroll-animations/scroll-timelines/pause-animation.html new file mode 100644 index 0000000000..9b263f843d --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/scroll-timelines/pause-animation.html @@ -0,0 +1,210 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Pausing an animation</title> +<link rel="help" + href="https://drafts.csswg.org/web-animations/#pausing-an-animation-section"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="testcommon.js"></script> +<style> +.scroller { + overflow: auto; + height: 100px; + width: 100px; + will-change: transform; +} + +.contents { + height: 1000px; + width: 100%; +} +</style> +<body> +<script> +'use strict'; + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + await animation.ready; + + const startTimeBeforePausing = animation.startTime; + + animation.pause(); + assert_percents_equal(animation.startTime, startTimeBeforePausing, + 'The start time does not change when pausing-pending'); + + await animation.ready; + + assert_equals(animation.startTime, null, + 'The start time is unresolved when paused'); +}, 'Pausing clears the start time'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + await animation.ready; + + animation.pause(); + assert_not_equals(animation.startTime, null, + 'The start time is resolved when pause-pending'); + + animation.play(); + assert_not_equals(animation.startTime, null, + 'The start time is preserved when a pause is aborted'); +}, 'Aborting a pause preserves the start time'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + const promise = animation.ready; + animation.pause(); + + const promiseResult = await promise; + + assert_equals(promiseResult, animation); + assert_equals(animation.ready, promise); + assert_false(animation.pending, 'No longer pause-pending'); +}, 'A pending ready promise should be resolved and not replaced when the' + + ' animation is paused'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + // Let animation start roughly half-way through + animation.currentTime = CSSNumericValue.parse("50%"); + await animation.ready; + + // Go pause-pending and also set a pending playback rate + animation.pause(); + animation.updatePlaybackRate(0.5); + + await animation.ready; + // If the current time was updated using the new playback rate it will jump + // back to 25s but if we correctly used the old playback rate the current time + // will be >= 50s. + assert_greater_than_equal(animation.currentTime.value, 50); +}, 'A pause-pending animation maintains the current time when applying a' + + ' pending playback rate'); + +promise_test(async t => { + // This test does not cover a specific step in the algorithm but serves as a + // high-level sanity check that pausing does, in fact, freeze the current + // time. + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + await animation.ready; + + animation.pause(); + await animation.ready; + + const currentTimeAfterPausing = animation.currentTime; + + scroller.scrollTop = 0.2 * maxScroll; + await waitForNextFrame(); + assert_percents_equal(animation.timeline.currentTime, 20, + 'Sanity check timeline time changed'); + + assert_percents_equal(animation.currentTime, currentTimeAfterPausing, + 'Animation.currentTime is unchanged after pausing'); +}, 'The animation\'s current time remains fixed after pausing'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + + const originalReadyPromise = animation.ready; + animation.cancel(); + assert_equals(animation.startTime, null); + assert_equals(animation.currentTime, null); + + const readyPromise = animation.ready; + assert_not_equals(originalReadyPromise, readyPromise, + 'Canceling an animation should create a new ready promise'); + + animation.pause(); + assert_equals(animation.playState, 'paused', + 'Pausing a canceled animation should update the play state'); + assert_true(animation.pending, 'animation should be pause-pending'); + await animation.ready; + assert_false(animation.pending, + 'animation should no longer be pause-pending'); + assert_equals(animation.startTime, null, 'start time should be unresolved'); + assert_percents_equal(animation.currentTime, 0, + 'current time should be set to zero'); +}, 'Pausing a canceled animation sets the current time'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + // Make the scroll timeline inactive. + scroller.style.overflow = 'visible'; + scroller.scrollTop; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + assert_equals(animation.timeline.currentTime, null, + 'Sanity check the timeline is inactive.'); + // Pause the animation when the timeline is inactive. + animation.pause(); + assert_equals(animation.currentTime, null, + 'The current time is null when the timeline is inactive.'); + assert_percents_equal(animation.startTime, 0, + 'The start time is zero in Pending state.'); + await waitForNextFrame(); + assert_true(animation.pending, + 'Animation has pause pending task while the timeline is inactive.'); + assert_equals(animation.playState, 'paused', + `State is 'paused' in Pending state.`); +}, `Pause pending task doesn't run when the timeline is inactive.`); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.2 * maxScroll; + // Make the scroll timeline inactive. + scroller.style.overflow = 'visible'; + scroller.scrollTop; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + assert_equals(animation.timeline.currentTime, null, + 'Sanity check the timeline is inactive.'); + // Play the animation when the timeline is inactive. + animation.pause(); + + // Make the scroll timeline active. + scroller.style.overflow = 'auto'; + await animation.ready; + // Ready promise is resolved as a result of the timeline becoming active. + assert_percents_equal(animation.currentTime, 20, + 'Animation current time is resolved when the animation is ready.'); + assert_equals(animation.startTime, null, + 'Animation start time is unresolved when the animation is ready.'); +}, 'Animation start and current times are correct if scroll timeline is ' + + 'activated after animation.pause call.'); + +</script> +</body> diff --git a/testing/web-platform/tests/scroll-animations/scroll-timelines/play-animation.html b/testing/web-platform/tests/scroll-animations/scroll-timelines/play-animation.html new file mode 100644 index 0000000000..d408547f55 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/scroll-timelines/play-animation.html @@ -0,0 +1,233 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Playing an animation</title> +<link rel="help" + href="https://drafts.csswg.org/web-animations/#playing-an-animation-section"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="testcommon.js"></script> +<style> +.scroller { + overflow: auto; + height: 100px; + width: 100px; + will-change: transform; +} + +.contents { + height: 1000px; + width: 100%; +} +</style> +<body> +<script> +'use strict'; + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + animation.currentTime = CSSNumericValue.parse("10%"); + assert_percents_equal(animation.currentTime, 10); + animation.play(); + assert_percents_equal(animation.currentTime, 10); +}, 'Playing a running animation leaves the current time unchanged'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + animation.finish(); + assert_percents_equal(animation.currentTime, 100); + animation.play(); + assert_percents_equal(animation.currentTime, 0); +}, 'Playing a finished animation seeks back to the start'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + animation.playbackRate = -1; + animation.currentTime = CSSNumericValue.parse("0%"); + assert_percents_equal(animation.currentTime, 0); + animation.play(); + assert_percents_equal(animation.currentTime, 100); +}, 'Playing a finished and reversed animation seeks to end'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + animation.finish(); + + // Initiate a pause then abort it + animation.pause(); + animation.play(); + + // Wait to return to running state + await animation.ready; + + assert_percents_equal(animation.currentTime, 0, + 'After aborting a pause when finished, the current time should jump ' + + 'back to the start of the animation'); +}, 'Playing a pause-pending but previously finished animation seeks back to' + + ' to the start'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + animation.finish(); + await animation.ready; + + animation.play(); + assert_percents_equal(animation.startTime, 0, 'start time is zero'); +}, 'Playing a finished animation clears the start time'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + animation.cancel(); + const promise = animation.ready; + animation.play(); + assert_not_equals(animation.ready, promise); +}, 'The ready promise should be replaced if the animation is not already' + + ' pending'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + const promise = animation.ready; + const promiseResult = await promise; + assert_equals(promiseResult, animation); + assert_equals(animation.ready, promise); +}, 'A pending ready promise should be resolved and not replaced when the' + + ' animation enters the running state'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + animation.currentTime = CSSNumericValue.parse("50%"); + await animation.ready; + + animation.pause(); + await animation.ready; + + const holdTime = animation.currentTime; + + animation.play(); + await animation.ready; + + assert_percents_equal( + animation.startTime, + animation.timeline.currentTime.value - holdTime.value); +}, 'Resuming an animation from paused calculates start time from hold time'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + await animation.ready; + + // Go to pause-pending state + animation.pause(); + assert_true(animation.pending, 'Animation is pending'); + const pauseReadyPromise = animation.ready; + + // Now play again immediately (abort the pause) + animation.play(); + assert_true(animation.pending, 'Animation is still pending'); + assert_equals(animation.ready, pauseReadyPromise, + 'The pause Promise is re-used when playing while waiting to pause'); + + // Sanity check: Animation proceeds to running state + await animation.ready; + assert_true(!animation.pending && animation.playState === 'running', + 'Animation is running after aborting a pause'); +}, 'If a pause operation is interrupted, the ready promise is reused'); + +promise_test(async t => { + // Seek animation beyond target end + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + animation.currentTime = CSSNumericValue.parse("-100%"); + await animation.ready; + + // Set pending playback rate to the opposite direction + animation.updatePlaybackRate(-1); + assert_true(animation.pending); + assert_equals(animation.playbackRate, 1); + + // When we play, we should seek to the target end, NOT to zero (which + // is where we would seek to if we used the playbackRate of 1. + animation.play(); + assert_percents_equal(animation.currentTime, 100); +}, 'A pending playback rate is used when determining auto-rewind behavior'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + animation.cancel(); + assert_equals(animation.startTime, null, + 'Start time should be unresolved'); + + animation.play(); + assert_true(animation.pending, 'Animation should be play-pending'); + + await animation.ready; + + assert_false(animation.pending, 'animation should no longer be pending'); + assert_percents_equal(animation.startTime, 0, + 'The start time of the playing animation should be zero'); +}, 'Playing a canceled animation sets the start time'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + animation.playbackRate = -1; + animation.cancel(); + assert_equals(animation.startTime, null, 'Start time should be unresolved'); + + animation.play(); + assert_true(animation.pending, 'Animation should be play-pending'); + + await animation.ready; + + assert_false(animation.pending, 'Animation should no longer be pending'); + assert_percents_equal(animation.startTime, 100, + 'The start time of the playing animation should be set'); +}, 'Playing a canceled animation backwards sets the start time'); + +</script> +</body> diff --git a/testing/web-platform/tests/scroll-animations/scroll-timelines/progress-based-effect-delay-ref.html b/testing/web-platform/tests/scroll-animations/scroll-timelines/progress-based-effect-delay-ref.html new file mode 100644 index 0000000000..59366a88dd --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/scroll-timelines/progress-based-effect-delay-ref.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<title>Reference for Web Animation with scroll timeline and effect delay tests</title> +<style> + #box { + width: 100px; + height: 100px; + background-color: green; + transform: translate(0, 100px); + opacity: 0.5; + will-change: transform; /* force compositing */ + } + + #covered { + width: 100px; + height: 100px; + background-color: red; + } + + #scroller { + overflow: auto; + height: 100px; + width: 100px; + will-change: transform; /* force compositing */ + } + + #contents { + height: 1000px; + width: 100%; + } +</style> + +<div id="box"></div> +<div id="covered"></div> +<div id="scroller"> + <div id="contents"></div> +</div> + +<script> + window.addEventListener('load', function() { + // Move the scroller to halfway. + const scroller = document.getElementById("scroller"); + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.75 * maxScroll; + }); +</script> diff --git a/testing/web-platform/tests/scroll-animations/scroll-timelines/progress-based-effect-delay.tentative.html b/testing/web-platform/tests/scroll-animations/scroll-timelines/progress-based-effect-delay.tentative.html new file mode 100644 index 0000000000..525d8448ff --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/scroll-timelines/progress-based-effect-delay.tentative.html @@ -0,0 +1,69 @@ +<html class="reftest-wait"> +<title>Animation effect delays should be accounted for when using a progress based timeline</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations/"> +<meta name="assert" content="Effect delay should be accounted for by progress based animations"> +<link rel="match" href="progress-based-effect-delay-ref.html"> + +<script src="/web-animations/testcommon.js"></script> +<script src="/common/reftest-wait.js"></script> + +<style> + #box { + width: 100px; + height: 100px; + background-color: green; + } + + #covered { + width: 100px; + height: 100px; + background-color: red; + } + + #scroller { + overflow: auto; + height: 100px; + width: 100px; + will-change: transform; /* force compositing */ + } + + #contents { + height: 1000px; + width: 100%; + } +</style> + +<div id="box"></div> +<div id="covered"></div> +<div id="scroller"> + <div id="contents"><p>Scrolling Contents</p></div> +</div> +<script> + const box = document.getElementById('box'); + const effect = new KeyframeEffect(box, + [ + { transform: 'translateY(0)', opacity: 1}, + { transform: 'translateY(200px)', opacity: 0} + ], { + delay: 1000, + duration: 1000 + } + ); + + const scroller = document.getElementById('scroller'); + const timeline = new ScrollTimeline( + { source: scroller, orientation: 'block' }); + const animation = new Animation(effect, timeline); + + animation.play(); + + animation.ready.then(() => { + // Move the scroller to the halfway point. + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.75 * maxScroll; + + waitForAnimationFrames(2).then(_ => { + takeScreenshot(); + }); + }); +</script> diff --git a/testing/web-platform/tests/scroll-animations/scroll-timelines/reverse-animation.html b/testing/web-platform/tests/scroll-animations/scroll-timelines/reverse-animation.html new file mode 100644 index 0000000000..cf6173b60a --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/scroll-timelines/reverse-animation.html @@ -0,0 +1,240 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Reversing an animation</title> +<link rel="help" + href="https://drafts.csswg.org/web-animations/#reversing-an-animation-section"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="testcommon.js"></script> +<style> +.scroller { + overflow: auto; + height: 100px; + width: 100px; + will-change: transform; +} + +.contents { + height: 1000px; + width: 100%; +} +</style> +<body> +<script> +'use strict'; + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + await animation.ready; + + animation.currentTime = CSSNumericValue.parse("50%"); + const previousPlaybackRate = animation.playbackRate; + animation.reverse(); + assert_equals(animation.playbackRate, previousPlaybackRate, + 'Playback rate should not have changed'); + await animation.ready; + + assert_equals(animation.playbackRate, -previousPlaybackRate, + 'Playback rate should be inverted'); +}, 'Reversing an animation inverts the playback rate'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + animation.currentTime = CSSNumericValue.parse("50%"); + animation.reverse(); + + assert_percents_equal(animation.currentTime, 50, + 'The current time should not change it is in the ' + + 'middle of the animation duration'); +}, 'Reversing an animation maintains the same current time'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + assert_true(animation.pending, + 'The animation is pending before we call reverse'); + + animation.reverse(); + + assert_true(animation.pending, + 'The animation is still pending after calling reverse'); +}, 'Reversing an animation does not cause it to leave the pending state'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + let readyResolved = false; + animation.ready.then(() => { readyResolved = true; }); + + animation.reverse(); + + await Promise.resolve(); + assert_false(readyResolved, + 'ready promise should not have been resolved yet'); +}, 'Reversing an animation does not cause it to resolve the ready promise'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + animation.currentTime = CSSNumericValue.parse("200%"); + animation.reverse(); + + assert_percents_equal(animation.currentTime, 100, + 'reverse() should start playing from the animation ' + + 'effect end if the playbackRate > 0 and the ' + + 'currentTime > effect end'); +}, 'Reversing an animation when playbackRate > 0 and currentTime > effect ' + + 'end should make it play from the end'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + + animation.currentTime = CSSNumericValue.parse("-200%"); + animation.reverse(); + + assert_percents_equal(animation.currentTime, 100, + 'reverse() should start playing from the animation ' + + 'effect end if the playbackRate > 0 and the ' + + 'currentTime < 0'); +}, 'Reversing an animation when playbackRate > 0 and currentTime < 0 should ' + + 'make it play from the end'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + animation.playbackRate = -1; + animation.currentTime = CSSNumericValue.parse("-200%"); + animation.reverse(); + + assert_percents_equal(animation.currentTime, 0, + 'reverse() should start playing from the start of ' + + 'animation time if the playbackRate < 0 and the ' + + 'currentTime < 0'); +}, 'Reversing an animation when playbackRate < 0 and currentTime < 0 should ' + + 'make it play from the start'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + animation.playbackRate = -1; + animation.currentTime = CSSNumericValue.parse("200%"); + animation.reverse(); + + assert_percents_equal(animation.currentTime, 0, + 'reverse() should start playing from the start of ' + + 'animation time if the playbackRate < 0 and the ' + + 'currentTime > effect end'); +}, 'Reversing an animation when playbackRate < 0 and currentTime > effect ' + + 'end should make it play from the start'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + animation.playbackRate = 0; + animation.currentTime = CSSNumericValue.parse("50%"); + animation.reverse(); + + await animation.ready; + assert_equals(animation.playbackRate, 0, + 'reverse() should preserve playbackRate if the playbackRate == 0'); + assert_percents_equal(animation.currentTime, 50, + 'reverse() should not affect the currentTime if the playbackRate == 0'); +}, 'Reversing when when playbackRate == 0 should preserve the current time ' + + 'and playback rate'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + assert_equals(animation.currentTime, null); + + animation.reverse(); + await animation.ready; + + assert_percents_equal(animation.startTime, 100, + 'animation.startTime should be at its effect end'); + assert_percents_equal(animation.currentTime, 100, + 'animation.currentTime should be at its effect end'); +}, 'Reversing an idle animation from starts playing the animation'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + // Make the scroll timeline inactive. + scroller.style.overflow = 'visible'; + scroller.scrollTop; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + + assert_throws_dom('InvalidStateError', () => { animation.reverse(); }); +}, 'Reversing an animation without an active timeline throws an ' + + 'InvalidStateError'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + animation.currentTime = CSSNumericValue.parse("50%"); + animation.pause(); + + await animation.ready; + + animation.reverse(); + await animation.ready; + + assert_equals(animation.playState, 'running', + 'Animation.playState should be "running" after reverse()'); +}, 'Reversing an animation plays a pausing animation'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + await animation.ready; + + animation.updatePlaybackRate(2); + animation.reverse(); + + await animation.ready; + assert_equals(animation.playbackRate, -2); +}, 'Reversing should use the negative pending playback rate'); + +</script> +</body> diff --git a/testing/web-platform/tests/scroll-animations/scroll-timelines/scroll-animation-effect-fill-modes.tentative.html b/testing/web-platform/tests/scroll-animations/scroll-timelines/scroll-animation-effect-fill-modes.tentative.html new file mode 100644 index 0000000000..b9cc154676 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/scroll-timelines/scroll-animation-effect-fill-modes.tentative.html @@ -0,0 +1,137 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Verify applied effect output for all fill modes in all timeline states: before start, at start, in range, at end, after end while using various effect delay values</title> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="testcommon.js"></script> +<style> + .scroller { + overflow: hidden; + height: 200px; + width: 200px; + } + .contents { + /* Make scroll range 1000 to simplify the math and avoid rounding errors */ + height: 1200px; + width: 100%; + } +</style> +<div id="log"></div> +<script> + 'use strict'; + + test(t => { + const effect = new KeyframeEffect(createDiv(t), { opacity: [0.3, 0.7] }); + const animation = new Animation(effect, createScrollTimeline(t)); + + assert_equals(effect.getTiming().fill, "auto"); + assert_equals(effect.getComputedTiming().fill, "none"); + }, "Scroll based animation effect fill mode should return 'auto' for" + + " getTiming() and should return 'none' for getComputedTiming().") + + /* All interesting transitions: + before start delay + at start delay + within active phase + at effect end + after effect end + + test_case data structure: + fill_mode: { + scroll_percentage: ["state description", expected applied effect value] + } + */ + const test_cases = { + "none": { + 0.10: ["before start delay", 1], + 0.25: ["at start delay", 0.3], + 0.50: ["at midpoint", 0.5], + 0.75: ["at effect end", 1], + 0.90: ["after effect end", 1] + }, + "backwards": { + 0.10: ["before start delay", 0.3], + 0.25: ["at start delay", 0.3], + 0.50: ["at midpoint", 0.5], + 0.75: ["at effect end", 1], + 0.90: ["after effect end", 1] + }, + "forwards": { + 0.10: ["before timeline start", 1], + 0.25: ["at timeline start", 0.3], + 0.50: ["in timeline range", 0.5], + 0.75: ["at timeline end", 0.7], + 0.90: ["after timeline end", 0.7] + }, + "both": { + 0.10: ["before timeline start", 0.3], + 0.25: ["at timeline start", 0.3], + 0.50: ["in timeline range", 0.5], + 0.75: ["at timeline end", 0.7], + 0.90: ["after timeline end", 0.7] + }, + "auto": { + 0.10: ["before timeline start", 1], + 0.25: ["at timeline start", 0.3], + 0.50: ["in timeline range", 0.5], + 0.75: ["at timeline end", 1], + 0.90: ["after timeline end", 1] + } + } + + for (const fill_mode in test_cases) { + const scroll_percents = test_cases[fill_mode] + + for (const scroll_percentage in scroll_percents) { + const expectation = scroll_percents[scroll_percentage]; + + const [test_name, expected_value] = expectation; + + const description = + `Applied effect value ${test_name} with fill: ${fill_mode}` + promise_test( + create_scroll_timeline_fill_test( + fill_mode, scroll_percentage, expected_value), + description); + } + } + + function create_scroll_timeline_fill_test( + fill_mode, scroll_percentage, expected){ + return async t => { + const target = createDiv(t); + + const timeline = createScrollTimeline(t); + const effect = + new KeyframeEffect(target, + { opacity: [0.3, 0.7] }, + { + fill: fill_mode, + /* Animation times normalized to fill scroll + range */ + duration: 2000, + delay: 1000, + endDelay: 1000 + }); + const animation = new Animation(effect, timeline); + const scroller = timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + + animation.play(); + + await animation.ready; + + scroller.scrollTop = scroll_percentage * maxScroll; + + // Wait for new animation frame which allows the timeline to compute + // new current time. + await waitForNextFrame(); + + assert_equals(parseFloat(window.getComputedStyle(target).opacity), + expected, + "animation effect applied property value"); + } + } +</script> diff --git a/testing/web-platform/tests/scroll-animations/scroll-timelines/scroll-animation-effect-phases.tentative.html b/testing/web-platform/tests/scroll-animations/scroll-timelines/scroll-animation-effect-phases.tentative.html new file mode 100644 index 0000000000..5ded56bbf7 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/scroll-timelines/scroll-animation-effect-phases.tentative.html @@ -0,0 +1,480 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Verify timeline time, animation time, effect time, and effect progress for all timeline states: before start, at start, in range, at end, after end while using various effect delay values</title> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="testcommon.js"></script> +<style> + .scroller { + overflow: hidden; + height: 200px; + width: 200px; + } + .contents { + /* Make scroll range 1000 to simplify the math and avoid rounding errors */ + height: 1200px; + width: 100%; + } +</style> +<div id="log"></div> +<script> + 'use strict'; + // Note: effects are scaled to fill the timeline. + + // Each entry is [[test input], [test expectations]] + // test input = ["description", delay, end_delay, scroll percent] + // test expectations = [timeline time, animation current time, + // effect local time, effect progress, effect phase, + // opacity] + + /* All interesting transitions: + at timeline start + before effect delay + at effect start + in active range + at effect end + after effect end + at timeline end + */ + const test_cases = [ + // Case 1: No delays. + // Boundary at end of active phase is inclusive. + [ + ["at start", 0, 0, 0], + [0, 0, 0, 0, "active", 0.3] + ], + [ + ["in active range", 0, 0, 0.50], + [50, 50, 50, 0.5, "active", 0.5] + ], + [ + ["at effect end time", 0, 0, 1.0], + [100, 100, 100, 1.0, "active", 0.7] + ], + + // Case 2: Positive start delay and no end delay. + // Boundary at end of active phase is inclusive. + [ + ["at timeline start", 500, 0, 0], + [0, 0, 0, null, "before", 1] + ], + [ + ["before start delay", 500, 0, 0.25], + [25, 25, 25, null, "before", 1] + ], + [ + ["at start delay", 500, 0, 0.5], + [50, 50, 50, 0, "active", 0.3] + ], + [ + ["in active range", 500, 0, 0.75], + [75, 75, 75, 0.5, "active", 0.5] + ], + [ + ["at effect end time", 500, 0, 1.0], + [100, 100, 100, 1.0, "active", 0.7] + ], + + // case 3: No start delay, Positive end delay. + // Boundary at end of active phase is exclusive. + [ + ["at timeline start", 0, 500, 0], + [0, 0, 0, 0, "active", 0.3] + ], + [ + ["in active range", 0, 500, 0.25], + [25, 25, 25, 0.5, "active", 0.5] + ], + [ + ["at effect end time", 0, 500, 0.5], + [50, 50, 50, null, "after", 1.0] + ], + [ + ["after effect end time", 0, 500, 0.75], + [75, 75, 75, null, "after", 1.0] + ], + [ + ["at timeline boundary", 0, 500, 1.0], + [100, 100, 100, null, "after", 1.0] + ], + + // case 4: Positive start and end delays. + // Boundary at end of active phase is exclusive. + [ + ["at timeline start", 250, 250, 0], + [0, 0, 0, null, "before", 1] + ], + [ + ["before start delay", 250, 250, 0.1], + [10, 10, 10, null, "before", 1] + ], + [ + ["at start delay", 250, 250, 0.25], + [25, 25, 25, 0, "active", 0.3] + ], + [ + ["in active range", 250, 250, 0.5], + [50, 50, 50, 0.5, "active", 0.5] + ], + [ + ["at effect end time", 250, 250, 0.75], + [75, 75, 75, null, "after", 1.0] + ], + [ + ["after effect end time", 250, 250, 0.9], + [90, 90, 90, null, "after", 1.0] + ], + [ + ["at timeline boundary", 250, 250, 1.0], + [100, 100, 100, null, "after", 1.0] + ], + + // Case 5: Negative start and end delays. + // Effect boundaries are not reachable. + [ + ["at timeline start", -125, -125, 0], + [0, 0, 0, 0.25, "active", 0.4] + ], + [ + ["in active range", -125, -125, 0.5], + [50, 50, 50, 0.5, "active", 0.5] + ], + [ + ["at timeline end", -125, -125, 1.0], + [100, 100, 100, 0.75, "active", 0.6] + ] + ]; + + for (const test_case of test_cases) { + const [inputs, expected] = test_case; + const [test_name, delay, end_delay, scroll_percentage] = inputs; + + const description = `Current times and effect phase ${test_name} when` + + ` delay = ${delay} and endDelay = ${end_delay} |`; + + promise_test( + create_scroll_timeline_delay_test( + delay, end_delay, scroll_percentage, expected), + description); + } + + function create_scroll_timeline_delay_test( + delay, end_delay, scroll_percentage, expected){ + return async t => { + const target = createDiv(t); + const timeline = createScrollTimeline(t); + const effect = new KeyframeEffect( + target, + { + opacity: [0.3, 0.7] + }, + { + duration: 500, + delay: delay, + endDelay: end_delay + } + ); + const animation = new Animation(effect, timeline); + t.add_cleanup(() => { + animation.cancel(); + }); + const scroller = timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + + animation.play(); + + await animation.ready; + + scroller.scrollTop = scroll_percentage * maxScroll; + + // Wait for new animation frame which allows the timeline to compute + // new current time. + await waitForNextFrame(); + + const [expected_timeline_current_time, + expected_animation_current_time, + expected_effect_local_time, + expected_effect_progress, + expected_effect_phase, + expected_opacity] = expected; + + assert_percents_equal( + animation.timeline.currentTime, + expected_timeline_current_time, + "timeline current time"); + assert_percents_equal( + animation.currentTime, + expected_animation_current_time, + "animation current time"); + assert_percents_equal( + animation.effect.getComputedTiming().localTime, + expected_effect_local_time, + "animation effect local time"); + assert_approx_equals_or_null( + animation.effect.getComputedTiming().progress, + expected_effect_progress, + 0.001, + "animation effect progress"); + assert_phase_at_time( + animation, expected_effect_phase, animation.currentTime); + assert_approx_equals( + parseFloat(getComputedStyle(target).opacity), expected_opacity, + 0.001, + 'target opacity'); + } + } + + function createKeyframeEffectOpacity(test){ + return new KeyframeEffect( + createDiv(test), + { + opacity: [0.3, 0.7] + }, + { + duration: 1000 + } + ); + } + + function verifyEffectBeforePhase(animation) { + // If currentTime is null, we are either idle, or running with an + // inactive timeline. Either way, the animation is not in effect and cannot + // be in the before phase. + assert_true(animation.currentTime != null, + 'Animation is not in effect'); + + const fillMode = animation.effect.getTiming().fill; + animation.effect.updateTiming({ fill: 'none' }); + + // progress == null AND opacity == 1 implies we are in the effect before + // or after phase. + assert_equals(animation.effect.getComputedTiming().progress, null); + assert_equals( + window.getComputedStyle(animation.effect.target) + .getPropertyValue("opacity"), + "1"); + + // If the progress is no longer null after adding fill: backwards, then we + // are in the before phase. + animation.effect.updateTiming({ fill: 'backwards' }); + assert_true(animation.effect.getComputedTiming().progress != null); + assert_equals( + window.getComputedStyle(animation.effect.target) + .getPropertyValue("opacity"), + "0.3"); + + // Reset fill mode to avoid side-effects. + animation.effect.updateTiming({ fill: fillMode }); + } + + function createScrollLinkedOpacityAnimationWithDelays(t) { + const animation = new Animation( + createKeyframeEffectOpacity(t), + createScrollTimeline(t) + ); + t.add_cleanup(() => { + animation.cancel(); + }); + animation.effect.updateTiming({ + duration: 1000, + delay: 500, + endDelay: 500 + }); + return animation; + } + + promise_test(async t => { + const animation = createScrollLinkedOpacityAnimationWithDelays(t); + const scroller = animation.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + + animation.play(); + await animation.ready; + verifyEffectBeforePhase(animation); + + animation.pause(); + await waitForNextFrame(); + verifyEffectBeforePhase(animation); + + animation.play(); + await waitForNextFrame(); + + verifyEffectBeforePhase(animation); + }, 'Verify that (play -> pause -> play) doesn\'t change phase/progress.'); + + promise_test(async t => { + const animation = createScrollLinkedOpacityAnimationWithDelays(t); + const scroller = animation.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + + animation.play(); + await animation.ready; + verifyEffectBeforePhase(animation); + + animation.pause(); + await waitForNextFrame(); + verifyEffectBeforePhase(animation); + + // Scrolling should not cause the animation effect to change. + scroller.scrollTop = 0.5 * maxScroll; + await waitForNextFrame(); + + // Check timeline phase + assert_percents_equal(animation.timeline.currentTime, 50); + assert_percents_equal(animation.currentTime, 0); + assert_percents_equal(animation.effect.getComputedTiming().localTime, 0, + "effect local time"); + + // Make sure the effect is still in the before phase even though the + // timeline is not. + verifyEffectBeforePhase(animation); + }, 'Pause in before phase, scroll timeline into active phase, animation ' + + 'should remain in the before phase'); + + promise_test(async t => { + const animation = createScrollLinkedOpacityAnimationWithDelays(t); + const scroller = animation.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + + animation.play(); + await animation.ready; + verifyEffectBeforePhase(animation); + + animation.pause(); + await waitForNextFrame(); + verifyEffectBeforePhase(animation); + + // Setting the current time should force the animation into effect. + const expected_time = 50; + animation.currentTime = CSS.percent(expected_time); + await waitForNextFrame(); + assert_percents_equal(animation.timeline.currentTime, 0); + assert_percents_equal(animation.currentTime, expected_time, + 'Current time matches set value'); + assert_percents_equal( + animation.effect.getComputedTiming().localTime, + expected_time, "Effect local time after setting animation.currentTime"); + assert_equals(animation.effect.getComputedTiming().progress, 0.5, + "Progress after setting animation.currentTime"); + assert_equals( + window.getComputedStyle(animation.effect.target) + .getPropertyValue("opacity"), + "0.5", "Opacity after setting animation.currentTime"); + + // Scrolling should not cause the animation effect to change since + // paused. + scroller.scrollTop = 0.75 * maxScroll; // scroll so that timeline is 75% + await waitForNextFrame(); + assert_percents_equal(animation.timeline.currentTime, 75); + + // animation and effect timings are unchanged. + assert_percents_equal(animation.currentTime, expected_time, + "Current time after scrolling while paused"); + assert_percents_equal( + animation.effect.getComputedTiming().localTime, + expected_time, + "Effect local time after scrolling while paused"); + assert_equals(animation.effect.getComputedTiming().progress, 0.5, + "Progress after scrolling while paused"); + assert_equals( + window.getComputedStyle(animation.effect.target) + .getPropertyValue("opacity"), + "0.5", "Opacity after scrolling while paused"); + }, 'Pause in before phase, set animation current time to be in active ' + + 'range, animation should become active. Scrolling should have no effect.'); + + promise_test(async t => { + const animation = createScrollLinkedOpacityAnimationWithDelays(t); + const scroller = animation.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + + animation.play(); + await animation.ready; + + // Causes the timeline to be inactive + scroller.style.overflow = "visible"; + await waitForNextFrame(); + await waitForNextFrame(); + + // Verify that he timeline is inactive + assert_equals(animation.timeline.currentTime, null, + "Timeline is inactive"); + assert_equals( + animation.currentTime, null, + "Current time for running animation with an inactive timeline"); + assert_equals(animation.effect.getComputedTiming().localTime, null, + "effect local time with inactive timeline"); + + // Setting the current time while timeline is inactive should pause the + // animation at the specified time. + animation.currentTime = CSS.percent(50); + await waitForNextFrame(); + await waitForNextFrame(); + + // Verify that animation currentTime is properly set despite the inactive + // timeline. + assert_equals(animation.timeline.currentTime, null); + assert_percents_equal(animation.currentTime, 50); + assert_percents_equal(animation.effect.getComputedTiming().localTime, 50, + "effect local time after setting animation current time"); + + // Check effect phase + // progress == 0.5 AND opacity == 0.5 shows we are in the effect active + // phase. + assert_equals(animation.effect.getComputedTiming().progress, 0.5, + "effect progress"); + assert_equals( + window.getComputedStyle(animation.effect.target) + .getPropertyValue("opacity"), + "0.5", + "effect opacity after setting animation current time"); + }, 'Make scroller inactive, then set current time to an in range time'); + + promise_test(async t => { + const animation = createScrollLinkedOpacityAnimationWithDelays(t); + const scroller = animation.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.5 * maxScroll; + // Update timeline.currentTime. + await waitForNextFrame(); + + animation.pause(); + await animation.ready; + // verify effect is applied. + const expected_progress = 0.5; + assert_equals( + animation.effect.getComputedTiming().progress, + expected_progress, + "Verify effect progress after pausing."); + + // cause the timeline to become inactive + scroller.style.overflow = 'visible'; + await waitForAnimationFrames(2); + assert_equals(animation.timeline.currentTime, null, + 'Sanity check the timeline is inactive.'); + assert_equals( + animation.effect.getComputedTiming().progress, + expected_progress, + "Verify effect progress after the timeline goes inactive."); + }, 'Animation effect is still applied after pausing and making timeline ' + + 'inactive.'); + + promise_test(async t => { + const animation = createScrollLinkedOpacityAnimationWithDelays(t); + const scroller = animation.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + + animation.play(); + await animation.ready; + + // cause the timeline to become inactive + scroller.style.overflow = 'visible'; + + scroller.scrollTop; + + animation.pause(); + }, 'Make timeline inactive, force style update then pause the animation. ' + + 'No crashing indicates test success.'); +</script> diff --git a/testing/web-platform/tests/scroll-animations/scroll-timelines/scroll-animation-inactive-timeline.html b/testing/web-platform/tests/scroll-animations/scroll-timelines/scroll-animation-inactive-timeline.html new file mode 100644 index 0000000000..236452eead --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/scroll-timelines/scroll-animation-inactive-timeline.html @@ -0,0 +1,170 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Test basic functionality of scroll linked animation.</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="testcommon.js"></script> +<style> + .scroller { + overflow: auto; + height: 100px; + width: 100px; + will-change: transform; + } + + .contents { + height: 1000px; + width: 100%; + } +</style> +<div id="log"></div> +<script> +'use strict'; + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + + // Ensure we have a valid animation frame time before continuing the test. + // This is so that we can properly determine frame advancement after the + // style change. + await waitForNextFrame(); + + // Make the scroll timeline inactive. + scroller.style.overflow = 'visible'; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + assert_equals(animation.timeline.currentTime, null, + 'Sanity check the timeline is inactive.'); + // Play the animation when the timeline is inactive. + animation.play(); + assert_equals(animation.currentTime, null, + 'The current time is null when the timeline is inactive.'); + assert_percents_equal(animation.startTime, 0, + 'The start time is zero in Pending state.'); + await waitForNextFrame(); + assert_true(animation.pending, + 'Animation has play pending task while timeline is inactive.'); + assert_equals(animation.playState, 'running', + `State is 'running' in Pending state.`); +}, `Play pending task doesn't run when the timeline is inactive.`); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + + await waitForNextFrame(); + + // Make the scroll timeline inactive. + scroller.style.overflow = 'visible'; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + assert_equals(animation.timeline.currentTime, null, + 'Sanity check the timeline is inactive.'); + // Play the animation when the timeline is inactive. + animation.play(); + + // Make the scroll timeline active. + scroller.style.overflow = 'auto'; + await animation.ready; + // Ready promise is resolved as a result of the timeline becoming active. + assert_percents_equal(animation.currentTime, 0, + 'Animation current time is resolved when the animation is ready.'); + assert_percents_equal(animation.startTime, 0, + 'Animation start time is resolved when the animation is ready.'); +}, 'Animation start and current times are correct if scroll timeline is ' + + 'activated after animation.play call.'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + const target = animation.effect.target; + + await waitForNextFrame(); + + // Make the scroll timeline inactive. + scroller.style.overflow = 'visible'; + scroller.scrollTop; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + assert_equals(animation.timeline.currentTime, null, + 'Sanity check the timeline is inactive.'); + // Set start time when the timeline is inactive. + animation.startTime = CSSNumericValue.parse("0%"); + assert_equals(animation.currentTime, null, + 'Sanity check current time is unresolved when the timeline ' + + 'is inactive.'); + + // Make the scroll timeline active. + scroller.style.overflow = 'auto'; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + + assert_percents_equal(animation.currentTime, 0, + 'Animation current time is resolved when the timeline is active.'); + assert_percents_equal(animation.startTime, 0, + 'Animation start time is resolved.'); + assert_percents_equal(animation.effect.getComputedTiming().localTime, 0, + 'Effect local time is resolved when the timeline is active.'); + assert_equals(Number(getComputedStyle(target).opacity), 0, + 'Animation has an effect when the timeline is active.'); +}, 'Animation start and current times are correct if scroll timeline is ' + + 'activated after setting start time.'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + const target = animation.effect.target; + + await waitForNextFrame(); + + // Advance the scroller. + scroller.scrollTop = 0.2 * maxScroll; + + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + // Play the animation when the timeline is active. + animation.play(); + await animation.ready; + + // Make the scroll timeline inactive. + scroller.style.overflow = 'visible'; + scroller.scrollTop; + await waitForNextFrame(); + assert_equals(animation.timeline.currentTime, null, + 'Sanity check the timeline is inactive.'); + assert_equals(animation.playState, 'running', + `State is 'running' when the timeline is inactive.`); + assert_equals(animation.currentTime, null, + 'Current time is unresolved when the timeline is inactive.'); + assert_percents_equal(animation.startTime, 0, + 'Start time is zero when the timeline is inactive.'); + assert_equals(animation.effect.getComputedTiming().localTime, null, + 'Effect local time is null when the timeline is inactive.'); + assert_equals(Number(getComputedStyle(target).opacity), 1, + 'Animation does not have an effect when the timeline is inactive.'); + + // Make the scroll timeline active. + scroller.style.overflow = 'auto'; + await waitForNextFrame(); + + assert_equals(animation.playState, 'running', + `State is 'running' when the timeline is active.`); + assert_percents_equal(animation.currentTime, 20, + 'Current time is resolved when the timeline is active.'); + assert_percents_equal(animation.startTime, 0, + 'Start time is zero when the timeline is active.'); + assert_percents_equal(animation.effect.getComputedTiming().localTime, 20, + 'Effect local time is resolved when the timeline is active.'); + assert_equals(Number(getComputedStyle(target).opacity), 0.2, + 'Animation has an effect when the timeline is active.'); +}, 'Animation current time is correct when the timeline becomes newly ' + + 'inactive and then active again.'); +</script> diff --git a/testing/web-platform/tests/scroll-animations/scroll-timelines/scroll-animation.html b/testing/web-platform/tests/scroll-animations/scroll-timelines/scroll-animation.html new file mode 100644 index 0000000000..bc7ab4a476 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/scroll-timelines/scroll-animation.html @@ -0,0 +1,216 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Test basic functionality of scroll linked animation.</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="testcommon.js"></script> +<style> + .scroller { + overflow: auto; + height: 100px; + width: 100px; + will-change: transform; + } + .contents { + height: 1000px; + width: 100%; + } +</style> +<div id="log"></div> +<script> + 'use strict'; + promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + + // Verify initial start and current times in Idle state. + assert_equals(animation.currentTime, null, + "The current time is null in Idle state."); + assert_equals(animation.startTime, null, + "The start time is null in Idle state."); + animation.play(); + assert_true(animation.pending, "Animation is in pending state."); + // Verify initial start and current times in Pending state. + assert_percents_equal(animation.currentTime, 0, + "The current time is zero in Pending state."); + assert_percents_equal(animation.startTime, 0, + "The start time is zero in Pending state."); + + await animation.ready; + // Verify initial start and current times in Playing state. + assert_percents_equal(animation.currentTime, 0, + "The current time is zero in Playing state."); + assert_percents_equal(animation.startTime, 0, + "The start time is zero in Playing state."); + + // Now do some scrolling and make sure that the Animation current time is + // correct. + scroller.scrollTop = 0.4 * maxScroll; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + assert_percents_equal(animation.currentTime, animation.timeline.currentTime, + "The current time corresponds to the scroll position of the scroller."); + assert_times_equal( + animation.effect.getComputedTiming().progress, + // Division by 100 is temporary, it will be removed when progress returns + // a CSSNumericValue instead of a double 0-1 + animation.timeline.currentTime.value / 100, + 'Effect progress corresponds to the scroll position of the scroller.'); +}, 'Animation start and current times are correct for each animation state.'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + + // Advance the scroller. + scroller.scrollTop = 0.2 * maxScroll; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + + // Verify initial start and current times in Idle state. + assert_equals(animation.currentTime, null, + "The current time is null in Idle state."); + assert_equals(animation.startTime, null, + "The start time is null in Idle state."); + animation.play(); + // Verify initial start and current times in Pending state. + assert_percents_equal(animation.currentTime, animation.timeline.currentTime, + "The current time is a hold time in Pending state."); + assert_percents_equal(animation.startTime, 0, + "The start time is zero in Pending state."); + + await animation.ready; + // Verify initial start and current times in Playing state. + assert_percents_equal(animation.currentTime, animation.timeline.currentTime, + "The current corresponds to the scroll position of the scroller."); + assert_percents_equal(animation.startTime, 0, + "The start time is zero in Playing state."); +}, 'Animation start and current times are correct for each animation state' + + ' when the animation starts playing with advanced scroller.'); + +promise_test(async t => { + const timeline = createScrollTimeline(t); + const animation1 = createScrollLinkedAnimation(t, timeline); + const animation2 = createScrollLinkedAnimation(t, timeline); + const scroller = timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + + // Advance the scroller. + scroller.scrollTop = 0.4 * maxScroll; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + + // Verify initial start and current times in Idle state. + assert_equals(animation1.currentTime, null, + "The current time is null in Idle state."); + assert_equals(animation1.startTime, null, + "The start time is null in Idle state."); + assert_equals(animation2.currentTime, null, + "The current time is null in Idle state."); + assert_equals(animation2.startTime, null, + "The start time is null in Idle state."); + animation1.play(); + animation2.play(); + // Verify initial start and current times in Pending state. + assert_percents_equal(animation1.currentTime, timeline.currentTime, + "The current time corresponds to the scroll position of the scroller" + + " in Pending state."); + assert_percents_equal(animation1.startTime, 0, + "The start time is zero in Pending state."); + assert_percents_equal(animation2.currentTime, timeline.currentTime, + "The current time corresponds to the scroll position of the scroller" + + " in Pending state."); + assert_percents_equal(animation2.startTime, 0, + "The start time is zero in Pending state."); + + await animation1.ready; + await animation2.ready; + // Verify initial start and current times in Playing state. + assert_percents_equal(animation1.currentTime, timeline.currentTime, + "The current corresponds to the scroll position of the scroller."); + assert_percents_equal(animation1.startTime, 0, + "The start time is zero in Playing state."); + assert_percents_equal(animation2.currentTime, timeline.currentTime, + "The current corresponds to the scroll position of the scroller."); + assert_percents_equal(animation2.startTime, 0, + "The start time is zero in Playing state."); +}, 'Animation start and current times are correct when multiple animations' + + ' are attached to the same timeline.'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + + animation.play(); + await animation.ready; + // Advance the scroller to max position. + scroller.scrollTop = maxScroll; + + await animation.finished; + + assert_equals(animation.playState, 'finished', + 'Animation state is in finished state.'); + assert_percents_equal(animation.currentTime, 100, + 'Animation current time is at 100% on reverse scrolling.'); + + // Scroll back. + scroller.scrollTop = 0.2 * maxScroll; + + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + // Verify animation state and current time on reverse scrolling. + assert_equals(animation.playState, 'running', + 'Animation state is playing on reverse scrolling.'); + assert_percents_equal(animation.currentTime, 20, + 'Animation current time is updated on reverse scrolling.'); +}, 'Finished animation plays on reverse scrolling.'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + + animation.play(); + await animation.ready; + + // Advance the scroller to max position. + scroller.scrollTop = maxScroll; + await animation.finished; + + var sent_finish_event = false; + animation.onfinish = function() { + sent_finish_event = true; + }; + + // Scroll back. + scroller.scrollTop = 0.2 * maxScroll; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + assert_false(sent_finish_event, + "No animation finished event is sent on reverse scroll."); + + scroller.scrollTop = maxScroll; + await animation.finished; + + // Wait for next frame to allow the animation to send finish events. The + // finished promise fires before events are sent. + await waitForNextFrame(); + + assert_true(sent_finish_event, + "Animation finished event is sent on reaching max scroll."); +}, 'Sending animation finished events by finished animation on reverse ' + + 'scrolling.'); +</script> diff --git a/testing/web-platform/tests/scroll-animations/scroll-timelines/scroll-timeline-in-removed-iframe-crash.html b/testing/web-platform/tests/scroll-animations/scroll-timelines/scroll-timeline-in-removed-iframe-crash.html new file mode 100644 index 0000000000..07edbd83f1 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/scroll-timelines/scroll-timeline-in-removed-iframe-crash.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <title>Starting an animation with a scroll timeline in a removed iframe + should not crash</title> +</head> +<body> + <div id="target"></div> + <iframe id="frame"></iframe> +</body> +<script type="text/javascript"> + const target = document.getElementById('target'); + const frame = document.getElementById('frame'); + const timeline = new frame.contentWindow.ScrollTimeline(); + frame.remove(); + target.animate(null, {timeline: timeline}); +</script> +</html> diff --git a/testing/web-platform/tests/scroll-animations/scroll-timelines/scroll-timeline-invalidation.html b/testing/web-platform/tests/scroll-animations/scroll-timelines/scroll-timeline-invalidation.html new file mode 100644 index 0000000000..0b9e10c984 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/scroll-timelines/scroll-timeline-invalidation.html @@ -0,0 +1,153 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>ScrollTimeline invalidation</title> +<link rel="help" href="https://wicg.github.io/scroll-animations/#current-time-algorithm"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="testcommon.js"></script> +<style> + .scroller { + overflow: auto; + height: 100px; + width: 100px; + will-change: transform; + } + .contents { + height: 1000px; + width: 100%; + } +</style> +<div id="log"></div> + +<script> +'use strict'; + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const effect_duration = 350; + animation.effect.updateTiming({ duration: effect_duration }); + const scroller = animation.timeline.source; + let maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.2 * maxScroll; + const initial_progress = (scroller.scrollTop / maxScroll) * 100; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + + animation.play(); + await animation.ready; + + // Animation current time is at 20% because scroller was scrolled to 20% + // assert_equals(animation.currentTime.value, 20); + assert_equals(animation.currentTime.value, 20); + assert_equals(scroller.scrollTop, 180); + assert_equals(maxScroll, 900); + + // Shrink scroller content size (from 1000 to 500). + // scroller.scrollTop maintains the same offset, which means shrinking the + // content has the effect of skipping the animation forward. + scroller.firstChild.style.height = "500px"; + maxScroll = scroller.scrollHeight - scroller.clientHeight; + + assert_equals(scroller.scrollTop, 180); + assert_equals(maxScroll, 400); + await waitForNextFrame(); + + const expected_progress = (scroller.scrollTop / maxScroll) * 100; + assert_true(expected_progress > initial_progress) + assert_equals(animation.currentTime.value, expected_progress); // 45% + assert_equals(animation.timeline.currentTime.value, expected_progress); // 45% + assert_equals(animation.effect.getComputedTiming().localTime.value, expected_progress); // 45% +}, 'Animation current time and effect local time are updated after scroller ' + + 'content size changes.'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + animation.effect.updateTiming({ duration: 350 }); + const scroller = animation.timeline.source; + let maxScroll = scroller.scrollHeight - scroller.clientHeight; + const scrollOffset = 0.2 * maxScroll + scroller.scrollTop = scrollOffset; + const initial_progress = (scroller.scrollTop / maxScroll) * 100; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + + animation.play(); + await animation.ready; + + // Animation current time is at 20% because scroller was scrolled to 20% + // assert_equals(animation.currentTime.value, 20); + assert_equals(animation.currentTime.value, 20); + assert_equals(scroller.scrollTop, scrollOffset); + assert_equals(maxScroll, 900); + + // Change scroller size. + scroller.style.height = "500px"; + maxScroll = scroller.scrollHeight - scroller.clientHeight; + + assert_equals(scroller.scrollTop, scrollOffset); + assert_equals(maxScroll, 500); + await waitForNextFrame(); + + const expected_progress = (scroller.scrollTop / maxScroll) * 100; + assert_true(expected_progress > initial_progress); + assert_equals(animation.currentTime.value, expected_progress); // 45% + assert_equals(animation.timeline.currentTime.value, expected_progress); // 45% + assert_equals(animation.effect.getComputedTiming().localTime.value, expected_progress); // 45% +}, 'Animation current time and effect local time are updated after scroller ' + + 'size changes.'); + +promise_test(async t => { + await waitForNextFrame(); + + const timeline = createScrollTimeline(t); + const scroller = timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + // Instantiate scroll animation that resizes its scroll timeline scroller. + const animation = new Animation( + new KeyframeEffect( + timeline.source.firstChild, + [{ height: '1000px' }, { height: '2000px' }], + { duration: 1000, } + ), timeline); + animation.play(); + await animation.ready; + await waitForNextFrame(); + scroller.scrollTop = 0.2 * maxScroll; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + + // Let s = scroll position + // p = fractional progress + // v = viewport height + // c = scroll height + // c[i] = c[i-1] * (1 + p[i-1]) + // p[i] = s / (c[i-1] - v) + + // c[0] = 1000 + // p[0] = 0.2 ==> s = 180 + // c[1] = 1000 * 1.2 = 1200 + assert_percents_equal(timeline.currentTime, 20, + 'Timeline current time is updated after animation frame.'); + assert_equals(scroller.scrollHeight, 1200); + await waitForNextFrame(); + + // Applying the animation effect alters the height of the scroll content and + // makes the scroll timeline stale. + // https://github.com/w3c/csswg-drafts/issues/8694 + + // p[1] = 180 / (1200 - 100) = 0.16363636363 + // c[2] = 1000 * 1.16363636363 = 1163.6363 => 1164 + // p[2] = 180 / (1164 - 100) = 0.1692 + // c[3] = 1169 + + assert_percents_equal(timeline.currentTime, 16.92, + 'Timeline current time is updated after two animation frames and ' + + 'reflects single layout run.'); + assert_approx_equals(scroller.scrollHeight, 1169, 1); +}, 'If scroll animation resizes its scroll timeline scroller, ' + + 'layout reruns once per frame.'); +</script> diff --git a/testing/web-platform/tests/scroll-animations/scroll-timelines/scroll-timeline-snapshotting.html b/testing/web-platform/tests/scroll-animations/scroll-timelines/scroll-timeline-snapshotting.html new file mode 100644 index 0000000000..1e43699d5b --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/scroll-timelines/scroll-timeline-snapshotting.html @@ -0,0 +1,44 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>ScrollTimeline snapshotting</title> +<link rel="help" href="https://wicg.github.io/scroll-animations/#avoiding-cycles"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="./testcommon.js"></script> + +<style> + body { + height: 800px; + width: 800px; + } +</style> +<div id="log"></div> + +<script> +'use strict'; + +promise_test(async t => { + const scroller = document.scrollingElement; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + const timeline = new ScrollTimeline({source: scroller}); + scroller.scrollTo(0, 0); + assert_equals(scroller.scrollTop, 0, "verify test pre-condition"); + + scroller.scrollBy({top: 100, behavior: 'smooth'}); + // Wait for the scroll to change. + const startScroll = scroller.scrollTop; + do { + await waitForNextFrame(); + } while(scroller.scrollTop == startScroll); + assert_percents_equal( + timeline.currentTime, + (scroller.scrollTop / maxScroll) * 100, + 'Scroll timeline current time corresponds to the scroll position.'); +}, 'ScrollTimeline current time is updated after programmatic animated ' + + 'scroll.'); + +</script> diff --git a/testing/web-platform/tests/scroll-animations/scroll-timelines/set-current-time-before-play.html b/testing/web-platform/tests/scroll-animations/scroll-timelines/set-current-time-before-play.html new file mode 100644 index 0000000000..cd3faeeee4 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/scroll-timelines/set-current-time-before-play.html @@ -0,0 +1,69 @@ +<html class="reftest-wait"> +<title>Setting current time before play should not timeout</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations/"> +<meta name="assert" content="Regression test to make sure the ready promise is correctly resolved"> +<link rel="match" href="animation-ref.html"> + +<script src="/web-animations/testcommon.js"></script> +<script src="/common/reftest-wait.js"></script> + +<style> + #box { + width: 100px; + height: 100px; + background-color: green; + } + + #covered { + width: 100px; + height: 100px; + background-color: red; + } + + #scroller { + overflow: auto; + height: 100px; + width: 100px; + will-change: transform; /* force compositing */ + } + + #contents { + height: 1000px; + width: 100%; + } +</style> + +<div id="box"></div> +<div id="covered"></div> +<div id="scroller"> + <div id="contents"></div> +</div> + +<script> + const box = document.getElementById('box'); + const effect = new KeyframeEffect(box, + [ + { transform: 'translateY(0)', opacity: 1}, + { transform: 'translateY(200px)', opacity: 0} + ], { + duration: 1000, + } + ); + + const scroller = document.getElementById('scroller'); + const timeline = new ScrollTimeline( + { source: scroller, orientation: 'block' }); + const animation = new Animation(effect, timeline); + animation.currentTime = CSS.percent(0); + animation.play(); + + animation.ready.then(() => { + // Move the scroller to the halfway point. + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.5 * maxScroll; + + waitForAnimationFrames(2).then(_ => { + takeScreenshot(); + }); + }); +</script> diff --git a/testing/web-platform/tests/scroll-animations/scroll-timelines/setting-current-time.html b/testing/web-platform/tests/scroll-animations/scroll-timelines/setting-current-time.html new file mode 100644 index 0000000000..f6c826db69 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/scroll-timelines/setting-current-time.html @@ -0,0 +1,286 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Setting the current time of an animation</title> +<link rel="help" href="https://drafts.csswg.org/web-animations-1/#setting-the-current-time-of-an-animation"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="testcommon.js"></script> +<style> +.scroller { + overflow: auto; + height: 200px; + width: 100px; + will-change: transform; +} +.contents { + height: 1000px; + width: 100%; +} +</style> +<body> +<div id="log"></div> +<script> +'use strict'; + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.25 * maxScroll; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + + await animation.ready; + + assert_throws_js(TypeError, () => { + animation.currentTime = null; + }); +}, 'Setting animation current time to null throws TypeError.'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + assert_throws_dom('NotSupportedError', () => { + animation.currentTime = CSSNumericValue.parse("300"); + }); + assert_throws_dom('NotSupportedError', () => { + animation.currentTime = CSSNumericValue.parse("300ms"); + }); + assert_throws_dom('NotSupportedError', () => { + animation.currentTime = CSSNumericValue.parse("0.3s"); + }); +}, 'Setting the current time to an absolute time value throws exception'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.25 * maxScroll; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + + animation.currentTime = CSSNumericValue.parse("33.3%"); + + assert_percents_equal(animation.currentTime, 33.3, + "Animation current time should be equal to the set value." + ); +}, 'Set animation current time to a valid value without playing.'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.25 * maxScroll; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + + await animation.ready; + animation.currentTime = CSSNumericValue.parse("33.3%"); + + assert_percents_equal(animation.currentTime, 33.3, + "Animation current time should be equal to the set value." + ); +}, 'Set animation current time to a valid value while playing.'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.25 * maxScroll; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + + await animation.ready; + animation.currentTime = CSSNumericValue.parse("200%"); + + assert_equals(animation.playState, "finished"); + assert_percents_equal(animation.currentTime, 200, + "Animation current time should be equal to the set value." + ); +}, 'Set animation current time to a value beyond effect end.'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.25 * maxScroll; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + + await animation.ready; + animation.currentTime = CSSNumericValue.parse("-10%"); + + assert_equals(animation.playState, "running"); + assert_percents_equal(animation.currentTime, -10, + "Animation current time should be equal to the set value." + ); +}, 'Set animation current time to a negative value.'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.25 * maxScroll; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + + animation.currentTime = CSSNumericValue.parse("30%"); + + assert_equals(animation.playState, "running"); + assert_true(animation.pending); + assert_percents_equal(animation.currentTime, 30); +}, "Setting current time while play pending overrides the current time"); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.25 * maxScroll; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + + await animation.ready; + animation.currentTime = CSSNumericValue.parse("33.3%"); + + assert_percents_equal(animation.currentTime, 33.3, + "Animation current time should be equal to the set value." + ); + + // Cancel the animation and play it again, check that current time has reset + // to scroll offset based current time. + animation.cancel(); + animation.play(); + await animation.ready; + + assert_percents_equal(animation.currentTime, animation.timeline.currentTime, + "Animation current time should return to a value matching its" + + " timeline current time after animation is cancelled and played again." + ); +}, 'Setting animation.currentTime then restarting the animation should' + + ' reset the current time.'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.25 * maxScroll; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + + await animation.ready; + const originalCurrentTime = animation.currentTime.value; + + // Set the current time to something other than where the scroll offset. + animation.currentTime = CSSNumericValue.parse("50%"); + + // Setting current time is internally setting the start time to + // scrollTimeline.currentTime - newAnimationCurrentTime. + // Which results in current time of (timeline.currentTime - start_time). + // This behavior puts the animation in a strange "out of sync" state between + // the scroller and the animation effect, this is currently expected + // behavior. + + const expectedStartTime = originalCurrentTime - animation.currentTime.value; + assert_percents_equal(animation.startTime, expectedStartTime, + "Animation current time should be updated when setting the current time" + + " to a time within the range of the animation."); + + scroller.scrollTop = 0.7 * maxScroll; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + + assert_percents_equal(animation.startTime, expectedStartTime, + "Animation start time should remain unchanged when the scroller changes" + + " position." + ); + assert_percents_equal(animation.currentTime, + animation.timeline.currentTime.value - animation.startTime.value, + "Animation current time should return to a value equal to" + + " (timeline.currentTime - animation.startTime) after timeline scroll" + + " source has been scrolled." + ); +}, 'Set Animation current time then scroll.'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + await animation.ready; + + // Make the timeline inactive. + scroller.style.overflow = 'visible'; + scroller.scrollTop; + await waitForNextFrame(); + + assert_equals(animation.currentTime, null, + 'Current time is unresolved when the timeline is inactive.'); + + animation.currentTime = CSSNumericValue.parse("30%"); + assert_percents_equal(animation.currentTime, 30, + 'Animation current time should be equal to the set value.'); + assert_equals(animation.playState, 'paused', + 'Animation play state is \'paused\' when current time is set and ' + + 'timeline is inactive.'); +}, 'Animation current time and play state are correct when current time is ' + + 'set while the timeline is inactive.'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + await animation.ready; + + // Make the timeline inactive. + scroller.style.overflow = 'visible'; + scroller.scrollTop; + await waitForNextFrame(); + + assert_equals(animation.timeline.currentTime, null, + 'Current time is unresolved when the timeline is inactive.'); + + animation.currentTime = CSSNumericValue.parse("30%"); + assert_percents_equal(animation.currentTime, 30, + 'Animation current time should be equal to the set value.'); + assert_equals(animation.playState, 'paused', + 'Animation play state is \'paused\' when current time is set and ' + + 'timeline is inactive.'); + + // Make the timeline active. + scroller.style.overflow = 'auto'; + scroller.scrollTop; + await waitForNextFrame(); + + assert_percents_equal(animation.timeline.currentTime, 0, + 'Current time is resolved when the timeline is active.'); + assert_percents_equal(animation.currentTime, 30, + 'Animation current time holds the set value.'); + assert_equals(animation.playState, 'paused', + 'Animation holds \'paused\' state.'); +}, 'Animation current time set while the timeline is inactive holds when the ' + + 'timeline becomes active again.'); +</script> +</body> diff --git a/testing/web-platform/tests/scroll-animations/scroll-timelines/setting-playback-rate.html b/testing/web-platform/tests/scroll-animations/scroll-timelines/setting-playback-rate.html new file mode 100644 index 0000000000..4d6fecae05 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/scroll-timelines/setting-playback-rate.html @@ -0,0 +1,323 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Setting the playback rate of an animation that is using a ScrollTimeline</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#setting-the-playback-rate-of-an-animation"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="testcommon.js"></script> +<style> +.scroller { + overflow: auto; + height: 100px; + width: 100px; + will-change: transform; +} +.contents { + height: 1000px; + width: 100%; +} +</style> +<body> +<script> + 'use strict'; + + promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + // this forces a layout which results in an active timeline + scroller.scrollTop = 0; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + + animation.playbackRate = 0.5; + animation.play(); + + assert_percents_equal(animation.currentTime, 0, + 'Zero current time is not affected by playbackRate change.'); + }, 'Zero current time is not affected by playbackRate set while the ' + + 'animation is in idle state.'); + + promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + // this forces a layout which results in an active timeline + scroller.scrollTop = 0; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + + animation.play(); + animation.playbackRate = 0.5; + + assert_percents_equal(animation.currentTime, 0, + 'Zero current time is not affected by playbackRate change.'); + }, 'Zero current time is not affected by playbackRate set while the ' + + 'animation is in play-pending state.'); + + promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.2 * maxScroll; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + + animation.playbackRate = 0.5; + animation.play(); + await animation.ready; + assert_percents_equal(animation.currentTime, 10, + 'Initial current time is scaled by playbackRate change.'); + }, 'Initial current time is scaled by playbackRate set while ' + + 'scroll-linked animation is in running state.'); + + promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + const playbackRate = 2; + + scroller.scrollTop = 0.2 * maxScroll; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + + animation.play(); + await animation.ready; + // Set playback rate while the animation is playing. + animation.playbackRate = playbackRate; + assert_percents_equal(animation.currentTime, 40, + 'The current time is scaled by the playback rate.'); + }, 'The current time is scaled by playbackRate set while the ' + + 'scroll-linked animation is in play state.'); + + promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + + // Set playback rate while the animation is in 'idle' state. + animation.playbackRate = 2; + animation.play(); + await animation.ready; + scroller.scrollTop = 0.2 * maxScroll; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + + assert_percents_equal(animation.currentTime, 40, + 'The current time should increase two times faster ' + + 'than timeline time.'); + }, 'The playback rate set before scroll-linked animation started playing ' + + 'affects the rate of progress of the current time'); + + promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + animation.play(); + + await animation.ready; + + animation.playbackRate = 2; + scroller.scrollTop = 0.25 * maxScroll; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + + assert_percents_equal( + animation.currentTime, + animation.timeline.currentTime.value * animation.playbackRate, + 'The current time should increase two times faster than timeline time'); + }, 'The playback rate affects the rate of progress of the current time' + + ' when scrolling'); + + promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.25 * maxScroll; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + + animation.playbackRate = 2; + + assert_equals(animation.playState, "running"); + assert_true(animation.pending); + assert_percents_equal(animation.currentTime, 50); + }, 'Setting the playback rate while play-pending scales the current time' + + ' from scrollTimeline.'); + + test(t => { + const animation = createScrollLinkedAnimation(t); + animation.play(); + animation.currentTime = CSSNumericValue.parse("25%"); + animation.playbackRate = 2; + + assert_equals(animation.playState, "running"); + assert_true(animation.pending); + assert_percents_equal(animation.currentTime, 50); + }, 'Setting the playback rate while play-pending scales the set current' + + ' time.'); + + promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.25 * maxScroll; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + assert_percents_equal(animation.currentTime, 25); + + await animation.ready; + animation.playbackRate = 2; + + assert_percents_equal(animation.currentTime, 50); + }, 'Setting the playback rate while playing scales the current time' + + ' from scrollTimeline.'); + + promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + + /* Wait for animation frame is here for now to avoid a renderer crash + caused by crbug.com/1042924. Once that is fixed, these can be removed */ + await waitForAnimationFrames(2); + + animation.play(); + + animation.currentTime = CSSNumericValue.parse("25%"); + await animation.ready; + animation.playbackRate = 2; + + assert_percents_equal(animation.currentTime, 50); + }, 'Setting the playback rate while playing scales the set current time.'); + + promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + animation.playbackRate = -1; + scroller.scrollTop = 0.3 * maxScroll; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + + await animation.ready; + const expectedCurrentTime = 100 - animation.timeline.currentTime.value; + assert_percents_equal(animation.currentTime, expectedCurrentTime); + }, 'Negative initial playback rate should correctly modify initial current' + + ' time.'); + + promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.5 * maxScroll; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + + await animation.ready; + const startingTimelineTime = animation.timeline.currentTime; + const startingCurrentTime = animation.currentTime; + assert_percents_equal(startingCurrentTime, 50); + assert_percents_equal(startingTimelineTime, 50); + + animation.playbackRate = -1; + + scroller.scrollTop = 0.8 * maxScroll; + await waitForNextFrame(); + // -300 = 500 - 800 + + // let timelineDiff = + // startingTimelineTime.value - animation.timeline.currentTime.value; + // // 200 = 500 + (-300) + // let expected = startingCurrentTime.value + timelineDiff; + assert_percents_equal(animation.timeline.currentTime, 80); + assert_percents_equal(animation.currentTime, 20); + + scroller.scrollTop = 0.2 * maxScroll; + await waitForNextFrame(); + // // 300 = 500 - 200 + // timelineDiff = + // startingTimelineTime.value - animation.timeline.currentTime.value; + // // 800 = 500 + 300 + // expected = startingCurrentTime.value + timelineDiff; + assert_percents_equal(animation.timeline.currentTime, 20); + assert_percents_equal(animation.currentTime, 80); + }, 'Reversing the playback rate while playing correctly impacts current' + + ' time during future scrolls'); + + promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + animation.playbackRate = 0; + scroller.scrollTop = 0.3 * maxScroll; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + + await animation.ready; + assert_percents_equal(animation.currentTime, 0); + }, 'Zero initial playback rate should correctly modify initial current' + + ' time.'); + + promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.2 * maxScroll; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + + await animation.ready; + assert_percents_equal(animation.currentTime, 20); + animation.playbackRate = 0; + scroller.scrollTop = 0.5 * maxScroll; + await waitForNextFrame(); + + // Ensure that current time does not change. + assert_percents_equal(animation.timeline.currentTime, 50); + assert_percents_equal(animation.currentTime, 0); + }, 'Setting a zero playback rate while running preserves the start time'); + + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.2 * maxScroll; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + + await animation.ready; + assert_percents_equal(animation.timeline.currentTime, 20); + assert_percents_equal(animation.currentTime, 20); + animation.startTime = animation.currentTime; + // timeline current time [0%, 100%] --> animation current time [-20%, 80%]. + assert_percents_equal(animation.currentTime, 0); + + animation.playbackRate = -1; + // timeline current time [0%, 100%] --> animation current time [80%, -20%]. + // timeline @ 20% --> animation current time @ (20% - 80%) * (-1) = 60%. + assert_percents_equal(animation.timeline.currentTime, 20); + assert_percents_equal(animation.currentTime, 60); + }, 'Reversing an animation with non-boundary aligned start time ' + + 'symmetrically adjusts the start time'); + +</script> +</body> diff --git a/testing/web-platform/tests/scroll-animations/scroll-timelines/setting-start-time.html b/testing/web-platform/tests/scroll-animations/scroll-timelines/setting-start-time.html new file mode 100644 index 0000000000..d506df65bf --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/scroll-timelines/setting-start-time.html @@ -0,0 +1,439 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Setting the start time of scroll animation</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#setting-the-start-time-of-an-animation"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="testcommon.js"></script> +<style> +.scroller { + overflow: auto; + height: 200px; + width: 100px; + will-change: transform; +} + +.contents { + height: 1000px; + width: 100%; +} +</style> +<body> +<div id="log"></div> +<script> +'use strict'; + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + assert_throws_dom('NotSupportedError', () => { + animation.startTime = CSSNumericValue.parse("300"); + }); + assert_throws_dom('NotSupportedError', () => { + animation.startTime = CSSNumericValue.parse("300ms"); + }); + assert_throws_dom('NotSupportedError', () => { + animation.startTime = CSSNumericValue.parse("0.3s"); + }); +}, 'Setting the start time to an absolute time value throws exception'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.2 * maxScroll; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + + // So long as a hold time is set, querying the current time will return + // the hold time. + + // Since the start time is unresolved at this point, setting the current time + // will set the hold time + animation.currentTime = CSSNumericValue.parse("30%"); + assert_equals(animation.startTime, null, 'The start time stays unresolved'); + assert_percents_equal(animation.currentTime, 30, + 'The current time is calculated from the hold time'); + + // If we set the start time, however, we should clear the hold time. + animation.startTime = CSSNumericValue.parse("0%"); + assert_percents_equal(animation.startTime, 0, + 'The start time is set to the requested value'); + assert_percents_equal(animation.currentTime, 20, + 'The current time is calculated from the start time, ' + + 'not the hold time'); + // Sanity check + assert_equals(animation.playState, 'running', + 'Animation reports it is running after setting a resolved ' + + 'start time'); +}, 'Setting the start time clears the hold time'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + // Make the scroll timeline inactive. + scroller.style.overflow = 'visible'; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + assert_equals(animation.timeline.currentTime, null, + 'Sanity check the timeline is inactive'); + + // So long as a hold time is set, querying the current time will return + // the hold time. + + // Since the start time is unresolved at this point, setting the current time + // will set the hold time + animation.currentTime = CSSNumericValue.parse("30%"); + assert_equals(animation.startTime, null, 'The start time stays unresolved'); + assert_percents_equal(animation.currentTime, 30, + 'The current time is calculated from the hold time'); + + // If we set the start time, however, we should clear the hold time. + animation.startTime = CSSNumericValue.parse("0%"); + assert_percents_equal(animation.startTime, 0, + 'The start time is set to the requested value'); + assert_equals(animation.currentTime, null, + 'The current time is calculated from the start time, not' + + ' the hold time'); + // Sanity check + assert_equals(animation.playState, 'running', + 'Animation reports it is running after setting a resolved ' + + 'start time'); +}, 'Setting the start time clears the hold time when the timeline is inactive'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.2 * maxScroll; + + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + + // Set up a running animation (i.e. both start time and current time + // are resolved). + animation.startTime = CSSNumericValue.parse("5%"); + assert_equals(animation.playState, 'running'); + assert_percents_equal(animation.startTime, 5, + 'The start time is set to the requested value'); + assert_percents_equal(animation.currentTime, 15, + 'Current time is resolved for a running animation'); + + // Clear start time + animation.startTime = null; + assert_equals(animation.startTime, null, + 'The start time is set to the requested value'); + assert_percents_equal(animation.currentTime, 15, + 'Hold time is set after start time is made unresolved'); + assert_equals(animation.playState, 'paused', + 'Animation reports it is paused after setting an unresolved' + + ' start time'); +}, 'Setting an unresolved start time sets the hold time'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + // Make the scroll timeline inactive. + scroller.style.overflow = 'visible'; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + assert_equals(animation.timeline.currentTime, null, + 'Sanity check the timeline is inactive'); + + // Set up a running animation (i.e. both start time and current time + // are resolved). + animation.startTime = CSSNumericValue.parse("5%"); + assert_equals(animation.playState, 'running'); + assert_percents_equal(animation.startTime, 5, + 'The start time is set to the requested value'); + assert_equals(animation.currentTime, null, + 'Current time is unresolved for a running animation when the ' + + 'timeline is inactive'); + + // Clear start time + animation.startTime = null; + assert_equals(animation.startTime, null, + 'The start time is set to the requested value'); + assert_equals(animation.currentTime, null, + 'Hold time is set to unresolved after start time is made ' + + 'unresolved'); + assert_equals(animation.playState, 'idle', + 'Animation reports it is idle after setting an unresolved' + + ' start time'); +}, 'Setting an unresolved start time sets the hold time to unresolved when ' + + 'the timeline is inactive'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + + let readyPromiseCallbackCalled = false; + animation.ready.then(() => { readyPromiseCallbackCalled = true; } ); + + // Put the animation in the play-pending state + animation.play(); + + // Sanity check + assert_true(animation.pending && animation.playState === 'running', + 'Animation is in play-pending state'); + + // Setting the start time should resolve the 'ready' promise, i.e. + // it should schedule a microtask to run the promise callbacks. + animation.startTime = CSSNumericValue.parse("10%"); + assert_percents_equal(animation.startTime, 10, + 'The start time is set to the requested value'); + assert_false(readyPromiseCallbackCalled, + 'Ready promise callback is not called synchronously'); + + // If we schedule another microtask then it should run immediately after + // the ready promise resolution microtask. + await Promise.resolve(); + assert_true(readyPromiseCallbackCalled, + 'Ready promise callback called after setting startTime'); +}, 'Setting the start time resolves a pending ready promise'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + // Make the scroll timeline inactive. + scroller.style.overflow = 'visible'; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + assert_equals(animation.timeline.currentTime, null, + 'Sanity check the timeline is inactive'); + + let readyPromiseCallbackCalled = false; + animation.ready.then(() => { readyPromiseCallbackCalled = true; } ); + + // Put the animation in the play-pending state + animation.play(); + + // Sanity check + assert_true(animation.pending && animation.playState === 'running', + 'Animation is in play-pending state'); + + // Setting the start time should resolve the 'ready' promise, i.e. + // it should schedule a microtask to run the promise callbacks. + animation.startTime = CSSNumericValue.parse("10%"); + assert_percents_equal(animation.startTime, 10, + 'The start time is set to the requested value'); + assert_false(readyPromiseCallbackCalled, + 'Ready promise callback is not called synchronously'); + + // If we schedule another microtask then it should run immediately after + // the ready promise resolution microtask. + await Promise.resolve(); + assert_true(readyPromiseCallbackCalled, + 'Ready promise callback called after setting startTime'); +}, 'Setting the start time resolves a pending ready promise when the timeline' + + 'is inactive'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + + // Put the animation in the play-pending state + animation.play(); + + // Sanity check + assert_true(animation.pending, 'Animation is pending'); + assert_equals(animation.playState, 'running', 'Animation is play-pending'); + assert_percents_equal(animation.startTime, 0, 'Start time is zero'); + + // Setting start time should cancel the pending task. + animation.startTime = null; + assert_false(animation.pending, 'Animation is no longer pending'); + assert_equals(animation.playState, 'paused', 'Animation is paused'); +}, 'Setting an unresolved start time on a play-pending animation makes it' + + ' paused'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + // Make the scroll timeline inactive. + scroller.style.overflow = 'visible'; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + assert_equals(animation.timeline.currentTime, null, + 'Sanity check the timeline is inactive'); + + // Put the animation in the play-pending state + animation.play(); + + // Sanity check + assert_true(animation.pending, 'Animation is pending'); + assert_equals(animation.playState, 'running', 'Animation is play-pending'); + assert_percents_equal(animation.startTime, 0, 'Start time is zero'); + + // Setting start time should cancel the pending task. + animation.startTime = null; + assert_false(animation.pending, 'Animation is no longer pending'); + assert_equals(animation.playState, 'idle', 'Animation is idle'); +}, 'Setting an unresolved start time on a play-pending animation makes it' + + ' idle when the timeline is inactive'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + + // Set start time such that the current time is past the end time + animation.startTime = CSSNumericValue.parse("-110%"); + assert_percents_equal(animation.startTime, -110, + 'The start time is set to the requested value'); + assert_equals(animation.playState, 'finished', + 'Seeked to finished state using the startTime'); + + // If the 'did seek' flag is true, the current time should be greater than + // the effect end. + assert_greater_than(animation.currentTime.value, + animation.effect.getComputedTiming().endTime.value, + 'Setting the start time updated the finished state with' + + ' the \'did seek\' flag set to true'); + + // Furthermore, that time should persist if we have correctly updated + // the hold time + const finishedCurrentTime = animation.currentTime; + await waitForNextFrame(); + assert_percents_equal(animation.currentTime, finishedCurrentTime, + 'Current time does not change after seeking past the ' + + 'effect end time by setting the current time'); +}, 'Setting the start time updates the finished state'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + + await animation.ready; + assert_equals(animation.playState, 'running'); + + // Setting the start time updates the finished state. The hold time is not + // constrained by the effect end time. + animation.startTime = CSSNumericValue.parse("-110%"); + assert_equals(animation.playState, 'finished'); + + assert_percents_equal(animation.currentTime, 110); +}, 'Setting the start time on a running animation updates the play state'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + await animation.ready; + + // Setting the start time updates the finished state. The hold time is not + // constrained by the normal range of the animation time. + animation.currentTime = CSSNumericValue.parse("100%"); + assert_equals(animation.playState, 'finished', 'Animation is finished'); + animation.playbackRate = -1; + assert_equals(animation.playState, 'running', 'Animation is running'); + animation.startTime = CSSNumericValue.parse("-200%"); + assert_equals(animation.playState, 'finished', 'Animation is finished'); + assert_percents_equal(animation.currentTime, -200); +}, 'Setting the start time on a reverse running animation updates the play ' + + 'state'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + let readyPromiseCallbackCalled = false; + animation.ready.then(() => { readyPromiseCallbackCalled = true; } ); + animation.pause(); + + // Sanity check + assert_true(animation.pending && animation.playState === 'paused', + 'Animation is in pause-pending state'); + + // Setting the start time should resolve the 'ready' promise although + // the resolution callbacks when be run in a separate microtask. + animation.startTime = null; + assert_false(readyPromiseCallbackCalled, + 'Ready promise callback is not called synchronously'); + + await Promise.resolve(); + assert_true(readyPromiseCallbackCalled, + 'Ready promise callback called after setting startTime'); +}, 'Setting the start time resolves a pending pause task'); + +promise_test(async t => { + const anim = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + anim.play(); + + // We should be play-pending now + assert_true(anim.pending); + assert_equals(anim.playState, 'running'); + + // Apply a pending playback rate + anim.updatePlaybackRate(2); + assert_equals(anim.playbackRate, 1); + assert_true(anim.pending); + + // Setting the start time should apply the pending playback rate + anim.startTime = CSSNumericValue.parse( + anim.timeline.currentTime.value - 2500 + "%"); + assert_equals(anim.playbackRate, 2); + assert_false(anim.pending); + + // Sanity check that the start time is preserved and current time is + // calculated using the new playback rate + assert_percents_equal(anim.startTime, + anim.timeline.currentTime.value - 2500); + assert_percents_equal(anim.currentTime, 5000); +}, 'Setting the start time of a play-pending animation applies a pending ' + + 'playback rate'); + +promise_test(async t => { + const anim = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + anim.play(); + await anim.ready; + + // We should be running now + assert_false(anim.pending); + assert_equals(anim.playState, 'running'); + + // Apply a pending playback rate + anim.updatePlaybackRate(2); + assert_equals(anim.playbackRate, 1); + assert_true(anim.pending); + + // Setting the start time should apply the pending playback rate + anim.startTime = CSSNumericValue.parse( + anim.timeline.currentTime.value - 25 + "%"); + assert_equals(anim.playbackRate, 2); + assert_false(anim.pending); + + // Sanity check that the start time is preserved and current time is + // calculated using the new playback rate + assert_percents_equal(anim.startTime, + anim.timeline.currentTime.value - 25); + assert_percents_equal(anim.currentTime, 50); +}, 'Setting the start time of a playing animation applies a pending playback ' + + 'rate'); +</script> +</body> diff --git a/testing/web-platform/tests/scroll-animations/scroll-timelines/setting-timeline.tentative.html b/testing/web-platform/tests/scroll-animations/scroll-timelines/setting-timeline.tentative.html new file mode 100644 index 0000000000..34d9af2bff --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/scroll-timelines/setting-timeline.tentative.html @@ -0,0 +1,394 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Setting the timeline of scroll animation</title> +<link rel="help" + href="https://drafts.csswg.org/web-animations-1/#setting-the-timeline"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="testcommon.js"></script> +<style> + .scroller { + overflow: auto; + height: 200px; + width: 100px; + will-change: transform; + } + .contents { + /* The height is set to align scrolling in pixels with logical time in ms */ + height: 1200px; + width: 100%; + } + @keyframes anim { + from { opacity: 0; } + to { opacity: 1; } + } + .anim { + animation: anim 1s paused linear; + } +</style> +<body> +<script> +'use strict'; + +function createAnimation(t) { + const elem = createDiv(t); + const animation = elem.animate({ opacity: [1, 0] }, 1000); + return animation; +} + +function createPausedCssAnimation(t) { + const elem = createDiv(t); + elem.classList.add('anim'); + return elem.getAnimations()[0]; +} + +function updateScrollPosition(timeline, offset) { + const scroller = timeline.source; + assert_true(!!scroller, 'source is resolved'); + scroller.scrollTop = offset; + // Wait for new animation frame which allows the timeline to compute new + // current time. + return waitForNextFrame(); +} + +function assert_timeline_current_time(animation, timeline_current_time) { + if (animation.currentTime instanceof CSSUnitValue){ + assert_percents_equal(animation.timeline.currentTime, timeline_current_time, + `Timeline's currentTime aligns with the scroll ` + + `position even when paused`); + } + else { + assert_times_equal(animation.timeline.currentTime, timeline_current_time, + `Timeline's currentTime aligns with the scroll ` + + `position even when paused`); + } +} + +function assert_scroll_synced_times(animation, timeline_current_time, + animation_current_time) { + assert_timeline_current_time(animation, timeline_current_time); + if (animation.currentTime instanceof CSSUnitValue){ + assert_percents_equal(animation.currentTime, animation_current_time, + `Animation's currentTime aligns with the scroll position`); + } + else { + assert_times_equal(animation.currentTime, animation_current_time, + `Animation's currentTime aligns with the scroll position`); + } +} + +function assert_paused_times(animation, timeline_current_time, + animation_current_time) { + assert_timeline_current_time(animation, timeline_current_time); + if (animation.currentTime instanceof CSSUnitValue){ + assert_percents_equal(animation.currentTime, animation_current_time, + `Animation's currentTime is fixed while paused`); + } + else { + assert_times_equal(animation.currentTime, animation_current_time, + `Animation's currentTime is fixed while paused`); + } +} + +promise_test(async t => { + const scrollTimeline = createScrollTimeline(t); + await updateScrollPosition(scrollTimeline, 100); + + const animation = createAnimation(t); + animation.timeline = scrollTimeline; + assert_true(animation.pending); + await animation.ready; + + assert_equals(animation.playState, 'running'); + assert_scroll_synced_times(animation, 10, 10); +}, 'Setting a scroll timeline on a play-pending animation synchronizes ' + + 'currentTime of the animation with the scroll position.'); + +promise_test(async t => { + const scrollTimeline = createScrollTimeline(t); + await updateScrollPosition(scrollTimeline, 100); + + const animation = createAnimation(t); + animation.pause(); + animation.timeline = scrollTimeline; + assert_true(animation.pending); + await animation.ready; + + assert_equals(animation.playState, 'paused'); + assert_paused_times(animation, 10, 0); + + await updateScrollPosition(animation.timeline, 200); + + assert_equals(animation.playState, 'paused'); + assert_paused_times(animation, 20, 0); + + animation.play(); + await animation.ready; + + assert_scroll_synced_times(animation, 20, 20); +}, 'Setting a scroll timeline on a pause-pending animation fixes the ' + + 'currentTime of the animation based on the scroll position once resumed'); + +promise_test(async t => { + const scrollTimeline = createScrollTimeline(t); + await updateScrollPosition(scrollTimeline, 100); + + const animation = createAnimation(t); + animation.reverse(); + animation.timeline = scrollTimeline; + await animation.ready; + + assert_equals(animation.playState, 'running'); + assert_scroll_synced_times(animation, 10, 90); +}, 'Setting a scroll timeline on a reversed play-pending animation ' + + 'synchronizes the currentTime of the animation with the scroll ' + + 'position.'); + +promise_test(async t => { + const scrollTimeline = createScrollTimeline(t); + await updateScrollPosition(scrollTimeline, 100); + + const animation = createAnimation(t); + await animation.ready; + + animation.timeline = scrollTimeline; + assert_false(animation.pending); + assert_equals(animation.playState, 'running'); + assert_scroll_synced_times(animation, 10, 10); +}, 'Setting a scroll timeline on a running animation synchronizes the ' + + 'currentTime of the animation with the scroll position.'); + +promise_test(async t => { + const scrollTimeline = createScrollTimeline(t); + await updateScrollPosition(scrollTimeline, 100); + + const animation = createAnimation(t); + animation.pause(); + await animation.ready; + + animation.timeline = scrollTimeline; + assert_false(animation.pending); + assert_equals(animation.playState, 'paused'); + assert_paused_times(animation, 10, 0); + + animation.play(); + await animation.ready; + + assert_scroll_synced_times(animation, 10, 10); +}, 'Setting a scroll timeline on a paused animation fixes the currentTime of ' + + 'the animation based on the scroll position when resumed'); + +promise_test(async t => { + const scrollTimeline = createScrollTimeline(t); + await updateScrollPosition(scrollTimeline, 100); + + const animation = createAnimation(t); + animation.reverse(); + animation.pause(); + await animation.ready; + + animation.timeline = scrollTimeline; + assert_false(animation.pending); + assert_equals(animation.playState, 'paused'); + assert_paused_times(animation, 10, 100); + + animation.play(); + await animation.ready; + + assert_scroll_synced_times(animation, 10, 90); +}, 'Setting a scroll timeline on a reversed paused animation ' + + 'fixes the currentTime of the animation based on the scroll ' + + 'position when resumed'); + +promise_test(async t => { + const animation = createAnimation(t); + const scrollTimeline = createScrollTimeline(t); + animation.timeline = scrollTimeline; + await animation.ready; + await updateScrollPosition(scrollTimeline, 100); + + animation.timeline = document.timeline; + assert_times_equal(animation.currentTime, 100); +}, 'Transitioning from a scroll timeline to a document timeline on a running ' + + 'animation preserves currentTime'); + +promise_test(async t => { + const animation = createAnimation(t); + const scrollTimeline = createScrollTimeline(t); + animation.timeline = scrollTimeline; + await animation.ready; + await updateScrollPosition(scrollTimeline, 100); + + animation.pause(); + animation.timeline = document.timeline; + + await animation.ready; + assert_times_equal(animation.currentTime, 100); +}, 'Transitioning from a scroll timeline to a document timeline on a ' + + 'pause-pending animation preserves currentTime'); + +promise_test(async t => { + const animation = createAnimation(t); + const scrollTimeline = createScrollTimeline(t); + animation.timeline = scrollTimeline; + await animation.ready; + + animation.reverse(); + animation.pause(); + await updateScrollPosition(scrollTimeline, 100); + assert_scroll_synced_times(animation, 10, 90); + + await animation.ready; + + animation.timeline = document.timeline; + assert_false(animation.pending); + assert_equals(animation.playState, 'paused'); + assert_times_equal(animation.currentTime, 900); +}, 'Transition from a scroll timeline to a document timeline on a reversed ' + + 'paused animation maintains correct currentTime'); + +promise_test(async t => { + const animation = createAnimation(t); + const scrollTimeline = createScrollTimeline(t); + animation.timeline = scrollTimeline; + await animation.ready; + await updateScrollPosition(scrollTimeline, 100); + + const progress = animation.currentTime.value / 100; + const duration = animation.effect.getTiming().duration; + animation.timeline = null; + + const expectedCurrentTime = progress * duration; + assert_times_equal(animation.currentTime, expectedCurrentTime); +}, 'Transitioning from a scroll timeline to a null timeline on a running ' + + 'animation preserves current progress.'); + +promise_test(async t => { + const keyframeEfect = new KeyframeEffect(createDiv(t), + { opacity: [0, 1] }, + 1000); + const animation = new Animation(keyframeEfect, null); + animation.startTime = 0; + assert_equals(animation.playState, 'running'); + + const scrollTimeline = createScrollTimeline(t); + await updateScrollPosition(scrollTimeline, 100); + + animation.timeline = scrollTimeline; + assert_equals(animation.playState, 'running'); + assert_percents_equal(animation.currentTime, 10); +}, 'Switching from a null timeline to a scroll timeline on an animation with ' + + 'a resolved start time preserved the play state'); + +promise_test(async t => { + const firstScrollTimeline = createScrollTimeline(t); + await updateScrollPosition(firstScrollTimeline, 100); + + const secondScrollTimeline = createScrollTimeline(t); + await updateScrollPosition(secondScrollTimeline, 200); + + const animation = createAnimation(t); + animation.timeline = firstScrollTimeline; + await animation.ready; + assert_percents_equal(animation.currentTime, 10); + + animation.timeline = secondScrollTimeline; + assert_percents_equal(animation.currentTime, 20); +}, 'Switching from one scroll timeline to another updates currentTime'); + +promise_test(async t => { + const scrollTimeline = createScrollTimeline(t); + await updateScrollPosition(scrollTimeline, 100); + + const animation = createPausedCssAnimation(t); + animation.timeline = scrollTimeline; + await animation.ready; + assert_equals(animation.playState, 'paused'); + assert_percents_equal(animation.currentTime, 0); + + const target = animation.effect.target; + target.style.animationPlayState = 'running'; + await animation.ready; + + assert_percents_equal(animation.currentTime, 10); +}, 'Switching from a document timeline to a scroll timeline updates ' + + 'currentTime when unpaused via CSS.'); + +promise_test(async t => { + const scrollTimeline = createScrollTimeline(t); + await updateScrollPosition(scrollTimeline, 100); + + const animation = createAnimation(t); + animation.pause(); + animation.timeline = scrollTimeline; + await animation.ready; + assert_percents_equal(animation.currentTime, 0); + + animation.currentTime = CSSNumericValue.parse("50%"); + + animation.play(); + await animation.ready; + assert_percents_equal(animation.currentTime, 50); +}, 'Switching from a document timeline to a scroll timeline and updating ' + + 'currentTime preserves the new value when unpaused.'); + +promise_test(async t => { + const scrollTimeline = createScrollTimeline(t); + await updateScrollPosition(scrollTimeline, 100); + + const animation = createAnimation(t); + animation.pause(); + animation.timeline = scrollTimeline; + await animation.ready; + assert_percents_equal(animation.currentTime, 0); + assert_equals(animation.playState, 'paused'); + + // Set a start time that will ensure that current time is in the active + // region in order not to trigger a seek when play is called. + animation.startTime = CSSNumericValue.parse("-10%"); + assert_equals(animation.playState, 'running'); + + animation.play(); + await animation.ready; + assert_percents_equal(animation.startTime, -10); +}, 'Switching from a document timeline to a scroll timeline and updating ' + + 'startTime preserves the new value when play is called.'); + +promise_test(async t => { + const elem = createDiv(t); + const animation = elem.animate(null, Infinity); + await animation.ready; + + animation.timeline = new ScrollTimeline(); + let timing = animation.effect.getComputedTiming(); + assert_percents_equal(timing.endTime, 100); + assert_percents_equal(timing.activeDuration, 100); + assert_percents_equal(timing.duration, 100); + + animation.effect.updateTiming({ iterations: 2 }); + timing = animation.effect.getComputedTiming(); + assert_percents_equal(timing.endTime, 100); + assert_percents_equal(timing.activeDuration, 100); + assert_percents_equal(timing.duration, 50); + + // Blink implementation does not permit setting an infinite number of + // iterations on a scroll-linked animation. Workaround by temporarily + // switching back to a document timeline. + animation.timeline = document.timeline; + animation.effect.updateTiming({ iterations: Infinity }); + animation.timeline = new ScrollTimeline(); + timing = animation.effect.getComputedTiming(); + // Having an infinite number of iterations with a finite timeline results in + // each iteration having zero duration. + assert_percents_equal(timing.duration, 0); + // If either the iteration duration or iteration count is zero, the active + // duration is always zero. + assert_percents_equal(timing.activeDuration, 0); + assert_percents_equal(timing.endTime, 0); + +}, 'Switching from a document timeline to a scroll timeline on an infinite ' + + 'duration animation.') + +</script> +</body> diff --git a/testing/web-platform/tests/scroll-animations/scroll-timelines/source-quirks-mode.html b/testing/web-platform/tests/scroll-animations/scroll-timelines/source-quirks-mode.html new file mode 100644 index 0000000000..17e95a0519 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/scroll-timelines/source-quirks-mode.html @@ -0,0 +1,36 @@ +<!-- Quirks mode --> +<html> +<head> + <title>ScrollTimeline default source in quirks mode</title> + <link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#dom-scrolltimeline-scrolltimeline"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <style> + /* This is just to make it possible for #body1 to be + "potentially scrollable". + + https://drafts.csswg.org/cssom-view/#potentially-scrollable */ + html { + overflow: hidden; + } + </style> +</head> +<body id=body1></body> +<script> +test(() => { + try { + assert_equals(document.scrollingElement.id, 'body1'); + assert_equals(new ScrollTimeline({}).source, body1); + + // Make #body1 "potentially scrollable". This causes the scrollingElement + // of the document to become null. + // + // https://drafts.csswg.org/cssom-view/#dom-document-scrollingelement + body1.style = 'overflow:scroll'; + assert_equals(new ScrollTimeline({}).source, null); + } finally { + body1.style = ''; + } +}, 'Style of <body> is reflected in source attribute in quirks mode'); +</script> +</html> diff --git a/testing/web-platform/tests/scroll-animations/scroll-timelines/testcommon.js b/testing/web-platform/tests/scroll-animations/scroll-timelines/testcommon.js new file mode 100644 index 0000000000..97e81f494c --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/scroll-timelines/testcommon.js @@ -0,0 +1,124 @@ +'use strict'; + +// Builds a generic structure that looks like: +// +// <div class="scroller"> // 100x100 viewport +// <div class="contents"></div> // 500x500 +// </div> +// +// The |scrollerOverrides| and |contentOverrides| parameters are maps which +// are applied to the scroller and contents style after basic setup. +// +// Appends the outer 'scroller' element to the document body, and returns it. +function setupScrollTimelineTest( + scrollerOverrides = new Map(), contentOverrides = new Map()) { + let scroller = document.createElement('div'); + scroller.style.width = '100px'; + scroller.style.height = '100px'; + // Hide the scrollbars, but maintain the ability to scroll. This setting + // ensures that variability in scrollbar sizing does not contribute to test + // failures or flakes. + scroller.style.overflow = 'hidden'; + for (const [key, value] of scrollerOverrides) { + scroller.style[key] = value; + } + + let contents = document.createElement('div'); + contents.style.width = '500px'; + contents.style.height = '500px'; + for (const [key, value] of contentOverrides) { + contents.style[key] = value; + } + + scroller.appendChild(contents); + document.body.appendChild(scroller); + return scroller; +} + +// Helper method to calculate the current time, implementing only step 5 of +// https://wicg.github.io/scroll-animations/#current-time-algorithm +function calculateCurrentTime( + currentScrollOffset, startScrollOffset, endScrollOffset) { + return ((currentScrollOffset - startScrollOffset) / + (endScrollOffset - startScrollOffset)) * + 100; +} + +function createScroller(test) { + var scroller = createDiv(test); + scroller.innerHTML = "<div class='contents'></div>"; + scroller.classList.add('scroller'); + // Trigger layout run. + scroller.scrollTop; + return scroller; +} + +function createScrollerWithStartAndEnd(test, orientationClass = 'vertical') { + var scroller = createDiv(test); + scroller.innerHTML = + `<div class='contents'> + <div id='start'></div> + <div id='end'></div> + </div>`; + scroller.classList.add('scroller'); + scroller.classList.add(orientationClass); + + return scroller; +} + +function createScrollTimeline(test, options) { + options = options || { + source: createScroller(test) + } + return new ScrollTimeline(options); +} + +function createScrollLinkedAnimation(test, timeline) { + return createScrollLinkedAnimationWithTiming(test, /* duration in ms*/ 1000, timeline); +} + +function createScrollLinkedAnimationWithTiming(test, timing, timeline) { + if (timeline === undefined) + timeline = createScrollTimeline(test); + const KEYFRAMES = { opacity: [0, 1] }; + return new Animation( + new KeyframeEffect(createDiv(test), KEYFRAMES, timing), timeline); +} + +function assert_approx_equals_or_null(actual, expected, tolerance, name) { + if (actual === null || expected === null){ + assert_equals(actual, expected, name); + } + else { + assert_approx_equals(actual, expected, tolerance, name); + } +} + +function assert_percents_approx_equal(actual, expected, maxScroll, + description) { + // Base the tolerance on being out by up to half a pixel. + const tolerance = 0.5 / maxScroll * 100; + assert_equals(actual.unit, "percent", `'actual' unit type must be ` + + `'percent' for "${description}"`); + assert_true(actual instanceof CSSUnitValue, `'actual' must be of type ` + + `CSSNumberish for "${description}"`); + if (expected instanceof CSSUnitValue){ + // Verify that when the expected in a CSSUnitValue, it is the correct unit + // type + assert_equals(expected.unit, "percent", `'expected' unit type must be ` + + `'percent' for "${description}"`); + assert_approx_equals(actual.value, expected.value, tolerance, + `values do not match for "${description}"`); + } else if (typeof expected, "number"){ + assert_approx_equals(actual.value, expected, tolerance, + `values do not match for "${description}"`); + } +} + +function assert_percents_equal(actual, expected, description) { + // Rough estimate of the default size of the scrollable area based on + // sizes in setupScrollTimelineTest. + const defaultScrollRange = 400; + return assert_percents_approx_equal(actual, expected, defaultScrollRange, + description); +} diff --git a/testing/web-platform/tests/scroll-animations/scroll-timelines/two-animations-attach-to-same-scroll-timeline-cancel-one.html b/testing/web-platform/tests/scroll-animations/scroll-timelines/two-animations-attach-to-same-scroll-timeline-cancel-one.html new file mode 100644 index 0000000000..ed8e8337a6 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/scroll-timelines/two-animations-attach-to-same-scroll-timeline-cancel-one.html @@ -0,0 +1,84 @@ +<html class="reftest-wait"> +<title>Scroll timeline shared by two animation, one gets cancelled</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations/"> +<meta name="assert" content="Cancelling animations should not affect other + animation that is attached to the same timeline."> +<link rel="match" href="animation-ref.html"> + +<script src="/web-animations/testcommon.js"></script> +<script src="/common/reftest-wait.js"></script> + +<style> + #box { + width: 100px; + height: 100px; + background-color: green; + } + + #covered { + width: 100px; + height: 100px; + background-color: red; + } + + #scroller { + overflow: auto; + height: 100px; + width: 100px; + will-change: transform; /* force compositing */ + } + + #contents { + height: 1000px; + width: 100%; + } +</style> + +<div id="box"></div> +<div id="covered"></div> +<div id="scroller"> + <div id="contents"><p>Scrolling Contents</p></div> +</div> + +<script> + const box = document.getElementById('box'); + const effect = new KeyframeEffect(box, + [ + { transform: 'translateY(0)', opacity: 1}, + { transform: 'translateY(200px)', opacity: 0} + ], { + duration: 1000, + } + ); + const temporary_effect = new KeyframeEffect(box, + [ + { transform: 'translateX(0)'}, + { transform: 'translateX(200px)'} + ], { + duration: 1000, + } + ); + + const scroller = document.getElementById('scroller'); + const timeline = new ScrollTimeline( + { source: scroller, orientation: 'block' }); + const animation = new Animation(effect, timeline); + const temporary_animation = new Animation(temporary_effect, timeline); + animation.play(); + temporary_animation.play(); + + Promise.all([animation.ready, temporary_animation.ready]).then(() => { + temporary_animation.cancel(); + temporary_animation.ready.then(() => { + waitForAnimationFrames(2).then(_ => { + // Move the scroller to the halfway point. + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.5 * maxScroll; + + waitForAnimationFrames(2).then(_ => { + takeScreenshot(); + }); + }); + }); + }); +</script> diff --git a/testing/web-platform/tests/scroll-animations/scroll-timelines/two-animations-attach-to-same-scroll-timeline.html b/testing/web-platform/tests/scroll-animations/scroll-timelines/two-animations-attach-to-same-scroll-timeline.html new file mode 100644 index 0000000000..de50599fba --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/scroll-timelines/two-animations-attach-to-same-scroll-timeline.html @@ -0,0 +1,79 @@ +<html class="reftest-wait"> +<title>Scroll timeline shared by two animation</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations/"> +<meta name="assert" content="Should be able to use the same scroll timeline to +drive two animations"> +<link rel="match" href="animation-ref.html"> + +<script src="/web-animations/testcommon.js"></script> +<script src="/common/reftest-wait.js"></script> + +<style> + #box { + width: 100px; + height: 100px; + background-color: green; + } + + #covered { + width: 100px; + height: 100px; + background-color: red; + } + + #scroller { + overflow: auto; + height: 100px; + width: 100px; + will-change: transform; /* force compositing */ + } + + #contents { + height: 1000px; + width: 100%; + } +</style> + +<div id="box"></div> +<div id="covered"></div> +<div id="scroller"> + <div id="contents"><p>Scrolling Contents</p></div> +</div> + +<script> + const box = document.getElementById('box'); + const transform_effect = new KeyframeEffect(box, + [ + { transform: 'translateY(0)'}, + { transform: 'translateY(200px)'} + ], { + duration: 1000, + } + ); + const opacity_effect = new KeyframeEffect(box, + [ + { opacity: 1}, + { opacity: 0} + ], { + duration: 1000, + } + ); + + const scroller = document.getElementById('scroller'); + const timeline = new ScrollTimeline( + { source: scroller, orientation: 'block' }); + const transform_animation = new Animation(transform_effect, timeline); + transform_animation.play(); + const opacity_animation = new Animation(opacity_effect, timeline); + opacity_animation.play(); + + Promise.all([transform_animation.ready, opacity_animation.ready]).then(() => { + // Move the scroller to the halfway point. + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = 0.5 * maxScroll; + + waitForAnimationFrames(2).then(_ => { + takeScreenshot(); + }); + }); +</script> diff --git a/testing/web-platform/tests/scroll-animations/scroll-timelines/update-playback-rate.html b/testing/web-platform/tests/scroll-animations/scroll-timelines/update-playback-rate.html new file mode 100644 index 0000000000..1db31602f3 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/scroll-timelines/update-playback-rate.html @@ -0,0 +1,181 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Seamlessly updating the playback rate of an animation</title> +<link rel="help" + href="https://drafts.csswg.org/web-animations-1/#seamlessly-updating-the-playback-rate-of-an-animation"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="testcommon.js"></script> +<style> +.scroller { + overflow: auto; + height: 100px; + width: 100px; + will-change: transform; +} + +.contents { + height: 1000px; + width: 100%; +} +</style> +<body> +<script> +'use strict'; + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + await animation.ready; + + animation.currentTime = CSSNumericValue.parse("50%"); + + animation.updatePlaybackRate(0.5); + await animation.ready; + assert_percents_equal(animation.currentTime, 50, + 'Reducing the playback rate should not change the ' + + 'current time of a playing animation'); + + animation.updatePlaybackRate(2); + await animation.ready; + assert_percents_equal(animation.currentTime, 50, + 'Increasing the playback rate should not change the ' + + 'current time of a playing animation'); +}, 'Updating the playback rate maintains the current time'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + await animation.ready; + + assert_false(animation.pending); + animation.updatePlaybackRate(2); + assert_true(animation.pending); +}, 'Updating the playback rate while running makes the animation pending'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + animation.currentTime = CSSNumericValue.parse("50%"); + assert_true(animation.pending); + + animation.updatePlaybackRate(0.5); + + // Check that the hold time is updated as expected + assert_percents_equal(animation.currentTime, 50); + + await animation.ready; + + // As above, check that the currentTime is not calculated by simply + // substituting in the updated playbackRate without updating the startTime. + assert_percents_equal(animation.currentTime, 50, + 'Reducing the playback rate should not change the ' + + 'current time of a play-pending animation'); +}, 'Updating the playback rate on a play-pending animation maintains the ' + + 'current time'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + animation.currentTime = CSSNumericValue.parse("50%"); + await animation.ready; + + animation.pause(); + animation.updatePlaybackRate(0.5); + + assert_percents_equal(animation.currentTime, 50); +}, 'Updating the playback rate on a pause-pending animation maintains the ' + + 'current time'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + + animation.updatePlaybackRate(2); + animation.updatePlaybackRate(3); + animation.updatePlaybackRate(4); + + assert_equals(animation.playbackRate, 1); + await animation.ready; + + assert_equals(animation.playbackRate, 4); +}, 'If a pending playback rate is set multiple times, the latest wins'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + animation.cancel(); + + animation.updatePlaybackRate(2); + assert_equals(animation.playbackRate, 2); + assert_false(animation.pending); +}, 'In the idle state, the playback rate is applied immediately'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.pause(); + await animation.ready; + + animation.updatePlaybackRate(2); + assert_equals(animation.playbackRate, 2); + assert_false(animation.pending); +}, 'In the paused state, the playback rate is applied immediately'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + animation.finish(); + assert_percents_equal(animation.currentTime, 100); + assert_false(animation.pending); + + animation.updatePlaybackRate(2); + assert_equals(animation.playbackRate, 2); + assert_percents_equal(animation.currentTime, 100); + assert_false(animation.pending); +}, 'Updating the playback rate on a finished animation maintains the current ' + + 'time'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + animation.play(); + animation.finish(); + assert_percents_equal(animation.currentTime, 100); + assert_false(animation.pending); + + animation.updatePlaybackRate(0); + assert_equals(animation.playbackRate, 0); + assert_percents_equal(animation.currentTime, 100); + assert_false(animation.pending); +}, 'Updating the playback rate to zero on a finished animation maintains the ' + + 'current time'); + +</script> +</body> diff --git a/testing/web-platform/tests/scroll-animations/scroll-timelines/updating-the-finished-state.html b/testing/web-platform/tests/scroll-animations/scroll-timelines/updating-the-finished-state.html new file mode 100644 index 0000000000..86b52d5aa0 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/scroll-timelines/updating-the-finished-state.html @@ -0,0 +1,565 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Updating the finished state</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#updating-the-finished-state"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="testcommon.js"></script> +<style> +.scroller { + overflow: auto; + height: 100px; + width: 100px; + will-change: transform; +} + +.contents { + height: 1000px; + width: 100%; +} +</style> +<body> +<script> +'use strict'; + +// -------------------------------------------------------------------- +// +// TESTS FOR UPDATING THE HOLD TIME +// +// -------------------------------------------------------------------- + +// CASE 1: playback rate > 0 and current time >= target effect end +// (Also the start time is resolved and there is pending task) +// Did seek = true +promise_test(async t => { + const anim = createScrollLinkedAnimation(t); + const scroller = anim.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + anim.play(); + + await anim.ready; + + anim.currentTime = CSS.percent(200); + scroller.scrollTop = 0.7 * maxScroll; + await waitForNextFrame(); + + assert_percents_equal(anim.currentTime, 200, + 'Hold time is set so current time should NOT change'); +}, 'Updating the finished state when seeking past end'); + +// Did seek = false +promise_test(async t => { + const anim = createScrollLinkedAnimation(t); + const scroller = anim.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + anim.play(); + await anim.ready; + + scroller.scrollTop = maxScroll; + await waitForNextFrame(); + + assert_percents_equal(anim.currentTime, 100, + 'Hold time is set to target end clamping current time'); +}, 'Updating the finished state when playing exactly to end'); + +// Did seek = true +promise_test(async t => { + const anim = createScrollLinkedAnimation(t); + const scroller = anim.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + await anim.ready; + + anim.currentTime = CSS.percent(100); + scroller.scrollTop = 0.7 * maxScroll; + await waitForNextFrame(); + + assert_percents_equal(anim.currentTime, 100, + 'Hold time is set so current time should NOT change'); +}, 'Updating the finished state when seeking exactly to end'); + + +// CASE 2: playback rate < 0 and current time <= 0 +// (Also the start time is resolved and there is pending task) + +// Did seek = false +promise_test(async t => { + const anim = createScrollLinkedAnimation(t); + const scroller = anim.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + anim.playbackRate = -1; + anim.play(); // Make sure animation is not initially finished + + await anim.ready; + + // Seek to 1ms before 0 and then wait 1ms + anim.currentTime = CSS.percent(1); + scroller.scrollTop = 0.2 * maxScroll; + await waitForNextFrame(); + + assert_percents_equal(anim.currentTime, 0, + 'Hold time is set to zero clamping current time'); +}, 'Updating the finished state when playing in reverse past zero'); + +// Did seek = true +promise_test(async t => { + const anim = createScrollLinkedAnimation(t); + const scroller = anim.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + anim.playbackRate = -1; + anim.play(); + + await anim.ready; + + anim.currentTime = CSS.percent(-100); + scroller.scrollTop = 0.2 * maxScroll; + await waitForNextFrame(); + + assert_percents_equal(anim.currentTime, -100, + 'Hold time is set so current time should NOT change'); +}, 'Updating the finished state when seeking a reversed animation past zero'); + +// Did seek = false +promise_test(async t => { + const anim = createScrollLinkedAnimation(t); + const scroller = anim.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + anim.playbackRate = -1; + anim.play(); + await anim.ready; + + scroller.scrollTop = maxScroll; + await waitForNextFrame(); + + assert_percents_equal(anim.currentTime, 0, + 'Hold time is set to target end clamping current time'); +}, 'Updating the finished state when playing a reversed animation exactly ' + + 'to zero'); + +// Did seek = true +promise_test(async t => { + const anim = createScrollLinkedAnimation(t); + const scroller = anim.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + anim.playbackRate = -1; + anim.play(); + await anim.ready; + + anim.currentTime = CSS.percent(0); + + scroller.scrollTop = 0.2 * maxScroll; + await waitForNextFrame(); + + assert_percents_equal(anim.currentTime, 0, + 'Hold time is set so current time should NOT change'); +}, 'Updating the finished state when seeking a reversed animation exactly ' + + 'to zero'); + +// CASE 3: playback rate > 0 and current time < target end OR +// playback rate < 0 and current time > 0 +// (Also the start time is resolved and there is pending task) + +// Did seek = true; playback rate > 0 +promise_test(async t => { + const anim = createScrollLinkedAnimation(t); + const scroller = anim.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + anim.play(); + anim.finish(); + await anim.ready; + assert_percents_equal(anim.startTime, -100); + + anim.currentTime = CSS.percent(50); + // When did seek = true, updating the finished state: (i) updates + // the animation's start time and (ii) clears the hold time. + // We can test both by checking that the currentTime is initially + // updated and then increases. + assert_percents_equal(anim.currentTime, 50, 'Start time is updated'); + assert_percents_equal(anim.startTime, -50); + + scroller.scrollTop = 0.2 * maxScroll; + await waitForNextFrame(); + + assert_percents_equal(anim.currentTime, 70, + 'Hold time is not set so current time should increase'); +}, 'Updating the finished state when seeking before end'); + +// Did seek = false; playback rate < 0 +// +// Unfortunately it is not possible to test this case. We need to have +// a hold time set, a resolved start time, and then perform some +// operation that updates the finished state with did seek set to true. +// +// However, the only situation where this could arrive is when we +// replace the timeline and that procedure is likely to change. For all +// other cases we either have an unresolved start time (e.g. when +// paused), we don't have a set hold time (e.g. regular playback), or +// the current time is zero (and anything that gets us out of that state +// will set did seek = true). + +// Did seek = true; playback rate < 0 +promise_test(async t => { + const anim = createScrollLinkedAnimation(t); + const scroller = anim.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + anim.play(); + anim.playbackRate = -1; + await anim.ready; + + anim.currentTime = CSS.percent(50); + assert_percents_equal(anim.startTime, 50, 'Start time is updated'); + assert_percents_equal(anim.currentTime, 50, 'Current time is updated'); + + scroller.scrollTop = 0.2 * maxScroll; + await waitForNextFrame(); + + assert_percents_equal(anim.currentTime, 30, + 'Hold time is not set so current time should decrease'); +}, 'Updating the finished state when seeking a reversed animation before end'); + + +// CASE 4: playback rate == 0 + +// current time < 0 +promise_test(async t => { + const anim = createScrollLinkedAnimation(t); + const scroller = anim.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + anim.play(); + anim.playbackRate = 0; + await anim.ready; + + anim.currentTime = CSS.percent(-100); + + scroller.scrollTop = 0.2 * maxScroll; + await waitForNextFrame(); + + assert_percents_equal(anim.currentTime, -100, + 'Hold time should not be cleared so current time should NOT change'); +}, 'Updating the finished state when playback rate is zero and the current ' + + 'time is less than zero'); + +// current time < target end +promise_test(async t => { + const anim = createScrollLinkedAnimation(t); + const scroller = anim.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + anim.play(); + + anim.playbackRate = 0; + await anim.ready; + + anim.currentTime = CSS.percent(50); + scroller.scrollTop = 0.2 * maxScroll; + await waitForNextFrame(); + + assert_percents_equal(anim.currentTime, 50, + 'Hold time should not be cleared so current time should NOT change'); +}, 'Updating the finished state when playback rate is zero and the current ' + + 'time is less than end'); + +// current time > target end +promise_test(async t => { + const anim = createScrollLinkedAnimation(t); + const scroller = anim.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + anim.play(); + anim.playbackRate = 0; + await anim.ready; + + anim.currentTime = CSS.percent(200); + scroller.scrollTop = 0.2 * maxScroll; + await waitForNextFrame(); + + assert_percents_equal(anim.currentTime, 200, + 'Hold time should not be cleared so current time should NOT change'); +}, 'Updating the finished state when playback rate is zero and the current' + + 'time is greater than end'); + +// CASE 5: current time unresolved + +promise_test(async t => { + const anim = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + anim.play(); + anim.cancel(); + // Trigger a change that will cause the "update the finished state" + // procedure to run. + anim.effect.updateTiming({ duration: 2000 }); + assert_equals(anim.currentTime, null, + 'The animation hold time / start time should not be updated'); + // The "update the finished state" procedure is supposed to run after any + // change to timing, but just in case an implementation defers that, let's + // wait a frame and check that the hold time / start time has still not been + // updated. + await waitForAnimationFrames(1); + + assert_equals(anim.currentTime, null, + 'The animation hold time / start time should not be updated'); +}, 'Updating the finished state when current time is unresolved'); + +// CASE 7: start time unresolved + +// Did seek = true +promise_test(async t => { + const anim = createScrollLinkedAnimation(t); + // Wait for new animation frame which allows the timeline to compute new + // current time. + await waitForNextFrame(); + anim.cancel(); + anim.currentTime = CSS.percent(150); + // Trigger a change that will cause the "update the finished state" + // procedure to run. + anim.currentTime = CSS.percent(50); + assert_percents_equal(anim.currentTime, 50, + 'The animation hold time should not be updated'); + assert_equals(anim.startTime, null, + 'The animation start time should not be updated'); +}, 'Updating the finished state when start time is unresolved and did seek = ' + + 'true'); + +// -------------------------------------------------------------------- +// +// TESTS FOR RUNNING FINISH NOTIFICATION STEPS +// +// -------------------------------------------------------------------- + +function waitForFinishEventAndPromise(animation) { + const eventPromise = new Promise(resolve => { + animation.onfinish = resolve; + }); + return Promise.all([eventPromise, animation.finished]); +} + +promise_test(t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + animation.play(); + animation.onfinish = + t.unreached_func('Seeking to finish should not fire finish event'); + animation.finished.then( + t.unreached_func( + 'Seeking to finish should not resolve finished promise')); + animation.currentTime = CSS.percent(100); + animation.currentTime = CSS.percent(0); + animation.pause(); + scroller.scrollTop = 0.2 * maxScroll; + return waitForAnimationFrames(3); +}, 'Finish notification steps don\'t run when the animation seeks to finish ' + + 'and then seeks back again'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + animation.play(); + await animation.ready; + scroller.scrollTop = maxScroll; + + return waitForFinishEventAndPromise(animation); +}, 'Finish notification steps run when the animation completes normally'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + animation.effect.target = null; + + animation.play(); + await animation.ready; + scroller.scrollTop = maxScroll; + return waitForFinishEventAndPromise(animation); +}, 'Finish notification steps run when an animation without a target effect ' + + 'completes normally'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + animation.play(); + await animation.ready; + + animation.currentTime = CSS.percent(101); + return waitForFinishEventAndPromise(animation); +}, 'Finish notification steps run when the animation seeks past finish'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + animation.play(); + await animation.ready; + + // Register for notifications now since once we seek away from being + // finished the 'finished' promise will be replaced. + const finishNotificationSteps = waitForFinishEventAndPromise(animation); + animation.finish(); + animation.currentTime = CSS.percent(0); + animation.pause(); + return finishNotificationSteps; +}, 'Finish notification steps run when the animation completes with ' + + '.finish(), even if we then seek away'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + + animation.play(); + scroller.scrollTop = maxScroll; + const initialFinishedPromise = animation.finished; + await animation.finished; + + animation.currentTime = CSS.percent(0); + assert_not_equals(initialFinishedPromise, animation.finished); +}, 'Animation finished promise is replaced after seeking back to start'); + +promise_test(async t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + + animation.play(); + + const initialFinishedPromise = animation.finished; + scroller.scrollTop = maxScroll; + await animation.finished; + + scroller.scrollTop = 0; + await waitForNextFrame(); + + animation.play(); + assert_not_equals(initialFinishedPromise, animation.finished); +}, 'Animation finished promise is replaced after replaying from start'); + +async_test(t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + + animation.play(); + + animation.onfinish = event => { + scroller.scrollTop = 0; + window.requestAnimationFrame(function() { + window.requestAnimationFrame(function() { + scroller.scrollTop = maxScroll; + }); + }); + animation.onfinish = event => { + t.done(); + }; + }; + scroller.scrollTop = maxScroll; +}, 'Animation finish event is fired again after seeking back to start'); + +async_test(t => { + const animation = createScrollLinkedAnimation(t); + const scroller = animation.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + + animation.play(); + + animation.onfinish = event => { + scroller.scrollTop = 0; + window.requestAnimationFrame(function() { + animation.play(); + scroller.scrollTop = maxScroll; + animation.onfinish = event => { + t.done(); + }; + }); + }; + scroller.scrollTop = maxScroll; +}, 'Animation finish event is fired again after replaying from start'); + +async_test(t => { + const anim = createScrollLinkedAnimation(t); + const scroller = anim.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + + anim.effect.updateTiming({ duration: 800, endDelay: 200}); + + anim.onfinish = t.step_func(event => { + assert_unreached('finish event should not be fired'); + }); + anim.play(); + anim.ready.then(() => { + scroller.scrollTop = 0.9 * maxScroll; + return waitForAnimationFrames(3); + }).then(t.step_func(() => { + t.done(); + })); +}, 'finish event is not fired at the end of the active interval when the ' + + 'endDelay has not expired'); + +async_test(t => { + const anim = createScrollLinkedAnimation(t); + const scroller = anim.timeline.source; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + + anim.effect.updateTiming({ duration: 800, endDelay: 100}); + anim.play(); + anim.ready.then(() => { + scroller.scrollTop = 0.95 * maxScroll; // during endDelay + anim.onfinish = t.step_func(event => { + assert_unreached('onfinish event should not be fired during endDelay'); + }); + return waitForAnimationFrames(2); + }).then(t.step_func(() => { + anim.onfinish = t.step_func(event => { + t.done(); + }); + scroller.scrollTop = maxScroll; + return waitForAnimationFrames(2); + })); +}, 'finish event is fired after the endDelay has expired'); + +</script> +</body> 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> |