| <!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> |
| <style> |
| @keyframes expand_width { |
| from { width: 100px; } |
| to { width: 100px; } |
| } |
| @keyframes expand_height { |
| from { height: 100px; } |
| to { height: 100px; } |
| } |
| @scroll-timeline timeline1 { |
| source: selector(#scroller1); |
| time-range: 10s; |
| } |
| @scroll-timeline timeline2 { |
| source: selector(#scroller2); |
| time-range: 10s; |
| } |
| main { |
| height: 0px; |
| overflow: hidden; |
| } |
| .scroller { |
| height: 100px; |
| overflow: scroll; |
| } |
| .scroller > div { |
| height: 200px; |
| } |
| #element1 { |
| width: 1px; |
| animation: expand_width 10s timeline1; |
| } |
| #element2 { |
| height: 1px; |
| animation: expand_height 10s timeline2; |
| } |
| </style> |
| <main id=main></main> |
| <div id=element1></div> |
| <div> |
| <div id=element2></div> |
| </div> |
| <script> |
| function insertScroller(id) { |
| let scroller = document.createElement('div'); |
| scroller.setAttribute('id', id); |
| scroller.setAttribute('class', 'scroller'); |
| scroller.append(document.createElement('div')); |
| main.append(scroller); |
| } |
| |
| promise_test(async () => { |
| await waitForNextFrame(); |
| |
| let events1 = []; |
| let events2 = []; |
| |
| insertScroller('scroller1'); |
| // Even though #scroller1 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('scroller2'); |
| 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> |