<!doctype html>
<meta charset=utf-8>
<title>Scroll based animation: AnimationEffect.updateTiming</title>
<!-- Adapted to progressed based scroll animations from "wpt\web-animations\interfaces\AnimationEffect\updateTiming.html" -->
<link rel="help" href="https://drafts.csswg.org/web-animations-1/#dom-animationeffect-updatetiming">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/web-animations/testcommon.js"></script>
<script src="testcommon.js"></script>
<script src="/web-animations/resources/easing-tests.js"></script>
<script src="/web-animations/resources/timing-tests.js"></script>
<style>
  .scroller {
    overflow: auto;
    height: 100px;
    width: 100px;
    will-change: transform;
  }
  .contents {
    height: 1000px;
    width: 100%;
  }
</style>
<body>
<div id="log"></div>
<script>
'use strict';

// ------------------------------
//  delay
// ------------------------------

promise_test(async t => {
  const anim = createScrollLinkedAnimationWithTiming(t, {duration: 1000, delay: 200})
  anim.play();

  assert_equals(anim.effect.getTiming().delay, 200, 'initial delay 200');
  assert_equals(anim.effect.getComputedTiming().delay, 200,
                'getComputedTiming() initially delay 200');

  anim.effect.updateTiming({ delay: 100 });
  assert_equals(anim.effect.getTiming().delay, 100, 'set delay 100');
  assert_equals(anim.effect.getComputedTiming().delay, 100,
                'getComputedTiming() after set delay 100');
}, 'Allows setting the delay to a positive number');

test(t => {
  const anim = createScrollLinkedAnimationWithTiming(t, {duration: 100, delay: -100})
  anim.play();
  anim.effect.updateTiming({ delay: -100 });
  assert_equals(anim.effect.getTiming().delay, -100, 'set delay -100');
  assert_equals(anim.effect.getComputedTiming().delay, -100,
                'getComputedTiming() after set delay -100');
  assert_percents_equal(anim.effect.getComputedTiming().endTime, 0,
                        'getComputedTiming().endTime after set delay -100');
}, 'Allows setting the delay to a negative number');

promise_test(async t => {
  const anim = createScrollLinkedAnimationWithTiming(t, {duration: 100})
  anim.play();
  await anim.ready;
  anim.effect.updateTiming({ delay: 100 });
  assert_equals(anim.effect.getComputedTiming().progress, null);
  assert_equals(anim.effect.getComputedTiming().currentIteration, null);
}, 'Allows setting the delay of an animation in progress: positive delay that'
   + ' causes the animation to be no longer in-effect');

promise_test(async t => {
  const anim =
      createScrollLinkedAnimationWithTiming(t, { fill: 'both', duration: 100 });
  anim.play();
  await anim.ready;
  anim.effect.updateTiming({ delay: -50 });
  assert_equals(anim.effect.getComputedTiming().progress, 0.5);
}, 'Allows setting the delay of an animation in progress: negative delay that'
   + ' seeks into the active interval');

promise_test(async t => {
  const anim =
      createScrollLinkedAnimationWithTiming(t, { fill: 'both', duration: 100 });
  anim.play();
  await anim.ready;
  anim.effect.updateTiming({ delay: -100 });
  assert_equals(anim.effect.getComputedTiming().progress, 1);
  assert_equals(anim.effect.getComputedTiming().currentIteration, 0);
}, 'Allows setting the delay of an animation in progress: large negative delay'
   + ' that causes the animation to be finished');

for (const invalid of gBadDelayValues) {
  test(t => {
  const anim = createScrollLinkedAnimationWithTiming(t)
  anim.play();
    assert_throws_js(TypeError, () => {
      anim.effect.updateTiming({ delay: invalid });
    });
  }, `Throws when setting invalid delay value: ${invalid}`);
}


// ------------------------------
//  endDelay
// ------------------------------

