Source code
Revision control
Copy as Markdown
Other Tools
Test Info: Warnings
- This test has a WPT meta file that expects 7 subtest issues.
- This WPT test may be referenced by the following Test IDs:
- /scroll-animations/css/scroll-timeline-dynamic.tentative.html - WPT Dashboard Interop Dashboard
<!DOCTYPE html>
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1">
<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).
await assert_width(element, '100px');
element.style.animationPlayState = 'running';
await assert_width(element, '120px');
}, 'Switching timelines and pausing at the same time');
</script>