blob: 0722115593c4800aac8b613be936e5aa71d7d4c4 [file] [log] [blame]
<!DOCTYPE html>
<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#scroll-timeline-at-rule">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/web-animations/testcommon.js"></script>
<style>
#scrollers > div {
overflow: scroll;
width: 100px;
height: 100px;
}
#scrollers > div > div {
height: 200px;
}
@keyframes expand {
from { width: 100px; }
to { width: 200px; }
}
#element {
width: 0px;
height: 20px;
animation-name: expand;
animation-duration: 1e10s;
animation-timing-function: linear;
}
</style>
<div id=scrollers>
<div id=scroller1><div></div></div>
<div id=scroller2><div></div></div>
</div>
<div id=container></div>
<script>
// Force layout of scrollers.
scroller1.offsetTop;
scroller2.offsetTop;
scroller1.scrollTop = 20;
scroller2.scrollTop = 40;
function insertElement() {
let element = document.createElement('div');
element.id = 'element';
container.append(element);
return element;
}
function insertSheet(text) {
let style = document.createElement('style');
style.textContent = text;
container.append(style);
return style;
}
// Insert an @scroll-timeline rule given 'options', where each option
// has a reasonable default.
function insertScrollTimeline(options) {
if (typeof(options) == 'undefined')
options = {};
if (typeof(options.name) == 'undefined')
options.name = 'timeline';
if (typeof(options.source) == 'undefined')
options.source = 'selector(#scroller1)';
if (typeof(options.timeRange) == 'undefined')
options.timeRange = '1e10s';
if (typeof(options.start) == 'undefined')
options.start = '0px';
if (typeof(options.end) == 'undefined')
options.end = '100px';
return insertSheet(`
@scroll-timeline ${options.name} {
source: ${options.source};
time-range: ${options.timeRange};
start: ${options.start};
end: ${options.end};
}
`);
}
// 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();
}
}, description);
};
// Verify that the computed style is as expected immediately after the
// rule change took place.
instantiate(async (element, expected) => {
assert_equals(getComputedStyle(element).width, expected);
}, description + ' [immediate]');
// Verify that the computed style after scrolling a bit.
instantiate(async (element, expected) => {
scroller1.scrollTop = scroller1.scrollTop + 1;
scroller2.scrollTop = scroller2.scrollTop + 1;
await waitForNextFrame();
scroller1.scrollTop = scroller1.scrollTop - 1;
scroller2.scrollTop = scroller2.scrollTop - 1;
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.
let sheet1 = insertScrollTimeline();
let sheet2 = insertSheet('#element { animation-timeline: timeline; }');
await assert_width(element, '120px');
// Switching from ScrollTimeline -> DocumentTimeline should preserve
// current time.
sheet1.remove();
sheet2.remove();
await assert_width(element, '120px');
}, 'Switching between document and scroll timelines');
dynamic_rule_test(async (t, assert_width) => {
let element = insertElement();
// Note: #scroller1 is at 20%, and #scroller2 is at 40%.
insertScrollTimeline({name: 'timeline1', source: 'selector(#scroller1)'});
insertScrollTimeline({name: 'timeline2', source: 'selector(#scroller2)'});
insertSheet(`
.tl1 { animation-timeline: timeline1; }
.tl2 { animation-timeline: timeline2; }
`);
await assert_width(element, '100px');
element.classList.add('tl1');
await assert_width(element, '120px');
element.classList.add('tl2');
await assert_width(element, '140px');
element.classList.remove('tl2');
await assert_width(element, '120px');
// Switching from ScrollTimeline -> DocumentTimeline should preserve
// current time.
element.classList.remove('tl1');
await assert_width(element, '120px');
}, 'Changing computed value of animation-timeline changes effective timeline');
dynamic_rule_test(async (t, assert_width) => {
let element = insertElement();
insertScrollTimeline({source: 'selector(#scroller1)'});
insertSheet(`
.scroll { animation-timeline: timeline; }
.none { animation-timeline: none; }
`);
// DocumentTimeline applies by default.
await assert_width(element, '100px');
// DocumentTimeline -> none
element.classList.add('none');
await assert_width(element, '0px');
// none -> DocumentTimeline
element.classList.remove('none');
await assert_width(element, '100px');
// DocumentTimeline -> ScrollTimeline
element.classList.add('scroll');
await assert_width(element, '120px');
// ScrollTimeline -> none
element.classList.add('none');
await assert_width(element, '0px');
// none -> ScrollTimeline
element.classList.remove('none');
await assert_width(element, '120px');
}, 'Changing to/from animation-timeline:none');
dynamic_rule_test(async (t, assert_width) => {
let element = insertElement();
insertSheet('#element { animation-timeline: timeline; }');
await assert_width(element, '0px');
insertScrollTimeline({source: 'selector(#scroller1)'});
await assert_width(element, '120px');
insertScrollTimeline({source: 'selector(#scroller2)'});
await assert_width(element, '140px');
}, 'Changing the source descriptor switches effective timeline');
dynamic_rule_test(async (t, assert_width) => {
let element = insertElement();
insertSheet('#element { animation-timeline: timeline; }');
await assert_width(element, '0px');
insertScrollTimeline({timeRange: '1e10s'});
await assert_width(element, '120px');
insertScrollTimeline({timeRange: '1e9s'});
await assert_width(element, '102px');
}, 'Changing the time-range descriptor switches effective timeline');
dynamic_rule_test(async (t, assert_width) => {
let element = insertElement();
insertSheet('#element { animation-timeline: timeline; }');
await assert_width(element, '0px');
insertScrollTimeline({start: '0px'});
await assert_width(element, '120px');
insertScrollTimeline({start: '50px'});
await assert_width(element, '0px');
}, 'Changing the start descriptor switches effective timeline');
dynamic_rule_test(async (t, assert_width) => {
let element = insertElement();
insertSheet('#element { animation-timeline: timeline; }');
await assert_width(element, '0px');
insertScrollTimeline({end: '100px'});
await assert_width(element, '120px');
insertScrollTimeline({end: '10px'});
await assert_width(element, '0px');
}, 'Changing the end descriptor switches effective timeline');
dynamic_rule_test(async (t, assert_width) => {
let element = insertElement();
let reverse = insertSheet('#element { animation-direction: reverse; }');
insertSheet('#element { animation-timeline: timeline; }');
await assert_width(element, '0px');
// Note: #scroller1 is at 20%.
insertScrollTimeline({source: 'selector(#scroller1)'});
await assert_width(element, '180px');
// Note: #scroller1 is at 40%.
insertScrollTimeline({source: 'selector(#scroller2)'});
await assert_width(element, '160px');
reverse.remove();
await assert_width(element, '140px');
}, 'Reverse animation direction');
dynamic_rule_test(async (t, assert_width) => {
let element = insertElement();
insertSheet('#element { animation-timeline: timeline; }');
await assert_width(element, '0px');
// Note: #scroller1 is at 20%.
insertScrollTimeline({source: 'selector(#scroller1)'});
await assert_width(element, '120px');
let paused = insertSheet('#element { animation-play-state: paused; }');
// We should still be at the same position after pausing.
await assert_width(element, '120px');
// Note: #scroller1 is at 40%.
insertScrollTimeline({source: 'selector(#scroller2)'});
// Even when switching timelines, we should be at the same position until
// we unpause.
await assert_width(element, '120px');
// Unpausing should synchronize to the scroll position.
paused.remove();
await assert_width(element, '140px');
}, 'Switching timelines while paused');
dynamic_rule_test(async (t, assert_width) => {
let element = insertElement();
// Note: #scroller1 is at 20%.
insertScrollTimeline({source: 'selector(#scroller1)'});
await assert_width(element, '100px');
insertSheet(`#element {
animation-timeline: timeline;
animation-play-state: paused;
}`);
// Pausing should happen before the timeline is modified. (Tentative).
// https://github.com/w3c/csswg-drafts/issues/5653
await assert_width(element, '100px');
insertSheet('#element { animation-play-state: running; }');
await assert_width(element, '120px');
}, 'Switching timelines and pausing at the same time');
</script>