<!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>