promise_test(async t => {
  const anim = createScrollLinkedAnimationWithTiming(t, { duration: 2000 });
  anim.play();
  await anim.ready;
  anim.effect.updateTiming({ endDelay: 123.45 });
  assert_time_equals_literal(anim.effect.getTiming().endDelay, 123.45,
                             'set endDelay 123.45');
  assert_time_equals_literal(anim.effect.getComputedTiming().endDelay, 123.45,
                             'getComputedTiming() after set endDelay 123.45');
}, 'Allows setting the endDelay to a positive number');

promise_test(async t => {
  const anim = createScrollLinkedAnimationWithTiming(t, { duration: 2000 });
  anim.play();
  await anim.ready;
  anim.effect.updateTiming({ endDelay: -1000 });
  assert_equals(anim.effect.getTiming().endDelay, -1000, 'set endDelay -1000');
  assert_equals(anim.effect.getComputedTiming().endDelay, -1000,
                'getComputedTiming() after set endDelay -1000');
}, 'Allows setting the endDelay to a negative number');

promise_test(async t => {
  const anim = createScrollLinkedAnimationWithTiming(t, { duration: 2000 });
  anim.play();
  await anim.ready;
  assert_throws_js(TypeError, () => {
    anim.effect.updateTiming({ endDelay: Infinity });
  });
}, 'Throws when setting the endDelay to infinity');

promise_test(async t => {
  const anim = createScrollLinkedAnimationWithTiming(t, { duration: 2000 });
  anim.play();
  await anim.ready;
  assert_throws_js(TypeError, () => {
    anim.effect.updateTiming({ endDelay: -Infinity });
  });
}, 'Throws when setting the endDelay to negative infinity');


// ------------------------------
//  fill
// ------------------------------

for (const fill of ['none', 'forwards', 'backwards', 'both']) {
  test(t => {
  const anim = createScrollLinkedAnimationWithTiming(t, { duration: 100 })
  anim.play();
    anim.effect.updateTiming({ fill });
    assert_equals(anim.effect.getTiming().fill, fill, 'set fill ' + fill);
    assert_equals(anim.effect.getComputedTiming().fill, fill,
                  'getComputedTiming() after set fill ' + fill);
  }, `Allows setting the fill to '${fill}'`);
}


// ------------------------------
//  iterationStart
// ------------------------------

promise_test(async t => {
  const anim = createScrollLinkedAnimationWithTiming(t, { iterationStart: 0.2,
                                      iterations: 1,
                                      fill: 'both',
                                      duration: 100,
                                      delay: 1 })
  anim.play();
  await anim.ready;
  anim.effect.updateTiming({ iterationStart: 2.5 });
  assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.5);
  assert_equals(anim.effect.getComputedTiming().currentIteration, 2);
}, 'Allows setting the iterationStart of an animation in progress:'
   + ' backwards-filling');

promise_test(async t => {
  const anim = createScrollLinkedAnimationWithTiming(t, { iterationStart: 0.2,
                                      iterations: 1,
                                      fill: 'both',
                                      duration: 100,
                                      delay: 0 })
  anim.play();
  await anim.ready;
  anim.effect.updateTiming({ iterationStart: 2.5 });
  assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.5);
  assert_equals(anim.effect.getComputedTiming().currentIteration, 2);
}, 'Allows setting the iterationStart of an animation in progress:'
   + ' active phase');

promise_test(async t => {
  const anim = createScrollLinkedAnimationWithTiming(t, { iterationStart: 0.3,
                                      iterations: 1,
                                      fill: 'both',
                                      duration: 200,
                                      delay: 0 })
  anim.play();
  await anim.ready;
  assert_percents_equal(anim.currentTime, 0);
  assert_percents_equal(anim.effect.getComputedTiming().localTime, 0,
                        "localTime");
  assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.3);
  assert_equals(anim.effect.getComputedTiming().currentIteration, 0);

  anim.finish();
  assert_percents_equal(anim.currentTime, 100);
  assert_percents_equal(anim.effect.getComputedTiming().localTime, 100,
                        "localTime");
  assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.3);
  assert_equals(anim.effect.getComputedTiming().currentIteration, 1);

  anim.effect.updateTiming({ iterationStart: 2.5 });
  assert_percents_equal(anim.currentTime, 100);
  assert_percents_equal(anim.effect.getComputedTiming().localTime, 100,
                        "localTime");
  assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.5);
  assert_equals(anim.effect.getComputedTiming().currentIteration, 3);
}, 'Allows setting the iterationStart of an animation in progress:'
   + ' forwards-filling');

