<!DOCTYPE html>
<title>Behavior of the timeline-scope property</title>
<link rel="help" src="https://github.com/w3c/csswg-drafts/issues/7759">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/web-animations/testcommon.js"></script>

<main id=main></main>
<script>
  async function inflate(t, template) {
    t.add_cleanup(() => main.replaceChildren());
    return runAndWaitForFrameUpdate(() => {
       main.append(template.content.cloneNode(true));
    });
  }

  async function scrollTop(e, value) {
    e.scrollTop = value;
    await waitForNextFrame();
  }
</script>
<style>
  @keyframes anim {
    from { width: 0px; }
    to { width: 200px; }
  }

  .scroller {
    overflow-y: hidden;
    width: 200px;
    height: 200px;
  }
  .scroller > .content {
    margin: 400px 0px;
    width: 100px;
    height: 100px;
    background-color: green;
  }
  .target {
    background-color: coral;
    width: 0px;
    animation: anim auto linear;
    animation-timeline: --t1;
  }
  .timeline {
    scroll-timeline-name: --t1;
  }
  .scope {
    timeline-scope: --t1;
  }

</style>

<!-- Basic Behavior -->

<template id=deferred_timeline>
  <div class="scope">
    <div class=target>Test</div>
    <div class="scroller timeline">
      <div class=content></div>
    </div>
  </div>
</template>
<script>
  promise_test(async (t) => {
    await inflate(t, deferred_timeline);
    let scroller = main.querySelector('.scroller');
    let target = main.querySelector('.target');

    const anim = target.getAnimations()[0];
    await anim.ready;

    await scrollTop(scroller, 350); // 50%
    assert_equals(getComputedStyle(target).width, '100px'); // 0px => 200px, 50%
  }, 'Descendant can attach to deferred timeline');
</script>

<template id=deferred_timeline_no_attachments>
  <div class="scope">
    <div class=target>Test</div>
    <div class="scroller">
      <div class=content></div>
    </div>
  </div>
</template>
<script>
  promise_test(async (t) => {
    await inflate(t, deferred_timeline_no_attachments);
    let scroller = main.querySelector('.scroller');
    let target = main.querySelector('.target');
    await scrollTop(scroller, 350); // 50%
    assert_equals(getComputedStyle(target).width, '0px');
  }, 'Deferred timeline with no attachments');
</script>

<template id=scroll_timeline_inner_interference>
  <div class="scroller timeline">
    <div class=content>
      <div class=target>Test</div>
      <div class="scroller timeline">
        <div class=content></div>
      </div>
    </div>
  </div>
</template>
<script>
  promise_test(async (t) => {
    await inflate(t, scroll_timeline_inner_interference);
    let scroller = main.querySelector('.scroller');
    let target = main.querySelector('.target');
    await scrollTop(scroller, 350); // 50%
    assert_equals(getComputedStyle(target).width, '100px'); // 0px => 200px, 50%
  }, 'Inner timeline does not interfere with outer timeline');
</script>

<template id=deferred_timeline_two_attachments>
  <div class="scope">
    <div class=target>Test</div>
    <div class="scroller timeline">
      <div class=content></div>
    </div>
    <!-- Extra attachment -->
    <div class="timeline"></div>
  </div>
</template>
<script>
  promise_test(async (t) => {
    await inflate(t, deferred_timeline_two_attachments);
    let scroller = main.querySelector('.scroller');
    let target = main.querySelector('.target');
    await scrollTop(scroller, 350); // 50%
    assert_equals(getComputedStyle(target).width, '0px');
  }, 'Deferred timeline with two attachments');
</script>

<!-- Dynamic Reattachment -->

<template id=deferred_timeline_reattach>
  <div class="scope">
    <div class=target>Test</div>
    <div class="scroller timeline">
      <div class=content></div>
    </div>
    <div class="scroller">
      <div class=content></div>
    </div>
  </div>
</template>
<script>
  promise_test(async (t) => {
    await inflate(t, deferred_timeline_reattach);
    let scrollers = main.querySelectorAll('.scroller');
    assert_equals(scrollers.length, 2);
    let target = main.querySelector('.target');
    await scrollTop(scrollers[0], 350); // 50%
    await scrollTop(scrollers[1], 175); // 25%

    // Attached to scrollers[0].
    assert_equals(getComputedStyle(target).width, '100px'); // 0px => 200px, 50%

    // Reattach to scrollers[1].
    await runAndWaitForFrameUpdate(() => {
      scrollers[0].classList.remove('timeline');
      scrollers[1].classList.add('timeline');
    });

    assert_equals(getComputedStyle(target).width, '50px'); // 0px => 200px, 25%
  }, 'Dynamically re-attaching');
</script>

<template id=deferred_timeline_dynamic_detach>
  <div class="scope">
    <div class=target>Test</div>
    <div class="scroller timeline">
      <div class=content></div>
    </div>
    <div class="scroller timeline">
      <div class=content></div>
    </div>
  </div>
