diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /testing/web-platform/tests/scroll-animations/css | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/scroll-animations/css')
101 files changed, 10451 insertions, 0 deletions
diff --git a/testing/web-platform/tests/scroll-animations/css/animation-duration-auto.tentative.html b/testing/web-platform/tests/scroll-animations/css/animation-duration-auto.tentative.html new file mode 100644 index 0000000000..0198285913 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/animation-duration-auto.tentative.html @@ -0,0 +1,58 @@ +<!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', '0s'); + 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', + '0s cubic-bezier(0, -2, 1, 3) -3s 4 reverse both paused anim'); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/animation-events.html b/testing/web-platform/tests/scroll-animations/css/animation-events.html new file mode 100644 index 0000000000..be53af487a --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/animation-events.html @@ -0,0 +1,87 @@ +<!DOCTYPE html> +<html id="top"> +<meta charset="utf-8"> +<title>View timeline animation events</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#events"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<style type="text/css"> + @keyframes anim { + from { transform: translateX(0); } + to { transform: translateX(100px); } + } + #target { + background: green; + height: 100px; + width: 100px; + margin-bottom: 150vh; + animation-timeline: view(); + } + .animate { + animation: anim auto; + } +</style> +<body> + <div id="target"></div> +</body> +<script type="text/javascript"> + promise_test(async t => { + const target = document.getElementById('target'); + + // Create a timeline and advance to the next frame to ensure that the + // timeline has a value for currentTime. + await waitForNextFrame(); + const timeline = new ViewTimeline({ subject: target }); + await waitForNextFrame(); + + let animationstart_events = 0; + let animationend_events = 0; + document.addEventListener('animationstart', () => { + animationstart_events++; + }); + document.addEventListener('animationend', () => { + animationend_events++; + }); + + // Start the animation and swap out its timeline while still play-pending + // so that it already has a value for current time. + target.classList.add('animate'); + const anim = target.getAnimations(); + anim.timeline = timeline; + // Introduce a style change that will make the timeline state stale when + // "ticked" at the start of the next animation frame. + target.style = 'margin-top: 150vh'; + + assert_false(!!anim.startTime, + 'Start time deferred until timeline is updated'); + + // Verify that we are not evaluating a start time based on a stale timeline. + await waitForNextFrame(); + await waitForNextFrame(); + assert_equals(animationstart_events, 0, + 'Target initially off-screen and no animationstart event'); + assert_equals(animationend_events, 0, + 'Target initially off-screen and no animationend event'); + + const scroller = document.scrollingElement; + scroller.scrollTop = target.getBoundingClientRect().top; + await waitForNextFrame(); + await waitForNextFrame(); + + assert_equals(animationstart_events, 1, + 'scrollstart event received after scrolling into view.'); + assert_equals(animationend_events, 0, + "No scrollend event until after scrolling out of view"); + + scroller.scrollTop = target.getBoundingClientRect().bottom; + + await waitForNextFrame(); + await waitForNextFrame(); + + assert_equals(animationstart_events, 1, + 'No additional scrollstart event'); + assert_equals(animationend_events, 1, + 'scrollend event received after scrolling out of view'); + }, 'View timelime generates animationstart and animationend events'); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/animation-fill-outside-range-ref.html b/testing/web-platform/tests/scroll-animations/css/animation-fill-outside-range-ref.html new file mode 100644 index 0000000000..998576b3a4 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/animation-fill-outside-range-ref.html @@ -0,0 +1,97 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/"> +<script src="/web-animations/testcommon.js"></script> +<style> + .scroller { + height: 200px; + width: 500px; + overflow: auto; + position: absolute; + top: 100px; + } + + .anim { + position: absolute; + width: 100px; + height: 100px; + background: darkred; + } + .anim.contain { + background: green; + } + .spacer { + height: 1000px; + } + + .before { + top: 450px; + } + .after { + top: 50px; + } + .contain { + top: 250px; + } + .indicator { + position: fixed; + top: 50px; + } + .contain .indicator { + top: 100px; + } + .contain .indicator:nth-child(2) { + left: 200px; + } + + .after .indicator { + left: 200px; + } + + .indicator > div { + display: inline-block; + width: 25px; + height: 25px; + position: relative; + border-radius: 100%; + box-sizing: border-box; + border: 2px solid black; + padding: 3px; + background: lightgray; + background-clip: content-box; + } + + .indicator > div > div { + width: 100%; + height: 100%; + border-radius: 100%; + background: green; + opacity: 1; + will-change: opacity; + } + +</style> +</head> +<body> + <p>All of the activity indicators should be active as the animations should be filling.</p> + <div class="scroller"> + <div class="anim after"><div class="indicator">After cover phase: <div><div></div></div></div></div> + <div class="anim before"><div class="indicator">Before cover phase: <div><div></div></div></div></div> + <div class="anim contain"> + <div class="indicator entry">After entry phase: <div><div></div></div></div> + <div class="indicator exit">Before exit phase: <div><div></div></div></div> + </div> + <div class="spacer"></div> + </div> +</body> +<script> + function run() { + let scroller = document.querySelector('.scroller'); + scroller.scrollTo({top: 200}); + } + run(); +</script> +</html> diff --git a/testing/web-platform/tests/scroll-animations/css/animation-fill-outside-range-test.html b/testing/web-platform/tests/scroll-animations/css/animation-fill-outside-range-test.html new file mode 100644 index 0000000000..90d4f4518b --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/animation-fill-outside-range-test.html @@ -0,0 +1,137 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/"> +<link rel="match" href="animation-fill-outside-range-ref.html"> +<script src="/common/reftest-wait.js"></script> +<script src="/web-animations/testcommon.js"></script> +<style> + .scroller { + height: 200px; + width: 500px; + overflow: auto; + position: absolute; + top: 100px; + } + + .anim { + position: absolute; + width: 100px; + height: 100px; + background: darkred; + view-timeline: --view; + } + .anim.contain { + background: green; + } + .spacer { + height: 1000px; + } + + .before { + top: 450px; + } + .after { + top: 50px; + } + .contain { + top: 250px; + } + @keyframes opaque-before { + 0% { opacity: 1; } + 0.01% { opacity : 0; } + 100% { opacity : 0; } + } + @keyframes opaque-after { + 0% { opacity: 0; } + 99.9% { opacity : 0; } + 100% { opacity : 1; } + } + .indicator { + position: fixed; + top: 50px; + } + .contain .indicator { + top: 100px; + } + .contain .indicator:nth-child(2) { + left: 200px; + } + + .after .indicator { + left: 200px; + } + + .indicator > div { + display: inline-block; + width: 25px; + height: 25px; + position: relative; + border-radius: 100%; + box-sizing: border-box; + border: 2px solid black; + padding: 3px; + background: lightgray; + background-clip: content-box; + } + + .indicator > div > div { + width: 100%; + height: 100%; + border-radius: 100%; + background: green; + opacity: 0; + animation-fill-mode: both; + animation-timeline: --view; + } + + .after .indicator > div > div, + .contain .indicator > div > div { + animation-name: opaque-after; + } + .before .indicator > div > div, + .contain .indicator:nth-child(2) > div > div { + animation-name: opaque-before; + } + + .contain .indicator > div > div { + animation-range: entry; + } + + .contain .indicator:nth-child(2) > div > div { + animation-range: exit; + } + +</style> +</head> +<body> + <p>All of the activity indicators should be active as the animations should be filling.</p> + <div class="scroller"> + <div class="anim after"><div class="indicator">After cover phase: <div><div></div></div></div></div> + <div class="anim before"><div class="indicator">Before cover phase: <div><div></div></div></div></div> + <div class="anim contain"> + <div class="indicator entry">After entry phase: <div><div></div></div></div> + <div class="indicator exit">Before exit phase: <div><div></div></div></div> + </div> + <div class="spacer"></div> + </div> +</body> +<script> + async function run() { + let scroller = document.querySelector('.scroller'); + // Scroll such that each animation becomes active. + scroller.scrollTo(0, 0); + await waitForCompositorReady(); + scroller.scrollTo({top: 400}); + await waitForNextFrame(); + + // Then scroll between them so that we are before one and after the other. + scroller.scrollTo({top: 200}); + await waitForNextFrame(); + takeScreenshot(); + } + run(); +</script> +</html> diff --git a/testing/web-platform/tests/scroll-animations/css/animation-inactive-outside-range-ref.html b/testing/web-platform/tests/scroll-animations/css/animation-inactive-outside-range-ref.html new file mode 100644 index 0000000000..e744055140 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/animation-inactive-outside-range-ref.html @@ -0,0 +1,100 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/"> +<script src="/web-animations/testcommon.js"></script> +<script src="/common/reftest-wait.js"></script> +<style> + .scroller { + height: 200px; + width: 500px; + overflow: auto; + position: absolute; + top: 100px; + } + + .anim { + position: absolute; + width: 100px; + height: 100px; + background: darkred; + view-timeline: --view; + } + .anim.contain { + background: green; + } + .spacer { + height: 1000px; + } + + .before { + top: 450px; + } + .after { + top: 50px; + } + .contain { + top: 250px; + } + .indicator { + position: fixed; + top: 50px; + } + .contain .indicator { + top: 100px; + } + .contain .indicator:nth-child(2) { + left: 200px; + } + + .after .indicator { + left: 200px; + } + + .indicator > div { + display: inline-block; + width: 25px; + height: 25px; + position: relative; + border-radius: 100%; + box-sizing: border-box; + border: 2px solid black; + padding: 3px; + background: lightgray; + background-clip: content-box; + } + + .indicator > div > div { + width: 100%; + height: 100%; + border-radius: 100%; + background: green; + opacity: 0; + } +</style> +</head> +<body onload="run()"> + <p>None of the activity indicators should be active all of the animations are outside of their active range.</p> + <div class="scroller"> + <div class="anim after"><div class="indicator">After cover phase: <div><div></div></div></div></div> + <div class="anim before"><div class="indicator">Before cover phase: <div><div></div></div></div></div> + <div class="anim contain"> + <div class="indicator entry">After entry phase: <div><div></div></div></div> + <div class="indicator exit">Before exit phase: <div><div></div></div></div> + </div> + <div class="spacer"></div> + </div> +</body> +<script> + async function run() { + // Ensure we don't take the screenshot while paint-holding. + await waitForCompositorReady(); + let scroller = document.querySelector('.scroller'); + scroller.scrollTo(0, 200); + await waitForNextFrame(); + takeScreenshot(); + } +</script> +</html> diff --git a/testing/web-platform/tests/scroll-animations/css/animation-inactive-outside-range-test.html b/testing/web-platform/tests/scroll-animations/css/animation-inactive-outside-range-test.html new file mode 100644 index 0000000000..8034e451be --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/animation-inactive-outside-range-test.html @@ -0,0 +1,124 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/"> +<link rel="match" href="animation-inactive-outside-range-ref.html"> +<script src="/common/reftest-wait.js"></script> +<script src="/web-animations/testcommon.js"></script> +<style> + .scroller { + height: 200px; + width: 500px; + overflow: auto; + position: absolute; + top: 100px; + } + + .anim { + position: absolute; + width: 100px; + height: 100px; + background: darkred; + view-timeline: --view; + } + .anim.contain { + background: green; + } + .spacer { + height: 1000px; + } + + .before { + top: 450px; + } + .after { + top: 50px; + } + .contain { + top: 250px; + } + @keyframes active-opacity { + 0% { opacity: 1; } + 100% { opacity: 1; } + } + .indicator { + position: fixed; + top: 50px; + } + .contain .indicator { + top: 100px; + } + .contain .indicator:nth-child(2) { + left: 200px; + } + + .after .indicator { + left: 200px; + } + + .indicator > div { + display: inline-block; + width: 25px; + height: 25px; + position: relative; + border-radius: 100%; + box-sizing: border-box; + border: 2px solid black; + padding: 3px; + background: lightgray; + background-clip: content-box; + } + + .indicator > div > div { + width: 100%; + height: 100%; + border-radius: 100%; + background: green; + opacity: 0; + animation: active-opacity; + animation-timeline: --view; + } + + .contain .indicator > div > div { + animation-range: entry; + } + + .contain .indicator:nth-child(2) > div > div { + animation-range: exit; + } + +</style> +</head> +<body onload="run()"> + <p>None of the activity indicators should be active all of the animations are outside of their active range.</p> + <div class="scroller"> + <div class="anim after"><div class="indicator">After cover phase: <div><div></div></div></div></div> + <div class="anim before"><div class="indicator">Before cover phase: <div><div></div></div></div></div> + <div class="anim contain"> + <div class="indicator entry">After entry phase: <div><div></div></div></div> + <div class="indicator exit">Before exit phase: <div><div></div></div></div> + </div> + <div class="spacer"></div> + </div> +</body> +<script> + async function run() { + await waitForCompositorReady(); + await waitForNextFrame(); + + let scroller = document.querySelector('.scroller'); + // Scroll such that each animation becomes active. + scroller.scrollTo({top: 0}); + await waitForNextFrame(); + scroller.scrollTo({top: 400}); + await waitForNextFrame(); + + // Then scroll between them so that we are before one and after the other. + scroller.scrollTo({top: 200}); + await waitForNextFrame(); + takeScreenshot(); + } +</script> +</html> 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..98a5d45c37 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/animation-range-ignored.html @@ -0,0 +1,235 @@ +<!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; + + 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. + await runAndWaitForFrameUpdate(() => { + anim.rangeEnd = 'contain 100%'; + target.style.animationRangeStart = 'entry 50%'; + }); + + // 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. + await runAndWaitForFrameUpdate(() => { + 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. + await runAndWaitForFrameUpdate(() => { + target.style.animationRangeStart = "entry 50%"; + }); + 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; + + 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. + await runAndWaitForFrameUpdate(() => { + anim.rangeStart = "entry 50%"; + target.style.animationRangeEnd = "contain 100%"; + }); + + 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. + await runAndWaitForFrameUpdate(() => { + 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. + await runAndWaitForFrameUpdate(() => { + target.style.animationRangeEnd = "cover 100%"; + }); + 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..a91e3d3e29 --- /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-range-visual-test-ref.html b/testing/web-platform/tests/scroll-animations/css/animation-range-visual-test-ref.html new file mode 100644 index 0000000000..7e584400f7 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/animation-range-visual-test-ref.html @@ -0,0 +1,79 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#named-timeline-range"> +<link rel="stylesheet" href="support/animation-range.css"> +<script src="/common/reftest-wait.js"></script> +<style> +.meter { + animation: active-interval linear 100s paused; + animation-timeline: auto; +} + +.bar { + animation: slide-in linear 100s paused; + animation-timeline: auto; +} +</style> +</head> +<body onload="test();"> +<h3>View timeline</h3> +<template id="meters"> + <div class="meters"> + <div class="cover"><div class="meter"><div class="bar"></div></div><div>Cover</div></div> + <div class="contain"><div class="meter"><div class="bar"></div></div><div>Contain</div></div> + <div class="entry"><div class="meter"><div class="bar"></div></div><div>Entry</div></div> + <div class="exit"><div class="meter"><div class="bar"></div></div><div>Exit</div></div> + </div> +</template> +<div class="flex"> + <div> + <div class="scroller" data-scroll-top="10"> + <div class="subject" data-progress=".08333,-1,.5,-1"></div> + <div class="spacer"></div> + </div> + </div> + <div> + <div class="scroller" data-scroll-top="30"> + <div class="subject" data-progress=".25,.125,2,-1"></div> + <div class="spacer"></div> + </div> + </div> + <div> + <div class="scroller" data-scroll-top="90"> + <div class="subject" data-progress=".75,.875,2,-1"></div> + <div class="spacer"></div> + </div> + </div> + <div> + <div class="scroller" data-scroll-top="110"> + <div class="subject" data-progress=".91667,2,2,.5"></div> + <div class="spacer"></div> + </div> + </div> +</div> +</body> +<script> + function test() { + let template = document.querySelector('#meters'); + let scrollers = document.querySelectorAll('.scroller'); + for (let i = 0; i < scrollers.length; i++) { + let subject = scrollers[i].querySelector('.subject'); + let clone = template.content.cloneNode(true); + let meters = clone.querySelectorAll('.meter'); + let progress = subject.getAttribute('data-progress').split(',').map(s => parseFloat(s)); + for (let meter of meters) { + let bar = meter.querySelector('.bar'); + let startTime = -progress.splice(0, 1)[0] * 100; + meter.style.animationDelay = `${startTime}s`; + bar.style.animationDelay = `${startTime}s`; + } + subject.appendChild(clone); + scrollers[i].scrollTop = parseInt(scrollers[i].getAttribute('data-scroll-top')); + } + takeScreenshot(); + } +</script> +</html> diff --git a/testing/web-platform/tests/scroll-animations/css/animation-range-visual-test.html b/testing/web-platform/tests/scroll-animations/css/animation-range-visual-test.html new file mode 100644 index 0000000000..1ff2b12d1f --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/animation-range-visual-test.html @@ -0,0 +1,62 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#named-timeline-range"> +<link rel="match" href="animation-range-visual-test-ref.html"> +<meta name=fuzzy content="maxDifference=0-64;totalPixels=0-15"> +<link rel="stylesheet" href="support/animation-range.css"> +<script src="/common/reftest-wait.js"></script> +<script src="/web-animations/testcommon.js"></script> +</head> +<body onload="test();"> +<h3>View timeline</h3> +<template id="meters"> + <div class="meters"> + <div class="cover"><div class="meter"><div class="bar"></div></div><div>Cover</div></div> + <div class="contain"><div class="meter"><div class="bar"></div></div><div>Contain</div></div> + <div class="entry"><div class="meter"><div class="bar"></div></div><div>Entry</div></div> + <div class="exit"><div class="meter"><div class="bar"></div></div><div>Exit</div></div> + </div> +</template> +<div class="flex"> + <div> + <div class="scroller" data-scroll-top="10"> + <div class="subject"></div> + <div class="spacer"></div> + </div> + </div> + <div> + <div class="scroller" data-scroll-top="30"> + <div class="subject"></div> + <div class="spacer"></div> + </div> + </div> + <div> + <div class="scroller" data-scroll-top="90"> + <div class="subject"></div> + <div class="spacer"></div> + </div> + </div> + <div> + <div class="scroller" data-scroll-top="110"> + <div class="subject"></div> + <div class="spacer"></div> + </div> + </div> +</div> +</body> +<script> + function test() { + let template = document.querySelector('#meters'); + let scrollers = document.querySelectorAll('.scroller'); + for (let i = 0; i < scrollers.length; i++) { + let subject = scrollers[i].querySelector('.subject'); + subject.appendChild(template.content.cloneNode(true)); + scrollers[i].scrollTop = parseInt(scrollers[i].getAttribute('data-scroll-top')); + } + waitForCompositorReady().then(takeScreenshot); + } +</script> +</html> 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..b7d5947a21 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/animation-shorthand.html @@ -0,0 +1,166 @@ +<!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', + 'animation-range-start': 'normal', + 'animation-range-end': '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'); + + target.style.animationTimeline = 'auto, auto'; + assert_equals(target.style.animation, ''); +}, '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'); + + target.style.animationTimeline = 'auto, auto'; + assert_equals(getComputedStyle(target).animation, ''); +}, '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'); + + target.style.animationDelayEnd = '0s, 0s'; + assert_equals(target.style.animation, ''); +}, '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'); + + target.style.animationDelayEnd = '0s, 0s'; + assert_equals(getComputedStyle(target).animation, ''); +}, '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'); + + target.style.animationRangeStart = 'normal, normal'; + assert_equals(target.style.animation, ''); +}, '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'); + + target.style.animationRangeStart = 'normal, normal'; + assert_equals(getComputedStyle(target).animation, ''); +}, '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'); + + target.style.animationRangeEnd = 'normal, normal'; + assert_equals(target.style.animation, ''); +}, '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'); + + target.style.animationRangeEnd = 'normal, normal'; + assert_equals(getComputedStyle(target).animation, ''); +}, '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..1e621eee53 --- /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'); + +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 | x | y +// <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(x)'); +test_computed_value('animation-timeline', 'scroll(y)'); +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(y root)', 'scroll(root y)'); + +// 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(x)', 'view(x)'); +test_computed_value('animation-timeline', 'view(y)', 'view(y)'); +test_computed_value('animation-timeline', 'view(y 1px)'); +test_computed_value('animation-timeline', 'view(1px auto)'); +test_computed_value('animation-timeline', 'view(auto 1px)'); +test_computed_value('animation-timeline', 'view(y 1px auto)'); +test_computed_value('animation-timeline', 'view(1px y)', 'view(y 1px)'); +test_computed_value('animation-timeline', 'view(y auto)', 'view(y)'); +test_computed_value('animation-timeline', 'view(y auto auto)', 'view(y)'); + +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/animation-timeline-deferred.html b/testing/web-platform/tests/scroll-animations/css/animation-timeline-deferred.html new file mode 100644 index 0000000000..d0671e5f23 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/animation-timeline-deferred.html @@ -0,0 +1,109 @@ +<!DOCTYPE html> +<title>Deferred timelines via Animation.timeline</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; } + to { width: 200px; } + } + .scroller { + overflow-y: hidden; + width: 200px; + height: 200px; + } + .scroller > .content { + margin: 400px 0px; + width: 100px; + height: 100px; + background-color: green; + } + .animating { + background-color: coral; + width: 0px; + animation: anim auto linear; + animation-timeline: --t1; + } + .timeline { + scroll-timeline-name: --t1; + } + .scope { + timeline-scope: --t1; + } +</style> + +<template id=animation_timeline_attached> + <div class="scope"> + <div class=animating>Test</div> + <div class="scroller timeline"> + <div class="content animating"></div> + </div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, animation_timeline_attached); + let scroller = main.querySelector('.scroller'); + let animating = Array.from(main.querySelectorAll('.animating')); + + assert_equals(animating.length, 2); + let animations = animating.map((e) => e.getAnimations()[0]); + assert_equals(animations.length, 2); + + // animations[0] is attached via deferred timeline (timeline-scope), + // and animations[1] is attached directly. + assert_equals(animations[0].timeline, animations[1].timeline); + }, 'Animation.timeline returns attached timeline'); +</script> + +<template id=animation_timeline_inactive> + <div class="scope"> + <div class=animating>Test</div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, animation_timeline_inactive); + let scroller = main.querySelector('.scroller'); + let animating = main.querySelector('.animating'); + + assert_equals(animating.getAnimations()[0].timeline, null); + }, 'Animation.timeline returns null for inactive deferred timeline'); +</script> + +<template id=animation_timeline_overattached> + <div class="scope"> + <div class=animating>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, animation_timeline_overattached); + let scroller = main.querySelector('.scroller'); + let animating = main.querySelector('.animating'); + + assert_equals(animating.getAnimations()[0].timeline, null); + }, 'Animation.timeline returns null for inactive (overattached) deferred timeline'); +</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..b9efbb428b --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/animation-timeline-ignored.tentative.html @@ -0,0 +1,153 @@ +<!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; + timeline-scope: --timeline1, --timeline2, --timeline3; + } + .scroller { + overflow: hidden; + width: 100px; + height: 100px; + } + .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> + 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 { + await runAndWaitForFrameUpdate(() => { + 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 () => { + let animation = element.getAnimations()[0]; + assert_equals(getComputedStyle(element).width, '120px'); + element.style = 'animation-timeline:--timeline2'; + await animation.ready; + + assert_equals(getComputedStyle(element).width, '140px'); + }, 'Changing animation-timeline changes the timeline (sanity check)'); + + test_animation_timeline(async () => { + let animation = element.getAnimations()[0]; + assert_equals(getComputedStyle(element).width, '120px'); + + // Set a (non-CSS) ScrollTimeline on the CSSAnimation. + let timeline4 = new ScrollTimeline({ source: scroller4 }); + + animation.timeline = timeline4; + await animation.ready; + assert_equals(getComputedStyle(element).width, '180px'); + + // Changing the animation-timeline property should have no effect. + element.style = 'animation-timeline:--timeline2'; + await animation.ready; + + assert_equals(getComputedStyle(element).width, '180px'); + }, 'animation-timeline ignored after setting timeline with JS ' + + '(ScrollTimeline from JS)'); + + test_animation_timeline(async () => { + let animation = element.getAnimations()[0]; + assert_equals(getComputedStyle(element).width, '120px'); + + let timeline1 = animation.timeline; + element.style = 'animation-timeline:--timeline2'; + await animation.ready; + assert_equals(getComputedStyle(element).width, '140px'); + + animation.timeline = timeline1; + await animation.ready; + + assert_equals(getComputedStyle(element).width, '120px'); + + // Should have no effect. + element.style = 'animation-timeline:--timeline3'; + await animation.ready; + + assert_equals(getComputedStyle(element).width, '120px'); + }, 'animation-timeline ignored after setting timeline with JS ' + + '(ScrollTimeline from CSS)'); + + test_animation_timeline(async () => { + let animation = element.getAnimations()[0]; + assert_equals(getComputedStyle(element).width, '120px'); + animation.timeline = document.timeline; + await animation.ready; + + // (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'; + await animation.ready; + assert_equals(getComputedStyle(element).width, '120px'); + }, 'animation-timeline ignored after setting timeline with JS (document timeline)'); + + test_animation_timeline(async () => { + let animation = element.getAnimations()[0]; + assert_equals(getComputedStyle(element).width, '120px'); + animation.timeline = null; + assert_false(animation.pending); + assert_equals(getComputedStyle(element).width, '120px'); + + // Changing the animation-timeline property should have no effect. + element.style = 'animation-timeline:--timeline2'; + assert_false(animation.pending); + 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..04b430c324 --- /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..3196653656 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/animation-timeline-multiple.html @@ -0,0 +1,91 @@ +<!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 { + timeline-scope: --top_timeline, --bottom_timeline, --left_timeline, --right_timeline; + } + + .scroller { + overflow: hidden; + width: 100px; + height: 100px; + } + .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> + promise_test(async (t) => { + await runAndWaitForFrameUpdate(() => { + 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; + }); + 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..5a1f26b3f3 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/animation-timeline-named-scroll-progress-timeline.tentative.html @@ -0,0 +1,444 @@ +<!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'; + + await runAndWaitForFrameUpdate(() => { + // <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% + }); + + 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'); + + await runAndWaitForFrameUpdate(() => { + // <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% + }); + + 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); + + await runAndWaitForFrameUpdate(() => { + // <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% + }); + + 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'; + + await runAndWaitForFrameUpdate(() => { + // <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'; + }); + + 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); + + await runAndWaitForFrameUpdate(() => { + // <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'; + }); + + 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); + + await runAndWaitForFrameUpdate(() => { + // <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'; + }); + + 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'; + + await runAndWaitForFrameUpdate(() => { + // <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% + }); + + 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); + + await runAndWaitForFrameUpdate(() => { + // <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'; + }); + + // 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 runAndWaitForFrameUpdate(() => { + // <div id='wrapper' class="scroller"> ... + // <div id='target'></div> + // </div> + wrapper.classList.add('scroller'); + wrapper.style.scrollTimelineName = '--timeline'; + wrapper.scrollTop = 50; // 25% + }); + + // 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'; + + await runAndWaitForFrameUpdate(() => { + // <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'; + + await runAndWaitForFrameUpdate(() => { + // <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'; + + await runAndWaitForFrameUpdate(() => { + // <div id='scroller' class='scroller'> ... + // <div id='target'></div> + // </div> + document.body.appendChild(scroller); + scroller.appendChild(target); + + scroller.style.scrollTimeline = '--timeline x'; + 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 x'); + +promise_test(async t => { + let [scroller, target] = createScrollerAndTarget(t); + scroller.style.writingMode = 'vertical-lr'; + + await runAndWaitForFrameUpdate(() => { + // <div id='scroller' class='scroller'> ... + // <div id='target'></div> + // </div> + document.body.appendChild(scroller); + scroller.appendChild(target); + + scroller.style.scrollTimeline = '--timeline y'; + 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 y'); + +promise_test(async t => { + let [scroller, target] = createScrollerAndTarget(t); + + await runAndWaitForFrameUpdate(() => { + // <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..7092523c48 --- /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..9e3c1078b5 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/animation-timeline-parsing.html @@ -0,0 +1,88 @@ +<!DOCTYPE html> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#animation-timeline"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/css/support/parsing-testcommon.js"></script> +</head> +<div id="target"></div> +<script> +test_valid_value('animation-timeline', 'initial'); +test_valid_value('animation-timeline', 'inherit'); +test_valid_value('animation-timeline', 'unset'); +test_valid_value('animation-timeline', 'revert'); +test_valid_value('animation-timeline', 'auto'); +test_valid_value('animation-timeline', 'none'); +test_valid_value('animation-timeline', 'auto, auto'); +test_valid_value('animation-timeline', 'none, none'); +test_valid_value('animation-timeline', 'auto, none'); +test_valid_value('animation-timeline', 'none, auto'); +test_valid_value('animation-timeline', '--test'); +test_valid_value('animation-timeline', '--test1, --test2'); +test_valid_value('animation-timeline', '--test1, --test2, none, --test3, auto'); + +test_invalid_value('animation-timeline', 'test1'); +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 | x | y +// <scroller> = root | nearest | self +test_valid_value('animation-timeline', 'scroll()'); +test_valid_value('animation-timeline', ' scroll() ', 'scroll()'); +test_valid_value('animation-timeline', 'scroll(block)', 'scroll()'); +test_valid_value('animation-timeline', 'scroll(inline)'); +test_valid_value('animation-timeline', 'scroll(x)'); +test_valid_value('animation-timeline', 'scroll(y)'); +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(y root)', 'scroll(root y)'); + +test_invalid_value('animation-timeline', 'scroll(abc root)'); +test_invalid_value('animation-timeline', 'scroll(abc)'); +test_invalid_value('animation-timeline', 'scroll(y 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() ', 'view()'); +test_valid_value('animation-timeline', 'view(block)', 'view()'); +test_valid_value('animation-timeline', 'view(inline)'); +test_valid_value('animation-timeline', 'view(x)'); +test_valid_value('animation-timeline', 'view(y)'); +test_valid_value('animation-timeline', 'view(y 1px 2px)'); +test_valid_value('animation-timeline', 'view(y 1px)'); +test_valid_value('animation-timeline', 'view(y auto)', 'view(y)'); +test_valid_value('animation-timeline', 'view(y auto auto)', 'view(y)'); +test_valid_value('animation-timeline', 'view(y auto 1px)'); +test_valid_value('animation-timeline', 'view(1px 2px y)', 'view(y 1px 2px)'); +test_valid_value('animation-timeline', 'view(1px y)', 'view(y 1px)'); +test_valid_value('animation-timeline', 'view(auto x)', 'view(x)'); +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(y 1px 2px 3px)'); +test_invalid_value('animation-timeline', 'view(1px y 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(y 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..31c85810d5 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/animation-timeline-scroll-functional-notation.tentative.html @@ -0,0 +1,177 @@ +<!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'); + await runAndWaitForFrameUpdate(() => { + 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'); + await runAndWaitForFrameUpdate(() => { + 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'); + await runAndWaitForFrameUpdate(() => { + 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'); + await runAndWaitForFrameUpdate(() => { + 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'); + await runAndWaitForFrameUpdate(() => { + 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'); + await runAndWaitForFrameUpdate(() => { + container.style.writingMode = 'vertical-lr'; + div.style.animation = "anim 10s linear"; + div.style.animationTimeline = "scroll(x)"; + }); + + await scrollLeft(container, 50); + assert_equals(getComputedStyle(div).translate, '100px'); +}, 'animation-timeline: scroll(x)'); + +promise_test(async t => { + let [container, div] = createTargetWithStuff(t, 'inline-content'); + await runAndWaitForFrameUpdate(() => { + container.style.writingMode = 'vertical-lr'; + div.style.animation = "anim 10s linear"; + div.style.animationTimeline = "scroll(y)"; + }); + + await scrollTop(container, 50); + assert_equals(getComputedStyle(div).translate, '100px'); +}, 'animation-timeline: scroll(y)'); + +// 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..d91dfe924b --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/animation-timeline-view-functional-notation.tentative.html @@ -0,0 +1,511 @@ +<!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://drafts.csswg.org/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']); + await runAndWaitForFrameUpdate(() => { + container.style.overflow = 'hidden'; + div.style.animation = "fade-in-out-without-timeline-range 1s linear forwards"; + 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']); + await runAndWaitForFrameUpdate(() => { + container.style.overflow = 'hidden'; + div.style.animation = "fade-in-out-without-timeline-range 1s linear forwards"; + 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']); + await runAndWaitForFrameUpdate(() => { + container.style.overflow = 'hidden'; + div.style.animation = "fade-in-out-without-timeline-range 1s linear forwards"; + 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']); + await runAndWaitForFrameUpdate(() => { + container.style.overflow = 'hidden'; + div.style.animation = "fade-out-without-timeline-range 1s linear forwards"; + 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']); + await runAndWaitForFrameUpdate(() => { + container.style.overflow = 'hidden'; + div.style.animation = "fade-out-without-timeline-range 1s linear forwards"; + div.style.animationTimeline = "view(x)"; + }); + // 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(x) without timeline range name'); + +promise_test(async t => { + let [container, div] = createTargetWithStuff(t, ['target', 'content']); + await runAndWaitForFrameUpdate(() => { + div.style.animation = "fade-out-without-timeline-range 1s linear forwards"; + div.style.animationTimeline = "view(y)"; + }); + // 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(y) without timeline range name'); + +promise_test(async t => { + let [container, div] = createTargetWithStuff(t, ['target', 'content']); + await runAndWaitForFrameUpdate(() => { + container.style.overflow = 'hidden'; + div.style.animation = "fade-out-without-timeline-range 1s linear forwards"; + div.style.animationTimeline = "view(x 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(x 50px) without timeline range name'); + +promise_test(async t => { + let [container, div] = createTargetWithStuff(t, ['target', 'content']); + await runAndWaitForFrameUpdate(() => { + container.style.overflow = 'hidden'; + div.style.animation + = "fade-out-without-timeline-range 1s linear forwards, " + + "change-font-size-without-timeline-range 1s linear forwards"; + 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']); + await runAndWaitForFrameUpdate(() => { + container.style.overflow = 'hidden'; + div.style.animation = "fade-out-without-timeline-range 1s linear forwards"; + 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']); + await runAndWaitForFrameUpdate(() => { + div.style.animation = "fade-in-out 1s linear forwards"; + 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']); + await runAndWaitForFrameUpdate(() => { + div.style.animation = "fade-in-out 1s linear forwards"; + 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']); + await runAndWaitForFrameUpdate(() => { + div.style.animation = "fade-in-out 1s linear forwards"; + 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']); + await runAndWaitForFrameUpdate(() => { + container.style.overflow = 'scroll'; + div.style.animation = "fade-out 1s linear forwards"; + 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']); + await runAndWaitForFrameUpdate(() => { + container.style.overflow = 'scroll'; + div.style.animation = "fade-out 1s linear forwards"; + div.style.animationTimeline = "view(x)"; + }); + + 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(x)'); + +promise_test(async t => { + let [container, div] = createTargetWithStuff(t, ['target', 'content']); + await runAndWaitForFrameUpdate(() => { + container.style.overflow = 'scroll'; + div.style.animation = "fade-out 1s linear forwards"; + div.style.animationTimeline = "view(y)"; + }); + + 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(y)'); + +promise_test(async t => { + let [container, div] = createTargetWithStuff(t, ['target', 'content']); + await runAndWaitForFrameUpdate(() => { + container.style.overflowY = 'hidden'; + container.style.overflowX = 'scroll'; + div.style.animation = "fade-out 1s linear forwards"; + div.style.animationTimeline = "view(x 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(x 50px)'); + +promise_test(async t => { + let [container, div] = createTargetWithStuff(t, ['target', 'content']); + await runAndWaitForFrameUpdate(() => { + container.style.overflow = 'scroll'; + div.style.animation + = "fade-out 1s linear forwards, change-font-size 1s linear forwards"; + 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']); + await runAndWaitForFrameUpdate(() => { + container.style.overflowY = 'hidden'; + container.style.overflowX = 'scroll'; + div.style.animation = "fade-out 1s linear forwards"; + }); + + 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/deferred-timeline-composited-ref.html b/testing/web-platform/tests/scroll-animations/css/deferred-timeline-composited-ref.html new file mode 100644 index 0000000000..088e93750a --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/deferred-timeline-composited-ref.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<style> + #target { + translate: 50px; + width: 50px; + height: 50px; + background-color: green; + } +</style> +<div id=target></div> diff --git a/testing/web-platform/tests/scroll-animations/css/deferred-timeline-composited.html b/testing/web-platform/tests/scroll-animations/css/deferred-timeline-composited.html new file mode 100644 index 0000000000..e2437911b3 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/deferred-timeline-composited.html @@ -0,0 +1,78 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<head> + <link rel="help" src="https://github.com/w3c/csswg-drafts/issues/7759"> + <link rel="match" href="deferred-timeline-composited-ref.html"> + <script src="/web-animations/testcommon.js"></script> + <script src="/common/reftest-wait.js"></script> + <style> + @keyframes anim { + from { translate: 0px; } + to { translate: 100px; } + } + main { + timeline-scope: --t1; + } + .scroller { + width: 100px; + height: 100px; + will-change: translate; + background-color: white; + /* Prevent scrollers from appearing in the screenshot. */ + opacity: 0; + } + .scroller > div { + height: 300px; + width: 300px; + } + #target { + animation: anim auto linear; + animation-timeline: --t1; + width: 50px; + height: 50px; + will-change: translate; + background-color: green; + } + .timeline { + scroll-timeline-name: --t1; + } + #scroller_block { + overflow-y: scroll; + overflow-x: hidden; + scroll-timeline-axis: block; + } + #scroller_inline { + overflow-y: hidden; + overflow-x: scroll; + scroll-timeline-axis: inline; + } + </style> +</head> +<body> + <main> + <div id=target></div> + <div id=scroller_block class="scroller timeline"> + <div></div> + </div> + <div id=scroller_inline class=scroller> + <div></div> + </div> + </main> + <script> + (async () => { + await waitForCompositorReady(); + // Switch out the timeline associated with timeline-scope:--t. + scroller_block.classList.toggle('timeline'); + scroller_inline.classList.toggle('timeline'); + await waitForNextFrame(); + let scrollPromise = new Promise((resolve) => { + scroller_inline.addEventListener('scrollend', resolve); + }); + scroller_inline.scrollTo({left: 100, behavior: "smooth"}); // 50% + await scrollPromise; + await waitForNextFrame(); + takeScreenshot(); + })(); + </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..83bc5b5f53 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/get-animations-inactive-timeline.html @@ -0,0 +1,84 @@ +<!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 => { + 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'); + + // 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..e5d5037d62 --- /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..c37c1b95ef --- /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..dd4add49b0 --- /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..25ce167553 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/progress-based-animation-animation-longhand-properties.tentative.html @@ -0,0 +1,277 @@ +<!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); + await runAndWaitForFrameUpdate(() => { + 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); + await runAndWaitForFrameUpdate(() => { + 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); + await runAndWaitForFrameUpdate(() => { + 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); + await runAndWaitForFrameUpdate(() => { + 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); + await runAndWaitForFrameUpdate(() => { + 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); + await runAndWaitForFrameUpdate(() => { + 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); + await runAndWaitForFrameUpdate(() => { + 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); + await runAndWaitForFrameUpdate(() => { + 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); + await runAndWaitForFrameUpdate(() => { + 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); + await runAndWaitForFrameUpdate(() => { + 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); + await runAndWaitForFrameUpdate(() => { + 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..bbc60e3fbd --- /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/pseudo-on-scroller.html b/testing/web-platform/tests/scroll-animations/css/pseudo-on-scroller.html new file mode 100644 index 0000000000..8dd49ce4d8 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/pseudo-on-scroller.html @@ -0,0 +1,55 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <title>Animating pseduo-element on scroller</title> +</head> +<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 type="text/css"> +.scroller { + overflow: auto; + width: 100px; + height: 100px; + margin: 1em; + outline: 1px solid; + animation: bg linear; + animation-timeline: scroll(self inline); +} +.pseudo::before { + content: ""; + display: block; + width: 200px; + height: 50px; + background: red; + animation: bg linear reverse; + animation-timeline: scroll(inline); +} +@keyframes bg { + from { + background: rgb(0, 255, 0); + } + to { + background: rgb(0, 0, 255); + } +} +</style> +<body> + <div class="scroller pseudo"></div> + <div id="log"></div> +</body> +<script type="text/javascript"> + 'use strict'; + + promise_test(async t => { + const scroller = document.querySelector('.scroller'); + await waitForNextFrame(); + assert_equals(getComputedStyle(scroller).backgroundColor, 'rgb(0, 255, 0)'); + assert_equals(getComputedStyle(scroller, ':before').backgroundColor, + 'rgb(0, 0, 255)'); + }, `scroll nearest on pseudo-element attaches to parent scroll container`); +</script> +</html> 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-axis-computed.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-axis-computed.html new file mode 100644 index 0000000000..c942fb4093 --- /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: y; } +</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', 'y'); +test_computed_value('scroll-timeline-axis', 'x'); +test_computed_value('scroll-timeline-axis', 'block, inline'); +test_computed_value('scroll-timeline-axis', 'inline, block'); +test_computed_value('scroll-timeline-axis', 'block, y, x, 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..a9a760a54a --- /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', 'y'); +test_valid_value('scroll-timeline-axis', 'x'); +test_valid_value('scroll-timeline-axis', 'block, inline'); +test_valid_value('scroll-timeline-axis', 'inline, block'); +test_valid_value('scroll-timeline-axis', 'block, y, x, 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..cb9a98dcd8 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-axis-writing-mode.html @@ -0,0 +1,143 @@ +<!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_y { + scroll-timeline: --timeline_y y; + } + #timeline_x { + scroll-timeline: --timeline_x x; + } + #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_y { animation-timeline: --timeline_y; } + #element_x { animation-timeline: --timeline_x; } + #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_y> + <div class=contents></div> + <div class=target id=element_y></div> +</div> +<div class=scroller id=timeline_x> + <div class=contents></div> + <div class=target id=element_x></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> + async function setScrollPositions() { + return runAndWaitForFrameUpdate(() => { + // Animations linked to vertical scroll-timelines are at 75% progress. + timeline_initial_axis.scrollTop = 75; + timeline_y.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_x.scrollLeft = 25; + timeline_block_in_vertical.scrollLeft = 25; + timeline_inline_in_horizontal.scrollLeft = 25; + }); + } + + promise_test(async (t) => { + await setScrollPositions(); + assert_equals(getComputedStyle(element_initial_axis).width, '175px'); + }, 'Initial axis'); + + promise_test(async (t) => { + await setScrollPositions(); + assert_equals(getComputedStyle(element_y).width, '175px'); + }, 'Vertical axis'); + + promise_test(async (t) => { + await setScrollPositions(); + assert_equals(getComputedStyle(element_x).width, '125px'); + }, 'Horizontal axis'); + + promise_test(async (t) => { + await setScrollPositions(); + assert_equals(getComputedStyle(element_block_in_horizontal).width, '175px'); + }, 'Block axis in horizontal writing-mode'); + + promise_test(async (t) => { + await setScrollPositions(); + assert_equals(getComputedStyle(element_inline_in_horizontal).width, '125px'); + }, 'Inline axis in horizontal writing-mode'); + + promise_test(async (t) => { + await setScrollPositions(); + 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 setScrollPositions(); + 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..d1f143c7c1 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-dynamic.tentative.html @@ -0,0 +1,267 @@ +<!DOCTYPE html> +<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1"> +<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#scroll-timelines"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="support/testcommon.js"></script> +<style> + main { + timeline-scope: --timeline; + } + + 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 the computed style after scrolling a bit. + instantiate(async (element, expected) => { + await waitForNextFrame(); + 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.timelineScope = "--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..742c35b258 --- /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..eedc8e3958 --- /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..403316ead0 --- /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; + timeline-scope: --timeline1, --timeline2; + } + .scroller { + height: 100px; + overflow: scroll; + } + .scroller > div { + height: 200px; + } + #element1 { + width: 1px; + animation: expand_width 10s; + animation-timeline: --timeline1; + } + #element2 { + height: 1px; + animation: expand_height 10s; + animation-timeline: --timeline2; + } +</style> +<main id=main> + <div id=element1></div> + <div> + <div id=element2></div> + </div> +</main> +<script> + setup(assert_implements_animation_timeline); + + function insertScroller(timeline_name) { + let scroller = document.createElement('div'); + scroller.classList.add('scroller'); + scroller.style.scrollTimeline = timeline_name; + scroller.append(document.createElement('div')); + main.insertBefore(scroller, element1); + } + + promise_test(async () => { + await waitForNextFrame(); + + let events1 = []; + let events2 = []; + + insertScroller('--timeline1'); + // Even though the scroller was just inserted into the DOM, |timeline1| + // remains inactive until the next frame. + // + // https://drafts.csswg.org/scroll-animations-1/#avoiding-cycles + assert_equals(getComputedStyle(element1).width, '1px'); + (new ResizeObserver(entries => { + events1.push(entries); + insertScroller('--timeline2'); + assert_equals(getComputedStyle(element2).height, '1px'); + })).observe(element1); + + (new ResizeObserver(entries => { + events2.push(entries); + })).observe(element2); + + await waitForNextFrame(); + + // According to the basic rules of the spec [1], the timeline is + // inactive at the time the resize observer event was delivered, because + // #scroller1 did not have a layout box at the time style recalc for + // #element1 happened. + // + // However, an additional style/layout pass should take place + // (before resize observer deliveries) if we detect new ScrollTimelines + // in this situation, hence we ultimately do expect the animation to + // apply [2]. + // + // [1] https://drafts.csswg.org/scroll-animations-1/#avoiding-cycles + // [2] https://github.com/w3c/csswg-drafts/issues/5261 + assert_equals(events1.length, 1); + assert_equals(events1[0].length, 1); + assert_equals(events1[0][0].contentBoxSize.length, 1); + assert_equals(events1[0][0].contentBoxSize[0].inlineSize, 100); + + // ScrollTimelines created during the ResizeObserver should remain + // inactive during the frame they're created, so the ResizeObserver + // event should not reflect the animated value. + assert_equals(events2.length, 1); + assert_equals(events2[0].length, 1); + assert_equals(events2[0][0].contentBoxSize.length, 1); + assert_equals(events2[0][0].contentBoxSize[0].blockSize, 1); + + assert_equals(getComputedStyle(element1).width, '100px'); + assert_equals(getComputedStyle(element2).height, '100px'); + }, 'Multiple style/layout passes occur when necessary'); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/scroll-timeline-name-computed.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-name-computed.html new file mode 100644 index 0000000000..b803ee8212 --- /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', '--foo'); +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..d38b9640af --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-name-parsing.html @@ -0,0 +1,32 @@ +<!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_invalid_value('scroll-timeline-name', 'auto'); +test_invalid_value('scroll-timeline-name', 'abc'); +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..a535b2a44f --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-name-shadow.html @@ -0,0 +1,180 @@ +<!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> + +<main id=main></main> +<script> + function inflate(t, template) { + t.add_cleanup(() => main.replaceChildren()); + main.append(template.content.cloneNode(true)); + main.offsetTop; + } +</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 x; + } + </style> + <div class=scroller> + <div class=scroller> + <template shadowrootmode=open shadowrootclonable> + <style> + :host { + scroll-timeline: --timeline y; + } + </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, 'y'); + }, '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 x; + } + </style> + <div class=host> + <template shadowrootmode=open shadowrootclonable> + <style> + ::slotted(.scroller) { + scroll-timeline: --timeline y; + } + </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, 'y'); + }, 'Outer animation can see scroll timeline defined by ::slotted'); +</script> + + +<template id=scroll_timeline_part> + <style> + .host { + scroll-timeline: --timeline y; + } + .host::part(foo) { + scroll-timeline: --timeline x; + } + </style> + <div class=host> + <template shadowrootmode=open shadowrootclonable> + <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, 'x'); + }, '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 x; + } + </style> + <div class=scroller> + <div class=host> + <template shadowrootmode=open shadowrootclonable> + <style> + div { + scroll-timeline: --timeline y; + } + </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, 'y'); + }, '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..57a1a94712 --- /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-range-animation.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-range-animation.html new file mode 100644 index 0000000000..df087da6e2 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-range-animation.html @@ -0,0 +1,182 @@ +<!DOCTYPE html> +<title>Scroll timelines and animation attachment ranges</title> +<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#named-timeline-range"> +<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#animation-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; + } + #scroller > div { + margin: 800px 0px; + width: 100px; + height: 100px; + } + #target { + font-size: 10px; + background-color: green; + z-index: -1; + } +</style> +<main id=main> +</main> + +<template id=template_without_scope> + <div id=scroller class=timeline> + <div id=target></div> + </div> +</template> + +<template id=template_with_scope> + <div id=scope> + <div id=target></div> + <div id=scroller class=timeline> + <div></div> + </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.ready)); + } + 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_range(options, template, desc_suffix) { + if (template === undefined) + template = template_without_scope; + if (desc_suffix === undefined) + desc_suffix = ''; + + promise_test(async (t) => { + inflate(t, template); + let scroller = main.querySelector('#scroller'); + let target = main.querySelector('#target'); + let timeline = main.querySelector('.timeline'); + let scope = main.querySelector('#scope'); + let maxScroll = scroller.scrollHeight - scroller.clientHeight; + + if (scope != null) { + scope.style.timelineScope = '--t1'; + } + + timeline.style.scrollTimeline = '--t1'; + 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'; + let before_scroll = options.startOffset - 10; + if (before_scroll >= 0) { + await assertValueAt(scroller, target, + { scrollTop: options.startOffset - 10, expected: -1 }); + } + let after_scroll = options.startOffset + 10; + if (after_scroll <= scroller.maxmum) { + 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}] ${desc_suffix}`.trim()); + } + + test_animation_range({ + rangeStart: 'initial', + rangeEnd: 'initial', + startOffset: 0, + endOffset: 1500 + }); + + test_animation_range({ + rangeStart: '0%', + rangeEnd: '100%', + startOffset: 0, + endOffset: 1500 + }); + + test_animation_range({ + rangeStart: '10%', + rangeEnd: '100%', + startOffset: 150, + endOffset: 1500 + }); + + test_animation_range({ + rangeStart: '0%', + rangeEnd: '50%', + startOffset: 0, + endOffset: 750 + }); + + test_animation_range({ + rangeStart: '10%', + rangeEnd: '50%', + startOffset: 150, + endOffset: 750 + }); + + test_animation_range({ + rangeStart: '150px', + rangeEnd: '75em', + startOffset: 150, + endOffset: 750 + }); + + test_animation_range({ + rangeStart: 'calc(1% + 135px)', + rangeEnd: 'calc(70em + 50px)', + startOffset: 150, + endOffset: 750 + }); + + // Test animation-range via timeline-scope. + test_animation_range({ + rangeStart: 'calc(1% + 135px)', + rangeEnd: 'calc(70em + 50px)', + startOffset: 150, + endOffset: 750 + }, template_with_scope, '(scoped)'); + +</script> 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..a67f3b94e4 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-sampling.html @@ -0,0 +1,52 @@ +<!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; + } + #element.animate { + 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) => { + assert_equals(getComputedStyle(element).width, '0px'); + await runAndWaitForFrameUpdate(() => { + element.classList.add('animate'); + }); + assert_equals(getComputedStyle(element).width, '100px'); + + 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.html b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-shorthand.html new file mode 100644 index 0000000000..722a8a1f4d --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/scroll-timeline-shorthand.html @@ -0,0 +1,109 @@ +<!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 x'); +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', '--y block', '--y'); +test_valid_value('scroll-timeline', '--x block', '--x'); + +test_valid_value('scroll-timeline', '--a, --b, --c'); +test_valid_value('scroll-timeline', '--a inline, --b block, --c y', '--a inline, --b, --c y'); +test_valid_value('scroll-timeline', '--auto'); + +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 y'); +test_computed_value('scroll-timeline', '--abc x'); +test_computed_value('scroll-timeline', '--y y'); +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', '--y block', '--y'); +test_computed_value('scroll-timeline', '--x block', '--x'); +test_computed_value('scroll-timeline', '--a, --b, --c'); +test_computed_value('scroll-timeline', '--a inline, --b block, --c y', '--a inline, --b, --c y'); + +test_shorthand_value('scroll-timeline', '--abc y', +{ + 'scroll-timeline-name': '--abc', + 'scroll-timeline-axis': 'y', +}); +test_shorthand_value('scroll-timeline', '--inline x', +{ + 'scroll-timeline-name': '--inline', + 'scroll-timeline-axis': 'x', +}); +test_shorthand_value('scroll-timeline', '--abc y, --def', +{ + 'scroll-timeline-name': '--abc, --def', + 'scroll-timeline-axis': 'y, block', +}); +test_shorthand_value('scroll-timeline', '--abc, --def', +{ + 'scroll-timeline-name': '--abc, --def', + 'scroll-timeline-axis': 'block, block', +}); + +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', +}, '--abc inline'); + +test_shorthand_contraction('scroll-timeline', { + 'scroll-timeline-name': '--a, --b', + 'scroll-timeline-axis': 'inline, block', +}, '--a inline, --b'); + +test_shorthand_contraction('scroll-timeline', { + 'scroll-timeline-name': 'none, none', + 'scroll-timeline-axis': 'block, block', +}, 'none, none'); + +// Longhands with different lengths: + +test_shorthand_contraction('scroll-timeline', { + 'scroll-timeline-name': '--a, --b, --c', + 'scroll-timeline-axis': 'inline, inline', +}, '--a inline, --b inline, --c inline'); + +test_shorthand_contraction('scroll-timeline', { + 'scroll-timeline-name': '--a, --b', + 'scroll-timeline-axis': 'inline, inline, inline', +}, '--a inline, --b inline'); +</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..6bc18544f4 --- /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/animation-range.css b/testing/web-platform/tests/scroll-animations/css/support/animation-range.css new file mode 100644 index 0000000000..1ebd0b429b --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/support/animation-range.css @@ -0,0 +1,82 @@ +.flex { + display: flex; +} + +.flex > div { + position: relative; + height: 160px; + margin: 0 10px; +} + +.scroller { + width: 100px; + height: 100px; + overflow: auto; + border: 1px solid black; +} + +.subject { + view-timeline-name: --view; + width: 20px; + height: 20px; + margin: 100px auto; + background: green; +} + +.meters { + position: absolute; + left: 0; + top: 110px; + height: 50px; +} + +.meters > div { + display: flex; + align-items: center; +} + +@keyframes active-interval { + 0% { opacity: 1; } + 100% { opacity: 1; } +} + +.meter { + width: 50px; + position: relative; + border: 2px solid black; + height: 5px; + overflow: clip; + opacity: 0.4; + animation: active-interval linear; + animation-timeline: --view; +} + +@keyframes slide-in { + 0% { transform: translateX(-100%)} + 100% { transform: translateX(0%)} +} + +.bar { + width: 100%; + height: 100%; + background: blue; + transform: translateX(-100%); + animation: slide-in linear; + animation-timeline: --view; +} + +.spacer { + height: 400px; +} + +.contain .bar, .contain .meter { + animation-range: contain; +} + +.entry .bar, .entry .meter { + animation-range: entry; +} + +.exit .bar, .exit .meter { + animation-range: exit; +} 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..91540774d0 --- /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..5a70820b88 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/timeline-offset-in-keyframe-change-timeline.tentative.html @@ -0,0 +1,147 @@ +<!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; + timeline-scope: --sibling; + } + #sibling { + margin-top: 800px; + margin-left: 10px; + margin-right: 10px; + width: 100px; + height: 50px; + background-color: blue; + view-timeline: --sibling block; + } + #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..011b8d4319 --- /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; + timeline-scope: --t1; + } + #block { + margin-top: 800px; + margin-left: 10px; + margin-right: 10px; + width: 100px; + height: 50px; + background-color: blue; + view-timeline: --t1; + } + #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..6fab0025da --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/timeline-range-name-offset-in-keyframes.tentative.html @@ -0,0 +1,111 @@ +<!DOCTYPE html> +<html> +<meta charset="utf-8"> +<title>Timeline offset in Animation Keyframes</title> +<link rel="help" href="https://drafts.csswg.org/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 => { + await waitForNextFrame(); + + // 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/timeline-scope.html b/testing/web-platform/tests/scroll-animations/css/timeline-scope.html new file mode 100644 index 0000000000..e4e90bc03a --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/timeline-scope.html @@ -0,0 +1,322 @@ +<!DOCTYPE html> +<title>Behavior of the timeline-scope property</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> + async function inflate(t, template) { + t.add_cleanup(() => main.replaceChildren()); + return runAndWaitForFrameUpdate(() => { + main.append(template.content.cloneNode(true)); + }); + } + + async function scrollTop(e, value) { + e.scrollTop = value; + await waitForNextFrame(); + } +</script> +<style> + @keyframes anim { + from { width: 0px; } + to { width: 200px; } + } + + .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; + } + .scope { + timeline-scope: --t1; + } + +</style> + +<!-- Basic Behavior --> + +<template id=deferred_timeline> + <div class="scope"> + <div class=target>Test</div> + <div class="scroller timeline"> + <div class=content></div> + </div> + </div> +</template> +<script> + promise_test(async (t) => { + await inflate(t, deferred_timeline); + let scroller = main.querySelector('.scroller'); + let target = main.querySelector('.target'); + + const anim = target.getAnimations()[0]; + await anim.ready; + + await scrollTop(scroller, 350); // 50% + assert_equals(getComputedStyle(target).width, '100px'); // 0px => 200px, 50% + }, 'Descendant can attach to deferred timeline'); +</script> + +<template id=deferred_timeline_no_attachments> + <div class="scope"> + <div class=target>Test</div> + <div class="scroller"> + <div class=content></div> + </div> + </div> +</template> +<script> + promise_test(async (t) => { + await inflate(t, deferred_timeline_no_attachments); + let scroller = main.querySelector('.scroller'); + let target = main.querySelector('.target'); + await scrollTop(scroller, 350); // 50% + assert_equals(getComputedStyle(target).width, '0px'); + }, 'Deferred timeline with no attachments'); +</script> + +<template id=scroll_timeline_inner_interference> + <div class="scroller timeline"> + <div class=content> + <div class=target>Test</div> + <div class="scroller timeline"> + <div class=content></div> + </div> + </div> + </div> +</template> +<script> + promise_test(async (t) => { + await inflate(t, scroll_timeline_inner_interference); + let scroller = main.querySelector('.scroller'); + let target = main.querySelector('.target'); + await scrollTop(scroller, 350); // 50% + assert_equals(getComputedStyle(target).width, '100px'); // 0px => 200px, 50% + }, 'Inner timeline does not interfere with outer timeline'); +</script> + +<template id=deferred_timeline_two_attachments> + <div class="scope"> + <div class=target>Test</div> + <div class="scroller timeline"> + <div class=content></div> + </div> + <!-- Extra attachment --> + <div class="timeline"></div> + </div> +</template> +<script> + promise_test(async (t) => { + await inflate(t, deferred_timeline_two_attachments); + let scroller = main.querySelector('.scroller'); + let target = main.querySelector('.target'); + await scrollTop(scroller, 350); // 50% + assert_equals(getComputedStyle(target).width, '0px'); + }, 'Deferred timeline with two attachments'); +</script> + +<!-- Dynamic Reattachment --> + +<template id=deferred_timeline_reattach> + <div class="scope"> + <div class=target>Test</div> + <div class="scroller timeline"> + <div class=content></div> + </div> + <div class="scroller"> + <div class=content></div> + </div> + </div> +</template> +<script> + promise_test(async (t) => { + await inflate(t, deferred_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]. + await runAndWaitForFrameUpdate(() => { + scrollers[0].classList.remove('timeline'); + scrollers[1].classList.add('timeline'); + }); + + assert_equals(getComputedStyle(target).width, '50px'); // 0px => 200px, 25% + }, 'Dynamically re-attaching'); +</script> + +<template id=deferred_timeline_dynamic_detach> + <div class="scope"> + <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) => { + await inflate(t, deferred_timeline_dynamic_detach); + 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'); + + // Detach scrollers[1]. + await runAndWaitForFrameUpdate(() => { + scrollers[1].classList.remove('timeline'); + }); + + assert_equals(getComputedStyle(target).width, '100px'); // 0px => 200px, 50% + + // Also detach scrollers[0]. + scrollers[0].classList.remove('timeline'); + + await waitForNextFrame(); + assert_equals(getComputedStyle(target).width, '0px'); + }, 'Dynamically detaching'); +</script> + +<template id=deferred_timeline_attached_removed> + <div class="scope"> + <div class=target>Test</div> + <div class="scroller timeline"> + <div class=content></div> + </div> + </div> +</template> +<script> + promise_test(async (t) => { + await inflate(t, deferred_timeline_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'); + + scroller_parent.append(scroller); + await scrollTop(scroller, 350); // 50% + assert_equals(getComputedStyle(target).width, '100px'); // 0px => 200px, 50% + }, 'Removing/inserting element with attaching timeline'); +</script> + +<template id=deferred_timeline_attached_display_none> + <div class="scope"> + <div class=target>Test</div> + <div class="scroller timeline"> + <div class=content></div> + </div> + </div> +</template> +<script> + promise_test(async (t) => { + await inflate(t, deferred_timeline_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'); + + 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=deferred_timeline_appearing> + <div class=container> + <div class=target>Test</div> + <div class="scroller timeline"> + <div class=content></div> + </div> + </div> +</template> +<script> + promise_test(async (t) => { + await inflate(t, deferred_timeline_appearing); + let container = main.querySelector('.container'); + let scroller = main.querySelector('.scroller'); + let target = main.querySelector('.target'); + + await scrollTop(scroller, 350); // 50% + + // Not attached to any timeline initially. + assert_equals(getComputedStyle(target).width, '0px'); + + // Add the deferred timeline. + container.classList.add('scope'); + await waitForNextFrame(); + assert_equals(getComputedStyle(target).width, '100px'); // 0px => 200px, 50% + + // Remove the deferred timeline. + container.classList.remove('scope'); + await waitForNextFrame(); + assert_equals(getComputedStyle(target).width, '0px'); + }, 'A deferred timeline appearing dynamically in the ancestor chain'); +</script> + +<template id=deferred_timeline_on_self> + <div class="scroller timeline scope"> + <div class=content> + <div class=target></div> + </div> + <div class=scroller2></div> + </div> +</template> +<script> + promise_test(async (t) => { + await inflate(t, deferred_timeline_on_self); + let scroller = main.querySelector('.scroller'); + let target = main.querySelector('.target'); + await scrollTop(scroller, 525); // 75% + + assert_equals(getComputedStyle(target).width, '150px'); // 0px => 200px, 75% + + // A second scroll-timeline now attaches to the same root. + let scroller2 = main.querySelector('.scroller2'); + scroller2.classList.add('timeline'); + await waitForNextFrame(); + + // The deferred timeline produced by timeline-scope is now inactive, + // but it doesn't matter, because we preferred to attach + // to the non-deferred timeline. + assert_equals(getComputedStyle(target).width, '150px'); // 0px => 200px, 75% + }, 'Animations prefer non-deferred timelines'); + +</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..552461c9b6 --- /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..1bf034a742 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-animation.html @@ -0,0 +1,223 @@ +<!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 forwards; + 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'); + document.getAnimations()[0].effect.updateTiming( { fill: 'none' }); + assert_equals(getComputedStyle(target).zIndex, '-1'); + }, 'Default view-timeline'); +</script> + +<template id=horizontal_timeline> + <style> + #target { + view-timeline: --t1 x; + animation: anim 1s linear forwards; + 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'); + document.getAnimations()[0].effect.updateTiming( { fill: 'none' }); + assert_equals(getComputedStyle(target).zIndex, '-1'); + }, 'Horizontal view-timeline'); +</script> + +<template id=multiple_timelines> + <style> + #timelines { + view-timeline: --tv y, --th x; + 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; + timeline-scope: --tv, --th; + } + #scroller > div { + z-index: -1; + width: 50px; + height: 50px; + } + #target_v { + animation: anim 1s linear forwards; + animation-timeline: --tv; + } + #target_h { + animation: anim 1s linear forwards; + 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'); + document.getElementById('target_v').getAnimations()[0]. + effect.updateTiming({ fill: 'none' }); + 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'); + document.getElementById('target_h').getAnimations()[0]. + effect.updateTiming({ fill: 'none' }); + assert_equals(getComputedStyle(target_v).zIndex, '-1'); + assert_equals(getComputedStyle(target_h).zIndex, '-1'); + }, 'Multiple view-timelines on the same element'); +</script> diff --git a/testing/web-platform/tests/scroll-animations/css/view-timeline-axis-computed.html b/testing/web-platform/tests/scroll-animations/css/view-timeline-axis-computed.html new file mode 100644 index 0000000000..30b2a1ae05 --- /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: y; } +</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', 'y'); +test_computed_value('view-timeline-axis', 'x'); +test_computed_value('view-timeline-axis', 'block, inline'); +test_computed_value('view-timeline-axis', 'inline, block'); +test_computed_value('view-timeline-axis', 'block, y, x, 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..1ebe4410a9 --- /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', 'y'); +test_valid_value('view-timeline-axis', 'x'); +test_valid_value('view-timeline-axis', 'block, inline'); +test_valid_value('view-timeline-axis', 'inline, block'); +test_valid_value('view-timeline-axis', 'block, y, x, 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..81dc8353c2 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-dynamic.html @@ -0,0 +1,200 @@ +<!DOCTYPE html> +<title>Changes to view-timeline are reflected in dependent elements</title> +<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#view-timeline-shorthand"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="support/testcommon.js"></script> +<style> + @keyframes anim { + from { z-index: 0; } + to { z-index: 100; } + } + .scroller { + overflow: hidden; + width: 100px; + height: 100px; + } + .scroller > div { + height: 100px; + } + #target { + height: 0px; + z-index: -1; + } +</style> +<main id=main></main> +<script> + setup(assert_implements_animation_timeline); + + function inflate(t, template) { + t.add_cleanup(() => main.replaceChildren()); + main.append(template.content.cloneNode(true)); + main.offsetTop; + } + async function scrollTop(e, value) { + e.scrollTop = value; + await waitForNextFrame(); + } + async function scrollLeft(e, value) { + e.scrollLeft = value; + await waitForNextFrame(); + } +</script> + +<template id=dynamic_view_timeline_attachment> + <style> + #scroller { + timeline-scope: --t1; + } + .timeline { + view-timeline: --t1; + } + #target { + animation: anim 1s linear; + animation-timeline: --t1; + } + </style> + <div id=scroller class=scroller> + <div id=div75></div> + <div id=div25></div> + <div id=div_before></div> + <div id=target></div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, dynamic_view_timeline_attachment); + + 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 attachment'); +</script> + +<template id=dynamic_view_timeline_axis> + <style> + #timeline { + view-timeline: --t1; + width: 100px; + height: 100px; + margin: 100px; + } + #target { + animation: anim 1s linear; + animation-timeline: --t1; + } + </style> + <div id=scroller class=scroller> + <div id=timeline style="background: red;"> + <div id=target></div> + </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 = 'x'; + 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; + } + #target { + animation: anim 1s linear; + animation-timeline: --t1; + } + </style> + <div id=scroller class=scroller> + <div id=timeline style="background: red;"> + <div id=target></div> + </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> + #scroller { + timeline-scope: --t1; + } + #timeline { + view-timeline: --t1; + } + #target { + animation: anim 1s linear; + animation-timeline: --t1; + } + </style> + <div id=scroller class=scroller> + <div></div> + <div id=timeline></div> + <div id=target></div> + </div> +</template> +<script> + promise_test(async (t) => { + inflate(t, timeline_display_none); + + await scrollTop(scroller, 50); + assert_equals(getComputedStyle(target).zIndex, '25', 'display:block'); + timeline.style.display = 'none'; + await waitForNextFrame(); + // The timeline became inactive. + assert_equals(getComputedStyle(target).zIndex, '-1', 'display:none'); + }, 'Element with scoped 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..f9aa0f2918 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-inset-animation.html @@ -0,0 +1,743 @@ +<!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 forwards; + 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% + }, '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 forwards; + 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% + }, '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 forwards; + 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% + }, '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 forwards; + 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% + }, '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 forwards; + 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% + }, 'view-timeline-inset with negative values'); +</script> + +<template id=test_horizontal> + <style> + #target { + view-timeline: --t1 x; + view-timeline-inset: 10px 20px; + animation: anim 1s linear forwards; + 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% + }, '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 forwards; + 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% + }, '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 forwards; + 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% + }, '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 forwards; + 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% + }, '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 forwards; + 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% + }, '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 forwards; + 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% + }, '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 forwards; + 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% + }, '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 forwards; + 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% + }, '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 forwards; + 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% + }, '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 forwards; + 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% + }, '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 forwards; + 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% + }, '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 forwards; + 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% + }, '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 y; + view-timeline-inset: auto auto; + animation: anim 1s linear forwards; + 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% + }, 'view-timeline-inset:auto, y'); +</script> + +<template id=test_auto_vertical_vertical_rl> + <style> + #scroller { + scroll-padding-inline: 10px 20px; + writing-mode: vertical-rl; + } + #target { + view-timeline: --t1 y; + view-timeline-inset: auto auto; + animation: anim 1s linear forwards; + 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% + }, 'view-timeline-inset:auto, y, 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 y; + view-timeline-inset: auto auto; + animation: anim 1s linear forwards; + 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% + }, 'view-timeline-inset:auto, y, vertical-rl, rtl'); +</script> + +<template id=test_auto_horizontal> + <style> + #scroller { + scroll-padding-inline: 10px 20px; + } + #target { + view-timeline: --t1 x; + view-timeline-inset: auto auto; + animation: anim 1s linear forwards; + 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% + }, 'view-timeline-inset:auto, x'); +</script> + +<template id=test_auto_horizontal_rtl> + <style> + #scroller { + scroll-padding-inline: 10px 20px; + direction: rtl; + } + #target { + view-timeline: --t1 x; + view-timeline-inset: auto auto; + animation: anim 1s linear forwards; + 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% + }, 'view-timeline-inset:auto, x, rtl'); +</script> + +<template id=test_auto_horizontal_vertical_lr> + <style> + #scroller { + scroll-padding-block: 10px 20px; + writing-mode: vertical-lr; + } + #target { + view-timeline: --t1 x; + view-timeline-inset: auto auto; + animation: anim 1s linear forwards; + 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% + }, 'view-timeline-inset:auto, x, 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 x; + view-timeline-inset: auto auto; + animation: anim 1s linear forwards; + 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% + }, 'view-timeline-inset:auto, x, 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 forwards; + 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% + }, '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..011f03cb5d --- /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..067ac1fa96 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-lookup.html @@ -0,0 +1,253 @@ +<!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> + async function inflate(t, template) { + t.add_cleanup(() => main.replaceChildren()); + return runAndWaitForFrameUpdate(() => { + main.append(template.content.cloneNode(true)); + }); + } +</script> + +<template id=timeline_self> + <style> + #target { + height: 0px; + view-timeline: --t1; + animation: anim 1s linear forwards; + 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) => { + await inflate(t, timeline_self); + assert_equals(getComputedStyle(target).zIndex, '100'); + }, 'view-timeline on self'); +</script> + +<template id=timeline_preceding_sibling> + <style> + #scroller { + timeline-scope: --t1; + } + #timeline { + height: 0px; + view-timeline: --t1; + } + #target { + animation: anim 1s linear; + animation-timeline: --t1; + } + </style> + <div id=scroller class=scroller> + <div></div> + <div id=timeline></div> + <div></div> + <div></div> + <div id=target></div> + <div></div> + <div></div> + </div> +</template> +<script> + promise_test(async (t) => { + await inflate(t, timeline_preceding_sibling); + assert_equals(getComputedStyle(target).zIndex, '75'); + }, 'timeline-scope 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) => { + await inflate(t, timeline_ancestor); + assert_equals(getComputedStyle(target).zIndex, '25'); + }, 'view-timeline on ancestor'); +</script> + +<template id=timeline_ancestor_sibling> + <style> + #scroller { + timeline-scope: --t1; + } + #timeline { + height: 0px; + view-timeline: --t1; + } + #target { + animation: anim 1s linear; + animation-timeline: --t1; + } + </style> + <div id=scroller class=scroller> + <div></div> + <div id=timeline></div> + <div></div> + <div> + <div> + <div id=target></div> + </div> + </div> + <div></div> + <div></div> + </div> +</template> +<script> + promise_test(async (t) => { + await inflate(t, timeline_ancestor_sibling); + assert_equals(getComputedStyle(target).zIndex, '75'); + }, 'timeline-scope on ancestor sibling'); +</script> + +<template id=timeline_ancestor_sibling_conflict> + <style> + #scroller { + timeline-scope: --t1; + } + #timeline1, #timeline2 { + height: 0px; + view-timeline: --t1; + } + #target { + animation: anim 1s linear; + animation-timeline: --t1; + } + </style> + <div id=scroller class=scroller> + <div></div> + <div id=timeline1></div> + <div></div> + <div id=timeline2></div> + <div> + <div> + <div id=target></div> + </div> + </div> + <div></div> + <div></div> + </div> +</template> +<script> + promise_test(async (t) => { + await inflate(t, timeline_ancestor_sibling_conflict); + assert_equals(getComputedStyle(target).zIndex, 'auto'); + }, 'timeline-scope on ancestor sibling, conflict remains unresolved'); +</script> + +<template id=timeline_ancestor_closer_timeline_wins> + <style> + #scroller { + timeline-scope: --t1; + } + #timeline { + height: 0px; + view-timeline: --t1; + } + #parent { + timeline-scope: --t1; /* Inactive */ + } + #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) => { + await inflate(t, timeline_ancestor_closer_timeline_wins); + assert_equals(getComputedStyle(target).zIndex, 'auto'); + }, 'timeline-scope on ancestor sibling, closer timeline wins'); +</script> + +<template id=timeline_ancestor_scroll_timeline_wins_on_same_element> + <style> + #scroller { + view-timeline: --t1; + view-timeline-inset: 50px; + scroll-timeline: --t1; + } + #target { + 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) => { + await inflate(t, timeline_ancestor_scroll_timeline_wins_on_same_element); + // 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..3304723f43 --- /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..2b22cbe036 --- /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'); +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_invalid_value('view-timeline-name', 'auto'); +test_invalid_value('view-timeline-name', 'abc'); +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..11902a3c6e --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-name-shadow.html @@ -0,0 +1,181 @@ +<!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> + +<main id=main></main> +<script> + function inflate(t, template) { + t.add_cleanup(() => main.replaceChildren()); + main.append(template.content.cloneNode(true)); + main.offsetTop; + } +</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 x; + } + </style> + <div class=scroller> + <div> + <div class=target> + <template shadowrootmode=open shadowrootclonable> + <style> + :host { + view-timeline: --timeline y; + } + </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, 'y'); + }, '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 x; + } + </style> + <div class=scroller> + <div class=host> + <template shadowrootmode=open shadowrootclonable> + <style> + ::slotted(.target) { + view-timeline: --timeline y; + } + </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, 'y'); + }, 'Outer animation can see view timeline defined by ::slotted'); +</script> + + +<template id=view_timeline_part> + <style> + .host { + view-timeline: --timeline y; + } + .host::part(foo) { + view-timeline: --timeline x; + } + </style> + <div class=host> + <template shadowrootmode=open shadowrootclonable> + <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, 'x'); + }, '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 x; + } + </style> + <div class=scroller> + <div class=host> + <template shadowrootmode=open shadowrootclonable> + <style> + div { + view-timeline: --timeline y; + } + </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, 'y'); + }, '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..9e5993b63a --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-range-animation.html @@ -0,0 +1,232 @@ +<!DOCTYPE html> +<title>View timelines and animation attachment ranges</title> +<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#named-timeline-range"> +<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#animation-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; + } + #scroller > div { + margin: 800px 0px; + width: 100px; + height: 100px; + } + #target { + font-size: 10px; + background-color: green; + z-index: -1; + } +</style> +<main id=main> +</main> + +<template id=template_without_scope> + <div id=scroller> + <div id=target class=timeline></div> + </div> +</template> + +<template id=template_with_scope> + <div id=scope> + <div id=target></div> + <div id=scroller> + <div class=timeline></div> + </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.ready)); + } + 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_range(options, template, desc_suffix) { + if (template === undefined) + template = template_without_scope; + if (desc_suffix === undefined) + desc_suffix = ''; + + promise_test(async (t) => { + inflate(t, template); + let scroller = main.querySelector('#scroller'); + let target = main.querySelector('#target'); + let timeline = main.querySelector('.timeline'); + let scope = main.querySelector('#scope'); + + if (scope != null) { + scope.style.timelineScope = '--t1'; + } + + timeline.style.viewTimeline = '--t1'; + 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}] ${desc_suffix}`.trim()); + } + + test_animation_range({ + rangeStart: 'initial', + rangeEnd: 'initial', + startOffset: 600, + endOffset: 900 + }); + + test_animation_range({ + rangeStart: 'cover 0%', + rangeEnd: 'cover 100%', + startOffset: 600, + endOffset: 900 + }); + + test_animation_range({ + rangeStart: 'contain 0%', + rangeEnd: 'contain 100%', + startOffset: 700, + endOffset: 800 + }); + + + test_animation_range({ + rangeStart: 'entry 0%', + rangeEnd: 'entry 100%', + startOffset: 600, + endOffset: 700 + }); + + test_animation_range({ + rangeStart: 'exit 0%', + rangeEnd: 'exit 100%', + startOffset: 800, + endOffset: 900 + }); + + test_animation_range({ + rangeStart: 'contain -50%', + rangeEnd: 'entry 200%', + startOffset: 650, + endOffset: 800 + }); + + test_animation_range({ + rangeStart: 'entry 0%', + rangeEnd: 'exit 100%', + startOffset: 600, + endOffset: 900 + }); + + test_animation_range({ + rangeStart: 'cover 20px', + rangeEnd: 'cover 100px', + startOffset: 620, + endOffset: 700 + }); + + test_animation_range({ + rangeStart: 'contain 20px', + rangeEnd: 'contain 100px', + startOffset: 720, + endOffset: 800 + }); + + test_animation_range({ + rangeStart: 'entry 20px', + rangeEnd: 'entry 100px', + startOffset: 620, + endOffset: 700 + }); + + test_animation_range({ + rangeStart: 'entry-crossing 20px', + rangeEnd: 'entry-crossing 100px', + startOffset: 620, + endOffset: 700 + }); + + test_animation_range({ + rangeStart: 'exit 20px', + rangeEnd: 'exit 80px', + startOffset: 820, + endOffset: 880 + }); + + test_animation_range({ + rangeStart: 'exit-crossing 20px', + rangeEnd: 'exit-crossing 80px', + startOffset: 820, + endOffset: 880 + }); + + test_animation_range({ + rangeStart: 'contain 20px', + rangeEnd: 'contain calc(100px - 10%)', + startOffset: 720, + endOffset: 790 + }); + + test_animation_range({ + rangeStart: 'exit 2em', + rangeEnd: 'exit 8em', + startOffset: 820, + endOffset: 880 + }); + + // Test animation-range via timeline-scope. + test_animation_range({ + rangeStart: 'exit 2em', + rangeEnd: 'exit 8em', + startOffset: 820, + endOffset: 880 + }, template_with_scope, '(scoped)'); + +</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..960a8e6ecf --- /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..e1938caf50 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-range-update.html @@ -0,0 +1,66 @@ +<!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(); + + // 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.html b/testing/web-platform/tests/scroll-animations/css/view-timeline-shorthand.html new file mode 100644 index 0000000000..9027eb0b09 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/css/view-timeline-shorthand.html @@ -0,0 +1,164 @@ +<!DOCTYPE html> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#view-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('view-timeline', '--abcd'); +test_valid_value('view-timeline', 'none block', 'none'); +test_valid_value('view-timeline', 'none inline'); + +// view-timeline-name: inline/block/x/y. +test_valid_value('view-timeline', '--inline block', '--inline'); +test_valid_value('view-timeline', '--block block', '--block'); +test_valid_value('view-timeline', '--y block', '--y'); +test_valid_value('view-timeline', '--x block', '--x'); + +test_valid_value('view-timeline', '--a, --b, --c'); +test_valid_value('view-timeline', '--a inline, --b block, --c y', '--a inline, --b, --c y'); +test_valid_value('view-timeline', '--auto'); +test_valid_value('view-timeline', '--abcd block auto', '--abcd'); +test_valid_value('view-timeline', '--abcd block auto auto', '--abcd'); +test_valid_value('view-timeline', '--abcd block 1px 2px', '--abcd 1px 2px'); +test_valid_value('view-timeline', '--abcd inline 1px 2px', '--abcd inline 1px 2px'); +test_valid_value('view-timeline', '--abcd 1px 2px inline', '--abcd inline 1px 2px'); +test_valid_value('view-timeline', '--abcd 1px 2px block', '--abcd 1px 2px'); +test_valid_value('view-timeline', '--abcd auto auto block', '--abcd'); +test_valid_value('view-timeline', '--abcd auto block', '--abcd'); +test_valid_value('view-timeline', '--abcd block 1px 1px', '--abcd 1px'); + +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_invalid_value('view-timeline', 'auto'); +test_invalid_value('view-timeline', 'auto auto'); +test_invalid_value('view-timeline', '--abc 500kg'); +test_invalid_value('view-timeline', '--abc #ff0000'); +test_invalid_value('view-timeline', '--abc red red'); + +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', '--y block', '--y'); +test_computed_value('view-timeline', '--x block', '--x'); +test_computed_value('view-timeline', '--a, --b, --c'); +test_computed_value('view-timeline', '--a inline, --b block, --c y', '--a inline, --b, --c y'); +test_computed_value('view-timeline', '--abcd block auto', '--abcd'); +test_computed_value('view-timeline', '--abcd block auto auto', '--abcd'); +test_computed_value('view-timeline', '--abcd block 1px 2px', '--abcd 1px 2px'); +test_computed_value('view-timeline', '--abcd inline 1px 2px', '--abcd inline 1px 2px'); +test_computed_value('view-timeline', '--abcd 1px 2px inline', '--abcd inline 1px 2px'); +test_computed_value('view-timeline', '--abcd 1px 2px block', '--abcd 1px 2px'); +test_computed_value('view-timeline', '--abcd auto auto block', '--abcd'); +test_computed_value('view-timeline', '--abcd auto block', '--abcd'); +test_computed_value('view-timeline', '--abcd block 1px 1px', '--abcd 1px'); + +test_shorthand_value('view-timeline', '--abc y', +{ + 'view-timeline-name': '--abc', + 'view-timeline-axis': 'y', + 'view-timeline-inset': 'auto', +}); +test_shorthand_value('view-timeline', '--abc y, --def', +{ + 'view-timeline-name': '--abc, --def', + 'view-timeline-axis': 'y, block', + 'view-timeline-inset': 'auto, auto', +}); +test_shorthand_value('view-timeline', '--abc, --def', +{ + 'view-timeline-name': '--abc, --def', + 'view-timeline-axis': 'block, block', + 'view-timeline-inset': 'auto, auto', +}); +test_shorthand_value('view-timeline', '--inline x', +{ + 'view-timeline-name': '--inline', + 'view-timeline-axis': 'x', + 'view-timeline-inset': 'auto', +}); +test_shorthand_value('view-timeline', '--abc 1px 2px', +{ + 'view-timeline-name': '--abc', + 'view-timeline-axis': 'block', + 'view-timeline-inset': '1px 2px', +}); +test_shorthand_value('view-timeline', '--abc 1px', +{ + 'view-timeline-name': '--abc', + 'view-timeline-axis': 'block', + 'view-timeline-inset': '1px', +}); +test_shorthand_value('view-timeline', '--abc 1px inline', +{ + 'view-timeline-name': '--abc', + 'view-timeline-axis': 'inline', + 'view-timeline-inset': '1px', +}); + +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-inset': 'auto', +}, '--abc inline'); + +test_shorthand_contraction('view-timeline', { + 'view-timeline-name': '--a, --b', + 'view-timeline-axis': 'inline, block', + 'view-timeline-inset': 'auto, auto', +}, '--a inline, --b'); + +test_shorthand_contraction('view-timeline', { + 'view-timeline-name': '--a, --b', + 'view-timeline-axis': 'inline, block', + 'view-timeline-inset': '1px 2px, 3px 3px', +}, '--a inline 1px 2px, --b 3px'); + +test_shorthand_contraction('view-timeline', { + 'view-timeline-name': 'none, none', + 'view-timeline-axis': 'block, block', + 'view-timeline-inset': 'auto auto, auto', +}, 'none, none'); + +// Longhands with different lengths: + +test_shorthand_contraction('view-timeline', { + 'view-timeline-name': '--a, --b, --c', + 'view-timeline-axis': 'inline, inline', + 'view-timeline-inset': 'auto, auto', +}, '--a inline, --b inline, --c inline'); + +test_shorthand_contraction('view-timeline', { + 'view-timeline-name': '--a, --b', + 'view-timeline-axis': 'inline, inline, inline', + 'view-timeline-inset': 'auto, auto, auto', +}, '--a inline, --b inline'); + +test_shorthand_contraction('view-timeline', { + 'view-timeline-name': '--a, --b', + 'view-timeline-axis': 'inline, inline', + 'view-timeline-inset': 'auto, auto, auto', +}, '--a inline, --b inline'); +</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..2961fedd42 --- /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..1bd6f0468c --- /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..db260f15f0 --- /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> |