for (const invalid of gBadIterationStartValues) {
  test(t => {
    const anim = createScrollLinkedAnimationWithTiming(t)
    anim.play();
    assert_throws_js(TypeError, () => {
      anim.effect.updateTiming({ iterationStart: invalid });
    }, `setting ${invalid}`);
  }, `Throws when setting invalid iterationStart value: ${invalid}`);
}

// ------------------------------
//  iterations
// ------------------------------

test(t => {
  const anim = createScrollLinkedAnimationWithTiming(t, { duration: 2000 });
  anim.play();
  anim.effect.updateTiming({ iterations: 2 });
  assert_equals(anim.effect.getTiming().iterations, 2, 'set duration 2');
  assert_equals(anim.effect.getComputedTiming().iterations, 2,
                'getComputedTiming() after set iterations 2');
}, 'Allows setting iterations to a double value');

test(t => {
  const anim = createScrollLinkedAnimationWithTiming(t, { duration: 2000 });
  anim.play();
  assert_throws_js(TypeError, () => {
    anim.effect.updateTiming({ iterations: Infinity });
  }, "test");
}, `Throws when setting iterations to Infinity`);


// progress based animations behave a bit differently than time based animations
// when changing iterations.
test(t => {
  const anim =
      createScrollLinkedAnimationWithTiming(
          t, { duration: 100000, fill: 'both' });
  anim.play();
  anim.finish();

  assert_equals(anim.effect.getComputedTiming().progress, 1,
                'progress when animation is finished');
  assert_percents_equal(anim.effect.getComputedTiming().duration, 100,
                        'duration when animation is finished');
  assert_equals(anim.effect.getComputedTiming().currentIteration, 0,
                'current iteration when animation is finished');

  anim.effect.updateTiming({ iterations: 2 });

  assert_equals(anim.effect.getComputedTiming().progress, 1,
                'progress after adding an iteration');
  assert_percents_equal(anim.effect.getComputedTiming().duration, 50,
                        'duration after adding an iteration');
  assert_equals(anim.effect.getComputedTiming().currentIteration, 1,
                'current iteration after adding an iteration');

  anim.effect.updateTiming({ iterations: 4 });

  assert_equals(anim.effect.getComputedTiming().progress, 1,
                'progress after setting iterations to 4');
  assert_percents_equal(anim.effect.getComputedTiming().duration, 25,
                        'duration after setting iterations to 4');
  assert_equals(anim.effect.getComputedTiming().currentIteration, 3,
                'current iteration after setting iterations to 4');

  anim.effect.updateTiming({ iterations: 0 });

  assert_equals(anim.effect.getComputedTiming().progress, 0,
                'progress after setting iterations to zero');
  assert_percents_equal(anim.effect.getComputedTiming().duration, 0,
                        'duration after setting iterations to zero');
  assert_equals(anim.effect.getComputedTiming().currentIteration, 0,
                'current iteration after setting iterations to zero');
}, 'Allows setting the iterations of an animation in progress');