</template>
<script>
  promise_test(async (t) => {
    await inflate(t, deferred_timeline_dynamic_detach);
    let scrollers = main.querySelectorAll('.scroller');
    assert_equals(scrollers.length, 2);
    let target = main.querySelector('.target');
    await scrollTop(scrollers[0], 350); // 50%
    await scrollTop(scrollers[1], 175); // 25%

    // Attached to two timelines initially:
    assert_equals(getComputedStyle(target).width, '0px');

    // Detach scrollers[1].
    await runAndWaitForFrameUpdate(() => {
      scrollers[1].classList.remove('timeline');
    });

    assert_equals(getComputedStyle(target).width, '100px'); // 0px => 200px, 50%

    // Also detach scrollers[0].
    scrollers[0].classList.remove('timeline');

    await waitForNextFrame();
    assert_equals(getComputedStyle(target).width, '0px');
  }, 'Dynamically detaching');
</script>

<template id=deferred_timeline_attached_removed>
  <div class="scope">
    <div class=target>Test</div>
    <div class="scroller timeline">
      <div class=content></div>
    </div>
  </div>
</template>
<script>
  promise_test(async (t) => {
    await inflate(t, deferred_timeline_attached_removed);
    let scroller = main.querySelector('.scroller');
    let target = main.querySelector('.target');
    await scrollTop(scroller, 350); // 50%
    assert_equals(getComputedStyle(target).width, '100px'); // 0px => 200px, 50%

    let scroller_parent = scroller.parentElement;
    scroller.remove();
    await waitForNextFrame();
    assert_equals(getComputedStyle(target).width, '0px');

    scroller_parent.append(scroller);
    await scrollTop(scroller, 350); // 50%
    assert_equals(getComputedStyle(target).width, '100px'); // 0px => 200px, 50%
  }, 'Removing/inserting element with attaching timeline');
</script>

<template id=deferred_timeline_attached_display_none>
  <div class="scope">
    <div class=target>Test</div>
    <div class="scroller timeline">
      <div class=content></div>
    </div>
  </div>
</template>
<script>
  promise_test(async (t) => {
    await inflate(t, deferred_timeline_attached_display_none);
    let scroller = main.querySelector('.scroller');
    let target = main.querySelector('.target');
    await scrollTop(scroller, 350); // 50%
    assert_equals(getComputedStyle(target).width, '100px'); // 0px => 200px, 50%

    scroller.style.display = 'none';
    await waitForNextFrame();
    assert_equals(getComputedStyle(target).width, '0px');

    scroller.style.display = 'block';
    await scrollTop(scroller, 350); // 50%
    assert_equals(getComputedStyle(target).width, '100px'); // 0px => 200px, 50%
  }, 'Ancestor attached element becoming display:none/block');
</script>

<template id=deferred_timeline_appearing>
  <div class=container>
    <div class=target>Test</div>
    <div class="scroller timeline">
      <div class=content></div>
    </div>
  </div>
</template>
<script>
  promise_test(async (t) => {
    await inflate(t, deferred_timeline_appearing);
    let container = main.querySelector('.container');
    let scroller = main.querySelector('.scroller');
    let target = main.querySelector('.target');

    await scrollTop(scroller, 350); // 50%

    // Not attached to any timeline initially.
    assert_equals(getComputedStyle(target).width, '0px');

    // Add the deferred timeline.
    container.classList.add('scope');
    await waitForNextFrame();
    assert_equals(getComputedStyle(target).width, '100px'); // 0px => 200px, 50%

    // Remove the deferred timeline.
    container.classList.remove('scope');
    await waitForNextFrame();
    assert_equals(getComputedStyle(target).width, '0px');
  }, 'A deferred timeline appearing dynamically in the ancestor chain');
</script>

<template id=deferred_timeline_on_self>
  <div class="scroller timeline scope">
    <div class=content>
      <div class=target></div>
    </div>
    <div class=scroller2></div>
  </div>
</template>
<script>
  promise_test(async (t) => {
    await inflate(t, deferred_timeline_on_self);
    let scroller = main.querySelector('.scroller');
    let target = main.querySelector('.target');
    await scrollTop(scroller, 525); // 75%

    assert_equals(getComputedStyle(target).width, '150px'); // 0px => 200px, 75%

    // A second scroll-timeline now attaches to the same root.
    let scroller2 = main.querySelector('.scroller2');
    scroller2.classList.add('timeline');
    await waitForNextFrame();

    // The deferred timeline produced by timeline-scope is now inactive,
    // but it doesn't matter, because we preferred to attach
    // to the non-deferred timeline.
    assert_equals(getComputedStyle(target).width, '150px'); // 0px => 200px, 75%
  }, 'Animations prefer non-deferred timelines');

</script>
