<!DOCTYPE html>
<meta charset=utf-8>
<title>Verify timeline time, animation time, effect time, and effect progress for all timeline states: before start, at start, in range, at end, after end while using various effect delay values</title>
<meta name="timeout" content="long">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/web-animations/testcommon.js"></script>
<script src="testcommon.js"></script>
<style>
  .scroller {
    overflow: hidden;
    height: 200px;
    width: 200px;
  }
  .contents {
    /* Make scroll range 1000 to simplify the math and avoid rounding errors */
    height: 1200px;
    width: 100%;
  }
</style>
<div id="log"></div>
<script>
  'use strict';
 // Note: effects are scaled to fill the timeline.

  // Each entry is [[test input], [test expectations]]
  // test input = ["description", delay, end_delay, scroll percent]
  // test expectations = [timeline time, animation current time,
  //                      effect local time, effect progress, effect phase,
  //                      opacity]

  /* All interesting transitions:
      at timeline start
      before effect delay
      at effect start
      in active range
      at effect end
      after effect end
      at timeline end
  */
  const test_cases = [
    // Case 1: No delays.
    // Boundary at end of active phase is inclusive.
    [
      ["at start", 0, 0, 0],
      [0, 0, 0, 0, "active", 0.3]
    ],
    [
      ["in active range", 0, 0, 0.50],
      [50, 50, 50, 0.5, "active", 0.5]
    ],
    [
      ["at effect end time", 0, 0, 1.0],
      [100, 100, 100, 1.0, "active", 0.7]
    ],

    // Case 2: Positive start delay and no end delay.
    // Boundary at end of active phase is inclusive.
    [
      ["at timeline start", 500, 0, 0],
      [0, 0, 0, null, "before", 1]
    ],
    [
      ["before start delay", 500, 0, 0.25],
      [25, 25, 25, null, "before", 1]
    ],
    [
      ["at start delay", 500, 0, 0.5],
      [50, 50, 50, 0, "active", 0.3]
    ],
    [
      ["in active range", 500, 0, 0.75],
      [75, 75, 75, 0.5, "active", 0.5]
    ],
    [
      ["at effect end time", 500, 0, 1.0],
      [100, 100, 100, 1.0, "active", 0.7]
    ],

    // case 3: No start delay, Positive end delay.
    // Boundary at end of active phase is exclusive.
    [
      ["at timeline start", 0, 500, 0],
      [0, 0, 0, 0, "active", 0.3]
    ],
    [
      ["in active range", 0, 500, 0.25],
      [25, 25, 25, 0.5, "active", 0.5]
    ],
    [
      ["at effect end time", 0, 500, 0.5],
      [50, 50, 50, null, "after", 1.0]
    ],
    [
      ["after effect end time", 0, 500, 0.75],
      [75, 75, 75, null, "after", 1.0]
    ],
    [
      ["at timeline boundary", 0, 500, 1.0],
      [100, 100, 100, null, "after", 1.0]
    ],

    // case 4: Positive start and end delays.
    // Boundary at end of active phase is exclusive.
    [
      ["at timeline start", 250, 250, 0],
      [0, 0, 0, null, "before", 1]
    ],
    [
      ["before start delay", 250, 250, 0.1],
      [10, 10, 10, null, "before", 1]
    ],
    [
      ["at start delay", 250, 250, 0.25],
      [25, 25, 25, 0, "active", 0.3]
    ],
    [
      ["in active range", 250, 250, 0.5],
      [50, 50, 50, 0.5, "active", 0.5]
    ],
    [
      ["at effect end time", 250, 250, 0.75],
      [75, 75, 75, null, "after", 1.0]
    ],
    [
      ["after effect end time", 250, 250, 0.9],
      [90, 90, 90, null, "after", 1.0]
    ],
    [
      ["at timeline boundary", 250, 250, 1.0],
      [100, 100, 100, null, "after", 1.0]
    ],

    // Case 5: Negative start and end delays.
    // Effect boundaries are not reachable.
    [
      ["at timeline start", -125, -125, 0],
      [0, 0, 0, 0.25, "active", 0.4]
    ],
    [
      ["in active range", -125, -125, 0.5],
      [50, 50, 50, 0.5, "active", 0.5]
    ],
    [
      ["at timeline end", -125, -125, 1.0],
      [100, 100, 100, 0.75, "active", 0.6]
    ]
  ];

  for (const test_case of test_cases) {
    const [inputs, expected] = test_case;
    const [test_name, delay, end_delay, scroll_percentage] = inputs;

    const description = `Current times and effect phase ${test_name} when` +
      ` delay = ${delay} and endDelay = ${end_delay} |`;

    promise_test(
        create_scroll_timeline_delay_test(
            delay, end_delay, scroll_percentage, expected),
        description);
  }

  function create_scroll_timeline_delay_test(
      delay, end_delay, scroll_percentage, expected){
    return async t => {
      const target = createDiv(t);
      const timeline = createScrollTimeline(t);
      const effect = new KeyframeEffect(
        target,
        {
          opacity: [0.3, 0.7]
        },
        {
          duration: 500,
          delay: delay,
          endDelay: end_delay
        }
      );
      const animation = new Animation(effect, timeline);
      t.add_cleanup(() => {
        animation.cancel();
      });
      const scroller = timeline.source;
      const maxScroll = scroller.scrollHeight - scroller.clientHeight;

      animation.play();

      await animation.ready;

      scroller.scrollTop = scroll_percentage * maxScroll;

      // Wait for new animation frame which allows the timeline to compute
      // new current time.
      await waitForNextFrame();

      const [expected_timeline_current_time,
          expected_animation_current_time,
          expected_effect_local_time,
          expected_effect_progress,
          expected_effect_phase,
          expected_opacity] = expected;

      assert_percents_equal(
          animation.timeline.currentTime,
          expected_timeline_current_time,
          "timeline current time");
      assert_percents_equal(
          animation.currentTime,
          expected_animation_current_time,
          "animation current time");
      assert_percents_equal(
          animation.effect.getComputedTiming().localTime,
          expected_effect_local_time,
          "animation effect local time");
      assert_approx_equals_or_null(
          animation.effect.getComputedTiming().progress,
          expected_effect_progress,
          0.001,
          "animation effect progress");
      assert_phase(
          animation, expected_effect_phase);
      assert_approx_equals(
          parseFloat(getComputedStyle(target).opacity), expected_opacity,
          0.001,
          'target opacity');
    }
  }

  function createKeyframeEffectOpacity(test){
    return new KeyframeEffect(
      createDiv(test),
      {
        opacity: [0.3, 0.7]
      },
      {
        duration: 1000
      }
    );
  }

  function verifyEffectBeforePhase(animation) {
    // If currentTime is null, we are either idle, or running with an
    // inactive timeline. Either way, the animation is not in effect and cannot
    // be in the before phase.
    assert_true(animation.currentTime != null,
                'Animation is not in effect');

    const fillMode = animation.effect.getTiming().fill;
    animation.effect.updateTiming({ fill: 'none' });

    // progress == null AND opacity == 1 implies we are in the effect before
    // or after phase.
    assert_equals(animation.effect.getComputedTiming().progress, null);
    assert_equals(
        window.getComputedStyle(animation.effect.target)
            .getPropertyValue("opacity"),
        "1");

    // If the progress is no longer null after adding fill: backwards, then we
    // are in the before phase.
    animation.effect.updateTiming({ fill: 'backwards' });
    assert_true(animation.effect.getComputedTiming().progress != null);
    assert_equals(
        window.getComputedStyle(animation.effect.target)
            .getPropertyValue("opacity"),
        "0.3");

    // Reset fill mode to avoid side-effects.
    animation.effect.updateTiming({ fill: fillMode });
  }

  function createScrollLinkedOpacityAnimationWithDelays(t) {
    const animation = new Animation(
      createKeyframeEffectOpacity(t),
      createScrollTimeline(t)
    );
    t.add_cleanup(() => {
      animation.cancel();
    });
    animation.effect.updateTiming({
       duration: 1000,
       delay: 500,
       endDelay: 500
    });
    return animation;
  }


  promise_test(async t => {
    const animation = createScrollLinkedOpacityAnimationWithDelays(t);
    const scroller = animation.timeline.source;
    const maxScroll = scroller.scrollHeight - scroller.clientHeight;

    // scroll pos
    // current time
    // start time
    // |
    // |---- 25% before ----|----  50% active ----|---- 25% after ----|
    animation.play();
    await animation.ready;
    assert_percents_equal(animation.startTime, 0);
    assert_phase(animation, 'before');

    // start time                 scroll pos
    // |                          current time
    // |                              |
    // |---- 25% before ----|----  50% active ----|---- 25% after ----|
    scroller.scrollTop = 0.5 * maxScroll;
    await waitForNextFrame();
    assert_phase(animation, 'active');

    // start time                scroll pos                      current time
    // |                              |                               |
    // |---- 25% before ----|----  50% active ----|---- 25% after ----|
    animation.playbackRate = 2;
    assert_phase(animation, 'after');

    // start time                scroll pos                      current time
    // |                              |                                |
    // |---- 33.3% before ----|----  66.7% active ---------------------|
    animation.effect.updateTiming({ endDelay: 0 });
    assert_phase(animation, 'active');

    //                           scroll pos                        start time
    //                           current time                           |
    //                                |                                 |
    // |---- 33.3% before ----|----  66.7% active ----------------------|
    animation.playbackRate = -1;
    assert_percents_equal(animation.startTime, 100);
    assert_phase(animation, 'active');

    //                                                             start time
    //                             scroll pos                     current time
    // |                               |                                  |
    // |---- 33.3% before  ----|----  66.7% active -----------------------|
    animation.playbackRate = -2;
    assert_phase(animation, 'active');

    // current time                                                  start time
    // |                                                             scroll pos
    // |                                                                  |
    // |---- 33.3% before  ----|----  66.7% active -----------------------|
    scroller.scrollTop = maxScroll;
    await waitForNextFrame();
    assert_phase(animation, 'before');

    // current time                                                  start time
    // |                                                             scroll pos
    // |                                                                  |
    // |---------------------  100% active -------------------------------|
    animation.effect.updateTiming({ delay: 0 });
    assert_phase(animation, 'active');

    // Finally, switch to a document timeline.  The before-active boundary
    // becomes exclusive.
    animation.timeline = document.timeline;
    animation.currentTime = 0;
    await waitForNextFrame();
    assert_phase(animation, 'before');

  }, 'Playback rate affects whether active phase boundary is inclusive.');

  promise_test(async t => {
    const animation = createScrollLinkedOpacityAnimationWithDelays(t);
    const scroller = animation.timeline.source;
    const maxScroll = scroller.scrollHeight - scroller.clientHeight;

    animation.play();
    await animation.ready;
    verifyEffectBeforePhase(animation);

    animation.pause();
    await waitForNextFrame();
    verifyEffectBeforePhase(animation);

    animation.play();
    await waitForNextFrame();

    verifyEffectBeforePhase(animation);
  }, 'Verify that (play -> pause -> play) doesn\'t change phase/progress.');

  promise_test(async t => {
    const animation = createScrollLinkedOpacityAnimationWithDelays(t);
    const scroller = animation.timeline.source;
    const maxScroll = scroller.scrollHeight - scroller.clientHeight;

    animation.play();
    await animation.ready;
    verifyEffectBeforePhase(animation);

    animation.pause();
    await animation.ready;
    verifyEffectBeforePhase(animation);

    // Scrolling should not cause the animation effect to change.
    scroller.scrollTop = 0.5 * maxScroll;
    await waitForNextFrame();

    // Check timeline phase
    assert_percents_equal(animation.timeline.currentTime, 50);
    assert_percents_equal(animation.currentTime, 0);
    assert_percents_equal(animation.effect.getComputedTiming().localTime, 0,
        "effect local time");

    // Make sure the effect is still in the before phase even though the
    // timeline is not.
    verifyEffectBeforePhase(animation);
  }, 'Pause in before phase, scroll timeline into active phase, animation ' +
     'should remain in the before phase');

  promise_test(async t => {
    const animation = createScrollLinkedOpacityAnimationWithDelays(t);
    const scroller = animation.timeline.source;
    const maxScroll = scroller.scrollHeight - scroller.clientHeight;

    animation.play();
    await animation.ready;
    verifyEffectBeforePhase(animation);

    animation.pause();
    await waitForNextFrame();
    verifyEffectBeforePhase(animation);

    // Setting the current time should force the animation into effect.
    const expected_time = 50;
    animation.currentTime = CSS.percent(expected_time);
    await waitForNextFrame();
    assert_percents_equal(animation.timeline.currentTime, 0);
    assert_percents_equal(animation.currentTime, expected_time,
                          'Current time matches set value');
    assert_percents_equal(
        animation.effect.getComputedTiming().localTime,
        expected_time, "Effect local time after setting animation.currentTime");
    assert_equals(animation.effect.getComputedTiming().progress, 0.5,
                  "Progress after setting animation.currentTime");
    assert_equals(
        window.getComputedStyle(animation.effect.target)
            .getPropertyValue("opacity"),
        "0.5", "Opacity after setting animation.currentTime");

    // Scrolling should not cause the animation effect to change since
    // paused.
    scroller.scrollTop = 0.75 * maxScroll; // scroll so that timeline is 75%
    await waitForNextFrame();
    assert_percents_equal(animation.timeline.currentTime, 75);

    // animation and effect timings are unchanged.
    assert_percents_equal(animation.currentTime, expected_time,
                          "Current time after scrolling while paused");
    assert_percents_equal(
        animation.effect.getComputedTiming().localTime,
        expected_time,
        "Effect local time after scrolling while paused");
    assert_equals(animation.effect.getComputedTiming().progress, 0.5,
                  "Progress after scrolling while paused");
    assert_equals(
        window.getComputedStyle(animation.effect.target)
            .getPropertyValue("opacity"),
        "0.5", "Opacity after scrolling while paused");
  }, 'Pause in before phase, set animation current time to be in active ' +
     'range, animation should become active. Scrolling should have no effect.');

  promise_test(async t => {
    const animation = createScrollLinkedOpacityAnimationWithDelays(t);
    const scroller = animation.timeline.source;
    const maxScroll = scroller.scrollHeight - scroller.clientHeight;

    animation.play();
    await animation.ready;

    // Causes the timeline to be inactive
    scroller.style.overflow = "visible";
    await waitForNextFrame();
    await waitForNextFrame();

    // Verify that he timeline is inactive
    assert_equals(animation.timeline.currentTime, null,
                  "Timeline is inactive");
    assert_equals(
        animation.currentTime, null,
        "Current time for running animation with an inactive timeline");
    assert_equals(animation.effect.getComputedTiming().localTime, null,
        "effect local time with inactive timeline");

    // Setting the current time while timeline is inactive should pause the
    // animation at the specified time.
    animation.currentTime = CSS.percent(50);
    await waitForNextFrame();
    await waitForNextFrame();

    // Verify that animation currentTime is properly set despite the inactive
    // timeline.
    assert_equals(animation.timeline.currentTime, null);
    assert_percents_equal(animation.currentTime, 50);
    assert_percents_equal(animation.effect.getComputedTiming().localTime, 50,
        "effect local time after setting animation current time");

    // Check effect phase
    // progress == 0.5 AND opacity == 0.5 shows we are in the effect active
    // phase.
    assert_equals(animation.effect.getComputedTiming().progress, 0.5,
                  "effect progress");
    assert_equals(
        window.getComputedStyle(animation.effect.target)
            .getPropertyValue("opacity"),
        "0.5",
        "effect opacity after setting animation current time");
  }, 'Make scroller inactive, then set current time to an in range time');

  promise_test(async t => {
    const animation = createScrollLinkedOpacityAnimationWithDelays(t);
    const scroller = animation.timeline.source;
    const maxScroll = scroller.scrollHeight - scroller.clientHeight;
    scroller.scrollTop = 0.5 * maxScroll;
    // Update timeline.currentTime.
    await waitForNextFrame();

    animation.pause();
    await animation.ready;
    // verify effect is applied.
    const expected_progress = 0.5;
    assert_equals(
        animation.effect.getComputedTiming().progress,
        expected_progress,
        "Verify effect progress after pausing.");

    // cause the timeline to become inactive
    scroller.style.overflow = 'visible';
    await waitForAnimationFrames(2);
    assert_equals(animation.timeline.currentTime, null,
        'Sanity check the timeline is inactive.');
    assert_equals(
        animation.effect.getComputedTiming().progress,
        expected_progress,
        "Verify effect progress after the timeline goes inactive.");
  }, 'Animation effect is still applied after pausing and making timeline ' +
     'inactive.');

  promise_test(async t => {
    const animation = createScrollLinkedOpacityAnimationWithDelays(t);
    const scroller = animation.timeline.source;
    const maxScroll = scroller.scrollHeight - scroller.clientHeight;

    animation.play();
    await animation.ready;

    // cause the timeline to become inactive
    scroller.style.overflow = 'visible';

    scroller.scrollTop;

    animation.pause();
  }, 'Make timeline inactive, force style update then pause the animation. ' +
     'No crashing indicates test success.');
</script>