// Added test for checking duration "auto"
test(t => {
  const anim = createScrollLinkedAnimationWithTiming(t,  { fill: 'both' });
  anim.play();
  anim.finish();

  assert_equals(anim.effect.getComputedTiming().progress, 1,
                'progress when animation is finished');
  assert_percents_equal(anim.effect.getComputedTiming().duration, 100,
                        'duration when animation is finished');
  assert_equals(anim.effect.getComputedTiming().currentIteration, 0,
                'current iteration when animation is finished');

  anim.effect.updateTiming({ iterations: 2 });

  assert_equals(anim.effect.getComputedTiming().progress, 1,
                'progress after adding an iteration');
  assert_percents_equal(anim.effect.getComputedTiming().duration, 50,
                        'duration after adding an iteration');
  assert_equals(anim.effect.getComputedTiming().currentIteration, 1,
                'current iteration after adding an iteration');

  anim.effect.updateTiming({ iterations: 4 });

  assert_equals(anim.effect.getComputedTiming().progress, 1,
                'progress after setting iterations to 4');
  assert_percents_equal(anim.effect.getComputedTiming().duration, 25,
                        'duration after setting iterations to 4');
  assert_equals(anim.effect.getComputedTiming().currentIteration, 3,
                'current iteration after setting iterations to 4');

  anim.effect.updateTiming({ iterations: 0 });

  assert_equals(anim.effect.getComputedTiming().progress, 0,
                'progress after setting iterations to zero');
  assert_percents_equal(anim.effect.getComputedTiming().duration, 0,
                        'duration after setting iterations to zero');
  assert_equals(anim.effect.getComputedTiming().currentIteration, 0,
                'current iteration after setting iterations to zero');
}, 'Allows setting the iterations of an animation in progress with duration ' +
   '"auto"');


// ------------------------------
//  duration
// ------------------------------
// adapted for progress based animations
const gGoodDurationValuesForProgressBased = [
  // until duration returns a CSSNumberish which can handle percentages, 100%
  // will be represented as 100
  { specified: 123.45, computed: 100 },
  { specified: 'auto', computed: 100 },
];

for (const duration of gGoodDurationValuesForProgressBased) {
  test(t => {
    const anim = createScrollLinkedAnimationWithTiming(t, 2000);
    anim.play();
    anim.effect.updateTiming({ duration: duration.specified });
    if (typeof duration.specified === 'number') {
      assert_time_equals_literal(anim.effect.getTiming().duration,
                                 duration.specified,
                                 'Updates specified duration');
    } else {
      assert_equals(anim.effect.getTiming().duration, duration.specified,
                    'Updates specified duration');
    }
    assert_percents_equal(anim.effect.getComputedTiming().duration,
                          duration.computed,
                          'Updates computed duration');
  }, `Allows setting the duration to ${duration.specified}`);
}

// adapted for progress based animations
const gBadDurationValuesForProgressBased = [
  -1, NaN, Infinity, -Infinity, 'abc', '100'
];

for (const invalid of gBadDurationValuesForProgressBased) {
  test(t => {
    assert_throws_js(TypeError, () => {
      const anim = createScrollLinkedAnimationWithTiming(t,  { duration: invalid })
      anim.play();
    });
  }, 'Throws when setting invalid duration: '
     + (typeof invalid === 'string' ? `"${invalid}"` : invalid));
}

test(t => {
  const anim =
      createScrollLinkedAnimationWithTiming(
          t, { duration: 100000, fill: 'both' });
  anim.play();
  anim.finish();
  assert_equals(anim.effect.getComputedTiming().progress, 1,
                'progress when animation is finished');
  anim.effect.updateTiming({ duration: anim.effect.getTiming().duration * 2 });
  assert_time_equals_literal(anim.effect.getComputedTiming().progress, 1,
                             'progress after doubling the duration');
  anim.effect.updateTiming({ duration: 0 });
  assert_equals(anim.effect.getComputedTiming().progress, 1,
                'progress after setting duration to zero');
  anim.effect.updateTiming({ duration: 'auto' });
  assert_equals(anim.effect.getComputedTiming().progress, 1,
                'progress after setting duration to \'auto\'');
}, 'Allows setting the duration of an animation in progress');

