<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#named-timeline-range">
<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>
<title>Animation range and delay</title>
</head>
<style type="text/css">
  #scroller {
    border:  10px solid lightgray;
    overflow-y: scroll;
    overflow-x: hidden;
    width: 300px;
    height: 200px;
  }
  #target {
    margin: 800px 10px;
    width: 100px;
    height: 100px;
    z-index: -1;
    background-color: green;
  }
</style>
<body>
  <div id=scroller>
    <div id=target></div>
  </div>
</body>
<script type="text/javascript">
  async function runTest() {
    function assert_progress_equals(anim, expected, errorMessage) {
      assert_approx_equals(
          anim.effect.getComputedTiming().progress,
          expected, 1e-6, errorMessage);
    }

    function assert_opacity_equals(expected, errorMessage) {
      assert_approx_equals(
          parseFloat(getComputedStyle(target).opacity), expected, 1e-6,
                     errorMessage);
    }

    async function runTimelineOffsetsInKeyframesTest(keyframes) {
      const testcase = JSON.stringify(keyframes);
      const anim = target.animate(keyframes, {
        timeline: new ViewTimeline( { subject: target }),
        rangeStart: { rangeName: 'contain', offset: CSS.percent(0) },
        rangeEnd: { rangeName: 'contain', offset: CSS.percent(100) },
        duration: 'auto', fill: 'both'
      });
      await anim.ready;
      await waitForNextFrame();

      // @ contain 0%
      scroller.scrollTop = 700;
      await waitForNextFrame();

      assert_progress_equals(
          anim, 0, `Testcase '${testcase}': progress at contain 0%`);
      assert_opacity_equals(
          1/3, `Testcase '${testcase}': opacity at contain 0%`);

      // @ contain 50%
      scroller.scrollTop = 750;
      await waitForNextFrame();
      assert_progress_equals(
          anim, 0.5, `Testcase '${testcase}': progress at contain 50%`);
      assert_opacity_equals(
          0.5, `Testcase '${testcase}': opacity at contain 50%`);

      // @ contain 100%
      scroller.scrollTop = 800;
      await waitForNextFrame();
      assert_progress_equals(
          anim, 1, `Testcase '${testcase}': progress at contain 100%`);
      assert_opacity_equals(
          2/3, `Testcase '${testcase}': opacity at contain 100%`);
      anim.cancel();
    }

    async function runParseNumberOrPercentInKeyframesTest(keyframes) {
      const anim = target.animate(keyframes, {
        timeline: new ViewTimeline( { subject: target }),
        rangeStart: { rangeName: 'contain', offset: CSS.percent(0) },
        rangeEnd: { rangeName: 'contain', offset: CSS.percent(100) },
        duration: 'auto', fill: 'both'
      });
      await anim.ready;
      await waitForNextFrame();

      const maxScroll = scroller.scrollHeight - scroller.clientHeight;
      scroller.scrollTop = maxScroll / 2;
      await waitForNextFrame();

      const testcase = JSON.stringify(keyframes);
      assert_progress_equals(anim, 0.5, testcase);
      assert_opacity_equals(0.5, testcase);
      anim.cancel();
    }

    async function runInvalidKeyframesTest(keyframes) {
      assert_throws_js(TypeError, () => {
        target.animate(keyframes, {
          timeline: new ViewTimeline( { subject: target }),
        });
      }, `Invalid keyframes test case "${JSON.stringify(keyframes)}"`);
    }

    promise_test(async t => {
      // Test equivalent typed-OM and CSS representations of timeline offsets.
      // Test array and object form for keyframes.
      const keyframeTests = [
        // BaseKeyframe form with offsets expressed as typed-OM.
        [
          {
            offset: { rangeName: 'cover', offset: CSS.percent(0) },
            opacity: 0
          },
          {
            offset: { rangeName: 'cover', offset: CSS.percent(100) },
            opacity: 1
          }
        ],
        // BaseKeyframe form with offsets expressed as CSS text.
        [
          { offset: "cover 0%", opacity: 0 },
          { offset: "cover 100%", opacity: 1 }
        ],
        // BasePropertyIndexedKeyframe form with offsets expressed as typed-OM.
        {
          opacity: [0, 1],
          offset: [
            { rangeName: 'cover', offset: CSS.percent(0) },
            { rangeName: 'cover', offset: CSS.percent(100) }
          ]
        },
        // BasePropertyIndexedKeyframe form with offsets expressed as CSS text.
        { opacity: [0, 1], offset: [ "cover 0%", "cover 100%" ]}
      ];

      for (let i = 0; i < keyframeTests.length; i++) {
        await runTimelineOffsetsInKeyframesTest(keyframeTests[i]);
      }

    }, 'Timeline offsets in programmatic keyframes');

    promise_test(async t => {
      const keyframeTests = [
        [{offset: "0.5", opacity: 0.5 }],
        [{offset: "50%", opacity: 0.5 }],
        [{offset: "calc(20% + 30%)", opacity: 0.5 }]
      ];

      for (let i = 0; i < keyframeTests.length; i++) {
        await runParseNumberOrPercentInKeyframesTest(keyframeTests[i]);
      }

    }, 'String offsets in programmatic keyframes');

    promise_test(async t => {
      const invalidKeyframeTests = [
        // BasePropertyKefyrame:
        [{ offset: { rangeName: 'somewhere', offset: CSS.percent(0) }}],
        [{ offset: { rangeName: 'entry', offset: CSS.px(0) }}],
        [{ offset: "here 0%" }],
        [{ offset: "entry 3px" }],
        // BasePropertyIndexedKeyframe with sequence:
        { offset: [{ rangeName: 'somewhere', offset: CSS.percent(0) }]},
        { offset: [{ rangeName: 'entry', offset: CSS.px(0) }]},
        { offset: ["here 0%"] },
        { offset: ["entry 3px" ]},
        // BasePropertyIndexedKeyframe without sequence:
        { offset: { rangeName: 'somewhere', offset: CSS.percent(0) }},
        { offset: { rangeName: 'entry', offset: CSS.px(0) }},
        { offset: "here 0%" },
        { offset: "entry 3px" },
        // <number> or <percent> as string:
        [{ offset: "-1" }],
        [{ offset: "2" }],
        [{ offset: "-10%" }],
        [{ offset: "110%" }],
        { offset: ["-1"], opacity: [0.5] },
        { offset: ["2"], opacity: [0.5] },
        { offset: "-1", opacity: 0.5 },
        { offset: "2", opacity: 0.5 },
        // Extra stuff at the end.
        [{ offset: "0.5 trailing nonsense" }],
        [{ offset: "cover 50% eureka" }]
      ];
      for( let i = 0; i < invalidKeyframeTests.length; i++) {
        await runInvalidKeyframesTest(invalidKeyframeTests[i]);
      }
    }, 'Invalid timeline offset in programmatic keyframe throws');


    promise_test(async t => {
      const anim = target.animate([
          { offset: "cover 0%", opacity: 0 },
          { offset: "cover 100%", opacity: 1 }
        ], {
        rangeStart: { rangeName: 'contain', offset: CSS.percent(0) },
        rangeEnd: { rangeName: 'contain', offset: CSS.percent(100) },
        duration: 10000, fill: 'both'
      });

      scroller.scrollTop = 750;

      await anim.ready;
      assert_opacity_equals(1, `Opacity with document timeline`);

      anim.timeline = new ViewTimeline( { subject: target });
      await anim.ready;

      assert_progress_equals(anim, 0.5, `Progress at contain 50%`);
      assert_opacity_equals(0.5, `Opacity at contain 50%`);

      anim.timeline = document.timeline;
      assert_false(anim.pending);
      await waitForNextFrame();
      assert_opacity_equals(1, `Opacity after resetting timeline`);

      anim.cancel();
    }, 'Timeline offsets in programmatic keyframes adjust for change in ' +
       'timeline');

    promise_test(async t => {
      const anim = target.animate([], {
          timeline: new ViewTimeline( { subject: target }),
          rangeStart: { rangeName: 'contain', offset: CSS.percent(0) },
          rangeEnd: { rangeName: 'contain', offset: CSS.percent(100) },
          duration: 'auto', fill: 'both'
        });

      await anim.ready;
      await waitForNextFrame();

      scroller.scrollTop = 750;
      await waitForNextFrame();
      assert_progress_equals(
          anim, 0.5, `Progress at contain 50% before effect change`);
      assert_opacity_equals(1, `Opacity at contain 50% before effect change`);

      anim.effect = new KeyframeEffect(target, [
          { offset: "cover 0%", opacity: 0 },
          { offset: "cover 100%", opacity: 1 }
        ], { duration: 'auto', fill: 'both' });
      await waitForNextFrame();
      assert_progress_equals(
          anim, 0.5, `Progress at contain 50% after effect change`);
      assert_opacity_equals(0.5, `Opacity at contain 50% after effect change`);
    }, 'Timeline offsets in programmatic keyframes resolved when updating ' +
       'the animation effect');
  }

  // TODO(kevers): Add tests for getKeyframes once
  // https://github.com/w3c/csswg-drafts/issues/8507 is resolved.

  window.onload = runTest;
</script>
</html>
