| <!DOCTYPE html> |
| <title>@scroll-timeline source invalidation</title> |
| <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 { |
| overflow: hidden; |
| height: 0; |
| } |
| .scroller { |
| overflow: scroll; |
| width: 100px; |
| height: 100px; |
| } |
| .contents { |
| height: 200px; |
| } |
| @keyframes expand { |
| from { width: 100px; } |
| to { width: 200px; } |
| } |
| @scroll-timeline timeline { |
| source: selector(#scroller); |
| time-range: 1e10s; |
| start: 0px; |
| end: 100px; |
| } |
| #element { |
| width: 0px; |
| height: 20px; |
| animation: expand 1e10s 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=scrollers></div> |
| <div id=element></div> |
| <script> |
| |
| function createScroller() { |
| let scroller = document.createElement('div'); |
| let contents = document.createElement('div'); |
| scroller.classList.add('scroller'); |
| contents.classList.add('contents'); |
| scroller.append(contents); |
| return scroller; |
| } |
| |
| function wrapInDiv(element) { |
| let div = document.createElement('div'); |
| div.append(element); |
| return div; |
| } |
| |
| function scrollerAt(n) { |
| return document.querySelectorAll('.scroller')[n]; |
| } |
| |
| // Resets #scrollers to a state where it has three .scroller children with |
| // scrollTop offsets 10, 20 and 30. |
| function cleanup() { |
| while (scrollers.firstChild) |
| scrollers.firstChild.remove(); |
| |
| for (let i = 0; i < 3; i++) |
| scrollers.append(createScroller()); |
| |
| scrollerAt(0).scrollTop = 10; |
| scrollerAt(1).scrollTop = 20; |
| scrollerAt(2).scrollTop = 30; |
| } |
| |
| // Do an initial "cleanup" to set up the first test. |
| cleanup(); |
| |
| function invalidation_test(func, description) { |
| promise_test(async (t) => { |
| t.add_cleanup(cleanup); |
| await func(); |
| }, description); |
| } |
| |
| invalidation_test(() => { |
| assert_equals(getComputedStyle(element).width, '0px'); |
| }, 'Nonexistent source'); |
| |
| invalidation_test(() => { |
| assert_equals(getComputedStyle(element).width, '0px'); |
| scrollerAt(0).setAttribute('id', 'scroller'); |
| assert_equals(getComputedStyle(element).width, '110px'); |
| scrollerAt(1).setAttribute('id', 'scroller'); // No effect |
| assert_equals(getComputedStyle(element).width, '110px'); |
| scrollerAt(2).setAttribute('id', 'scroller'); // No effect |
| assert_equals(getComputedStyle(element).width, '110px'); |
| }, 'Setting id attribute'); |
| |
| invalidation_test(() => { |
| assert_equals(getComputedStyle(element).width, '0px'); |
| scrollerAt(0).setAttribute('id', 'scroller'); |
| assert_equals(getComputedStyle(element).width, '110px'); |
| scrollerAt(0).removeAttribute('id'); |
| assert_equals(getComputedStyle(element).width, '0px'); |
| }, 'Removing id attribute'); |
| |
| invalidation_test(() => { |
| assert_equals(getComputedStyle(element).width, '0px'); |
| scrollerAt(2).setAttribute('id', 'scroller'); |
| assert_equals(getComputedStyle(element).width, '130px'); |
| scrollerAt(1).setAttribute('id', 'scroller'); |
| assert_equals(getComputedStyle(element).width, '120px'); |
| scrollerAt(0).setAttribute('id', 'scroller'); |
| assert_equals(getComputedStyle(element).width, '110px'); |
| }, 'Setting id attribute earlier in the tree'); |
| |
| invalidation_test(async () => { |
| assert_equals(getComputedStyle(element).width, '0px'); |
| |
| // Appending a new element with id 'scroller' already set before |
| // insertion into the tree. |
| let scroller = createScroller(); |
| scroller.setAttribute('id', 'scroller'); |
| scrollers.append(scroller); |
| |
| // Make sure |scroller| has a layout box. |
| // |
| // https://drafts.csswg.org/scroll-animations-1/#avoiding-cycles |
| // |
| // TODO: Depending on the outcome of Issue 5261, the call to offsetTop |
| // might be unnecessary. |
| // https://github.com/w3c/csswg-drafts/issues/5261 |
| scroller.offsetTop; |
| await waitForNextFrame(); |
| |
| assert_equals(getComputedStyle(element).width, '100px'); |
| }, 'Appending a new element'); |
| |
| invalidation_test(async () => { |
| assert_equals(getComputedStyle(element).width, '0px'); |
| |
| let scroller = createScroller(); |
| scroller.setAttribute('id', 'scroller'); |
| scrollers.append(wrapInDiv(wrapInDiv(scroller))); |
| await waitForNextFrame(); |
| |
| assert_equals(getComputedStyle(element).width, '100px'); |
| }, 'Inserting a subtree with #scroller descendant'); |
| |
| invalidation_test(() => { |
| assert_equals(getComputedStyle(element).width, '0px'); |
| |
| scrollerAt(0).setAttribute('id', 'scroller'); |
| scrollerAt(1).setAttribute('id', 'scroller'); |
| scrollerAt(2).setAttribute('id', 'scroller'); |
| assert_equals(getComputedStyle(element).width, '110px'); |
| |
| scrollerAt(0).remove(); |
| assert_equals(getComputedStyle(element).width, '120px'); |
| |
| scrollerAt(0).remove(); |
| assert_equals(getComputedStyle(element).width, '130px'); |
| |
| scrollerAt(0).remove(); |
| assert_equals(getComputedStyle(element).width, '0px'); |
| }, 'Removing source element'); |
| |
| invalidation_test(async () => { |
| assert_equals(getComputedStyle(element).width, '0px'); |
| |
| // Create a chain: #scrollers -> div -> div -> #scroller |
| let scroller = createScroller(); |
| let div = wrapInDiv(wrapInDiv(scroller)); |
| scrollers.append(div); |
| scroller.setAttribute('id', 'scroller'); |
| scroller.scrollTop = 50; |
| await waitForNextFrame(); |
| assert_equals(getComputedStyle(element).width, '150px'); |
| |
| div.remove(); |
| assert_equals(getComputedStyle(element).width, '0px'); |
| }, 'Removing ancestor of source element'); |
| |
| </script> |