promise_test(t => {
  const anim =
      createScrollLinkedAnimationWithTiming(
          t,  { duration: 100000, fill: 'both' });
  anim.play();
  return anim.ready.then(() => {
    const originalStartTime   = anim.startTime;
    const originalCurrentTime = anim.currentTime;
    assert_percents_equal(
      anim.effect.getComputedTiming().duration,
      100,
      'Initial duration should be as set on KeyframeEffect'
    );

    anim.effect.updateTiming({ duration: 200000 });
    assert_percents_equal(
      anim.effect.getComputedTiming().duration,
      100,
      'Effect duration should remain at 100% for progress based animations'
    );
    assert_percents_equal(anim.startTime, originalStartTime,
                          'startTime should be unaffected by changing effect ' +
                          'duration');

    assert_percents_equal(anim.currentTime, originalCurrentTime,
                          'currentTime should be unaffected by changing ' +
                          'effect duration');
  });
}, 'Allows setting the duration of an animation in progress such that the' +
   ' the start and current time do not change');


// ------------------------------
//  direction
// ------------------------------

test(t => {
  const anim = createScrollLinkedAnimationWithTiming(t, { duration: 2000 });
  anim.play();

  const directions = ['normal', 'reverse', 'alternate', 'alternate-reverse'];
  for (const direction of directions) {
    anim.effect.updateTiming({ direction: direction });
    assert_equals(anim.effect.getTiming().direction, direction,
                  `set direction to ${direction}`);
  }
}, 'Allows setting the direction to each of the possible keywords');

promise_test(async t => {
  const anim =
      createScrollLinkedAnimationWithTiming(
          t, { duration: 10000, direction: 'normal' });

  const scroller = anim.timeline.source;
  const maxScroll = scroller.scrollHeight - scroller.clientHeight;
  anim.play();
  await anim.ready;
  scroller.scrollTop = maxScroll * 0.07
  await waitForNextFrame();

  assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.07,
                             'progress before updating direction');

  anim.effect.updateTiming({ direction: 'reverse' });

  assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.93,
                             'progress after updating direction');
}, 'Allows setting the direction of an animation in progress from \'normal\' ' +
   'to \'reverse\'');

promise_test(async t => {
  const anim =
      createScrollLinkedAnimationWithTiming(
          t, { duration: 10000, direction: 'normal' });
  anim.play();
  await anim.ready;
  assert_equals(anim.effect.getComputedTiming().progress, 0,
                'progress before updating direction');

  anim.effect.updateTiming({ direction: 'reverse' });

  assert_equals(anim.effect.getComputedTiming().progress, 1,
                'progress after updating direction');
}, 'Allows setting the direction of an animation in progress from \'normal\' to'
   + ' \'reverse\' while at start of active interval');

promise_test(async t => {
  const anim = createScrollLinkedAnimationWithTiming(t, { fill: 'backwards',
                                      duration: 10000,
                                      delay: 10000,
                                      direction: 'normal' });
  anim.play();
  await anim.ready;
  assert_equals(anim.effect.getComputedTiming().progress, 0,
                'progress before updating direction');

  anim.effect.updateTiming({ direction: 'reverse' });

  assert_equals(anim.effect.getComputedTiming().progress, 1,
                'progress after updating direction');
}, 'Allows setting the direction of an animation in progress from \'normal\' to'
   + ' \'reverse\' while filling backwards');

promise_test(async t => {
  const anim = createScrollLinkedAnimationWithTiming(t, { iterations: 2,
                                      duration: 10000,
                                      direction: 'normal' });
  const scroller = anim.timeline.source;
  const maxScroll = scroller.scrollHeight - scroller.clientHeight;
  anim.play();
  await anim.ready;
  scroller.scrollTop = maxScroll * 0.17 // 34% through first iteration
  await waitForNextFrame();

  assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.34,
                             'progress before updating direction');

  anim.effect.updateTiming({ direction: 'alternate' });

  assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.34,
                             'progress after updating direction');
}, 'Allows setting the direction of an animation in progress from \'normal\' to'
   + ' \'alternate\'');

promise_test(async t => {
  const anim = createScrollLinkedAnimationWithTiming(t, { iterations: 2,
                                      duration: 10000,
                                      direction: 'alternate' });
  const scroller = anim.timeline.source;
  const maxScroll = scroller.scrollHeight - scroller.clientHeight;
  anim.play();
  await anim.ready;
  scroller.scrollTop = maxScroll * 0.17
  await waitForNextFrame();
  // anim.currentTime = 17000; // 34% through first iteration
  assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.34,
                             'progress before updating direction');

  anim.effect.updateTiming({ direction: 'alternate-reverse' });

  assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.66,
                             'progress after updating direction');
}, 'Allows setting the direction of an animation in progress from \'alternate\''
   + ' to \'alternate-reverse\'');


// ------------------------------
//  easing
// ------------------------------

async function assert_progress(animation, scrollPercent, easingFunction) {
  const scroller = animation.timeline.source;
  const maxScroll = scroller.scrollHeight - scroller.clientHeight;
  scroller.scrollTop = maxScroll * scrollPercent
  await waitForNextFrame();
  assert_approx_equals(animation.effect.getComputedTiming().progress,
                       easingFunction(scrollPercent),
                       0.01,
                       'The progress of the animation should be approximately'
                       + ` ${easingFunction(scrollPercent)} at ${scrollPercent}%`);
}

for (const options of gEasingTests) {
  promise_test(async t => {
    const anim = createScrollLinkedAnimationWithTiming(t, { duration: 100000,
                                  fill: 'forwards' });
    anim.play();
    anim.effect.updateTiming({ easing: options.easing });
    assert_equals(anim.effect.getTiming().easing,
                  options.serialization || options.easing);

    const easing = options.easingFunction;
    await assert_progress(anim, 0, easing);
    await assert_progress(anim, 0.25, easing);
    await assert_progress(anim, 0.5, easing);
    await assert_progress(anim, 0.75, easing);
    await assert_progress(anim, 1, easing);
  }, `Allows setting the easing to a ${options.desc}`);
}

for (const easing of gRoundtripEasings) {
  test(t => {
    const anim = createScrollLinkedAnimationWithTiming(t);
    anim.play();
    anim.effect.updateTiming({ easing: easing });
    assert_equals(anim.effect.getTiming().easing, easing);
  }, `Updates the specified value when setting the easing to '${easing}'`);
}

// Because of the delay being so large, this progress based animation is always
// in the finished state with progress 1. Included here because it is in the
// original test file for time based animations.
promise_test(async t => {
  const delay = 1000000;

  const anim = createScrollLinkedAnimationWithTiming(t,
    { duration: 1000000, fill: 'both', delay: delay, easing: 'steps(2, start)' });

  const scroller = anim.timeline.source;
  const maxScroll = scroller.scrollHeight - scroller.clientHeight;
  anim.play();
  await anim.ready;

  anim.effect.updateTiming({ easing: 'steps(2, end)' });
  assert_equals(anim.effect.getComputedTiming().progress, 0,
                'easing replace to steps(2, end) at before phase');

  scroller.scrollTop = maxScroll * 0.875
  await waitForNextFrame();

  assert_equals(anim.effect.getComputedTiming().progress, 0.5,
                'change currentTime to active phase');

  anim.effect.updateTiming({ easing: 'steps(2, start)' });
  assert_equals(anim.effect.getComputedTiming().progress, 1,
                'easing replace to steps(2, start) at active phase');

  scroller.scrollTop = maxScroll * 1.25
  await waitForNextFrame();

  anim.effect.updateTiming({ easing: 'steps(2, end)' });
  assert_equals(anim.effect.getComputedTiming().progress, 1,
                'easing replace to steps(2, end) again at after phase');
}, 'Allows setting the easing of an animation in progress');
</script>
</body>
