<!DOCTYPE html>
<meta charset=utf-8>
<title>Animation.finished</title>
<link rel="help" href="https://drafts.csswg.org/web-animations/#dom-animation-finished">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="../../testcommon.js"></script>
<body>
<div id="log"></div>
<script>
'use strict';

promise_test(t => {
  const div = createDiv(t);
  const animation = div.animate({}, 100 * MS_PER_SEC);
  const previousFinishedPromise = animation.finished;
  return animation.ready.then(() => {
    assert_equals(animation.finished, previousFinishedPromise,
                  'Finished promise is the same object when playing starts');
    animation.pause();
    assert_equals(animation.finished, previousFinishedPromise,
                  'Finished promise does not change when pausing');
    animation.play();
    assert_equals(animation.finished, previousFinishedPromise,
                  'Finished promise does not change when play() unpauses');

    animation.currentTime = 100 * MS_PER_SEC;

    return animation.finished;
  }).then(() => {
    assert_equals(animation.finished, previousFinishedPromise,
                  'Finished promise is the same object when playing completes');
  });
}, 'Test pausing then playing does not change the finished promise');

promise_test(t => {
  const div = createDiv(t);
  const animation = div.animate({}, 100 * MS_PER_SEC);
  let previousFinishedPromise = animation.finished;
  animation.finish();
  return animation.finished.then(() => {
    assert_equals(animation.finished, previousFinishedPromise,
                  'Finished promise is the same object when playing completes');
    animation.play();
    assert_not_equals(animation.finished, previousFinishedPromise,
                  'Finished promise changes when replaying animation');

    previousFinishedPromise = animation.finished;
    animation.play();
    assert_equals(animation.finished, previousFinishedPromise,
                  'Finished promise is the same after redundant play() call');

  });
}, 'Test restarting a finished animation');

promise_test(t => {
  const div = createDiv(t);
  const animation = div.animate({}, 100 * MS_PER_SEC);
  let previousFinishedPromise;
  animation.finish();
  return animation.finished.then(() => {
    previousFinishedPromise = animation.finished;
    animation.playbackRate = -1;
    assert_not_equals(animation.finished, previousFinishedPromise,
                      'Finished promise should be replaced when reversing a ' +
                      'finished promise');
    animation.currentTime = 0;
    return animation.finished;
  }).then(() => {
    previousFinishedPromise = animation.finished;
    animation.play();
    assert_not_equals(animation.finished, previousFinishedPromise,
                      'Finished promise is replaced after play() call on ' +
                      'finished, reversed animation');
  });
}, 'Test restarting a reversed finished animation');

promise_test(t => {
  const div = createDiv(t);
  const animation = div.animate({}, 100 * MS_PER_SEC);
  const previousFinishedPromise = animation.finished;
  animation.finish();
  return animation.finished.then(() => {
    animation.currentTime = 100 * MS_PER_SEC + 1000;
    assert_equals(animation.finished, previousFinishedPromise,
                  'Finished promise is unchanged jumping past end of ' +
                  'finished animation');
  });
}, 'Test redundant finishing of animation');

promise_test(t => {
  const div = createDiv(t);
  const animation = div.animate({}, 100 * MS_PER_SEC);
  // Setup callback to run if finished promise is resolved
  let finishPromiseResolved = false;
  animation.finished.then(() => {
    finishPromiseResolved = true;
  });
  return animation.ready.then(() => {
    // Jump to mid-way in interval and pause
    animation.currentTime = 100 * MS_PER_SEC / 2;
    animation.pause();
    return animation.ready;
  }).then(() => {
    // Jump to the end
    // (But don't use finish() since that should unpause as well)
    animation.currentTime = 100 * MS_PER_SEC;
    return waitForAnimationFrames(2);
  }).then(() => {
    assert_false(finishPromiseResolved,
                 'Finished promise should not resolve when paused');
  });
}, 'Finished promise does not resolve when paused');

promise_test(t => {
  const div = createDiv(t);
  const animation = div.animate({}, 100 * MS_PER_SEC);
  // Setup callback to run if finished promise is resolved
  let finishPromiseResolved = false;
  animation.finished.then(() => {
    finishPromiseResolved = true;
  });
  return animation.ready.then(() => {
    // Jump to mid-way in interval and pause
    animation.currentTime = 100 * MS_PER_SEC / 2;
    animation.pause();
    // Jump to the end
    animation.currentTime = 100 * MS_PER_SEC;
    return waitForAnimationFrames(2);
  }).then(() => {
    assert_false(finishPromiseResolved,
                 'Finished promise should not resolve when pause-pending');
  });
}, 'Finished promise does not resolve when pause-pending');

promise_test(t => {
  const div = createDiv(t);
  const animation = div.animate({}, 100 * MS_PER_SEC);
  animation.finish();
  return animation.finished.then(resolvedAnimation => {
    assert_equals(resolvedAnimation, animation,
                  'Object identity of animation passed to Promise callback'
                  + ' matches the animation object owning the Promise');
  });
}, 'The finished promise is fulfilled with its Animation');

promise_test(t => {
  const div = createDiv(t);
  const animation = div.animate({}, 100 * MS_PER_SEC);
  const previousFinishedPromise = animation.finished;

  // Set up listeners on finished promise
  const retPromise = animation.finished.then(() => {
    assert_unreached('finished promise was fulfilled');
  }).catch(err => {
    assert_equals(err.name, 'AbortError',
                  'finished promise is rejected with AbortError');
    assert_not_equals(animation.finished, previousFinishedPromise,
                      'Finished promise should change after the original is ' +
                      'rejected');
  });

  animation.cancel();

  return retPromise;
}, 'finished promise is rejected when an animation is canceled by calling ' +
   'cancel()');

promise_test(t => {
  const div = createDiv(t);
  const animation = div.animate({}, 100 * MS_PER_SEC);
  const previousFinishedPromise = animation.finished;
  animation.finish();
  return animation.finished.then(() => {
    animation.cancel();
    assert_not_equals(animation.finished, previousFinishedPromise,
                      'A new finished promise should be created when'
                      + ' canceling a finished animation');
  });
}, 'canceling an already-finished animation replaces the finished promise');

promise_test(t => {
  const div = createDiv(t);
  const animation = div.animate({}, 100 * MS_PER_SEC);
  const HALF_DUR = 100 * MS_PER_SEC / 2;
  const QUARTER_DUR = 100 * MS_PER_SEC / 4;
  let gotNextFrame = false;
  let currentTimeBeforeShortening;
  animation.currentTime = HALF_DUR;
  return animation.ready.then(() => {
    currentTimeBeforeShortening = animation.currentTime;
    animation.effect.updateTiming({ duration: QUARTER_DUR });
    // Below we use gotNextFrame to check that shortening of the animation
    // duration causes the finished promise to resolve, rather than it just
    // getting resolved on the next animation frame. This relies on the fact
    // that the promises are resolved as a micro-task before the next frame
    // happens.
    waitForAnimationFrames(1).then(() => {
      gotNextFrame = true;
    });

    return animation.finished;
  }).then(() => {
    assert_false(gotNextFrame, 'shortening of the animation duration should ' +
                               'resolve the finished promise');
    assert_equals(animation.currentTime, currentTimeBeforeShortening,
                  'currentTime should be unchanged when duration shortened');
    const previousFinishedPromise = animation.finished;
    animation.effect.updateTiming({ duration: 100 * MS_PER_SEC });
    assert_not_equals(animation.finished, previousFinishedPromise,
                      'Finished promise should change after lengthening the ' +
                      'duration causes the animation to become active');
  });
}, 'Test finished promise changes for animation duration changes');

promise_test(t => {
  const div = createDiv(t);
  const animation = div.animate({}, 100 * MS_PER_SEC);
  const retPromise = animation.ready.then(() => {
    animation.playbackRate = 0;
    animation.currentTime = 100 * MS_PER_SEC + 1000;
    return waitForAnimationFrames(2);
  });

  animation.finished.then(t.step_func(() => {
    assert_unreached('finished promise should not resolve when playbackRate ' +
                     'is zero');
  }));

  return retPromise;
}, 'Test finished promise changes when playbackRate == 0');

promise_test(t => {
  const div = createDiv(t);
  const animation = div.animate({}, 100 * MS_PER_SEC);
  return animation.ready.then(() => {
    animation.playbackRate = -1;
    return animation.finished;
  });
}, 'Test finished promise resolves when reaching to the natural boundary.');

promise_test(t => {
  const div = createDiv(t);
  const animation = div.animate({}, 100 * MS_PER_SEC);
  const previousFinishedPromise = animation.finished;
  animation.finish();
  return animation.finished.then(() => {
    animation.currentTime = 0;
    assert_not_equals(animation.finished, previousFinishedPromise,
                      'Finished promise should change once a prior ' +
                      'finished promise resolved and the animation ' +
                      'falls out finished state');
  });
}, 'Test finished promise changes when a prior finished promise resolved ' +
   'and the animation falls out finished state');

test(t => {
  const div = createDiv(t);
  const animation = div.animate({}, 100 * MS_PER_SEC);
  const previousFinishedPromise = animation.finished;
  animation.currentTime = 100 * MS_PER_SEC;
  animation.currentTime = 100 * MS_PER_SEC / 2;
  assert_equals(animation.finished, previousFinishedPromise,
                'No new finished promise generated when finished state ' +
                'is checked asynchronously');
}, 'Test no new finished promise generated when finished state ' +
   'is checked asynchronously');

test(t => {
  const div = createDiv(t);
  const animation = div.animate({}, 100 * MS_PER_SEC);
  const previousFinishedPromise = animation.finished;
  animation.finish();
  animation.currentTime = 100 * MS_PER_SEC / 2;
  assert_not_equals(animation.finished, previousFinishedPromise,
                    'New finished promise generated when finished state ' +
                    'is checked synchronously');
}, 'Test new finished promise generated when finished state ' +
   'is checked synchronously');

promise_test(t => {
  const div = createDiv(t);
  const animation = div.animate({}, 100 * MS_PER_SEC);
  let resolvedFinished = false;
  animation.finished.then(() => {
    resolvedFinished = true;
  });
  return animation.ready.then(() => {
    animation.finish();
    animation.currentTime = 100 * MS_PER_SEC / 2;
  }).then(() => {
    assert_true(resolvedFinished,
      'Animation.finished should be resolved even if ' +
      'the finished state is changed soon');
  });

}, 'Test synchronous finished promise resolved even if finished state ' +
   'is changed soon');

promise_test(t => {
  const div = createDiv(t);
  const animation = div.animate({}, 100 * MS_PER_SEC);
  let resolvedFinished = false;
  animation.finished.then(() => {
    resolvedFinished = true;
  });

  return animation.ready.then(() => {
    animation.currentTime = 100 * MS_PER_SEC;
    animation.finish();
  }).then(() => {
    assert_true(resolvedFinished,
      'Animation.finished should be resolved soon after finish() is ' +
      'called even if there are other asynchronous promises just before it');
  });
}, 'Test synchronous finished promise resolved even if asynchronous ' +
   'finished promise happens just before synchronous promise');

promise_test(t => {
  const div = createDiv(t);
  const animation = div.animate({}, 100 * MS_PER_SEC);
  animation.finished.then(t.step_func(() => {
    assert_unreached('Animation.finished should not be resolved');
  }));

  return animation.ready.then(() => {
    animation.currentTime = 100 * MS_PER_SEC;
    animation.currentTime = 100 * MS_PER_SEC / 2;
  });
}, 'Test finished promise is not resolved when the animation ' +
   'falls out finished state immediately');

promise_test(t => {
  const div = createDiv(t);
  const animation = div.animate({}, 100 * MS_PER_SEC);
  return animation.ready.then(() => {
    animation.currentTime = 100 * MS_PER_SEC;
    animation.finished.then(t.step_func(() => {
      assert_unreached('Animation.finished should not be resolved');
    }));
    animation.currentTime = 0;
  });

}, 'Test finished promise is not resolved once the animation ' +
   'falls out finished state even though the current finished ' +
   'promise is generated soon after animation state became finished');

promise_test(t => {
  const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
  let ready = false;
  animation.ready.then(
    t.step_func(() => {
      ready = true;
    }),
    t.unreached_func('Ready promise must not be rejected')
  );

  const testSuccess = animation.finished.then(
    t.step_func(() => {
      assert_true(ready, 'Ready promise has resolved');
    }),
    t.unreached_func('Finished promise must not be rejected')
  );

  const timeout = waitForAnimationFrames(3).then(() => {
    return Promise.reject('Finished promise did not arrive in time');
  });

  animation.finish();
  return Promise.race([timeout, testSuccess]);
}, 'Finished promise should be resolved after the ready promise is resolved');

promise_test(t => {
  const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
  let caught = false;
  animation.ready.then(
    t.unreached_func('Ready promise must not be resolved'),
    t.step_func(() => {
      caught = true;
    })
  );

  const testSuccess = animation.finished.then(
    t.unreached_func('Finished promise must not be resolved'),
    t.step_func(() => {
      assert_true(caught, 'Ready promise has been rejected');
    })
  );

  const timeout = waitForAnimationFrames(3).then(() => {
    return Promise.reject('Finished promise was not rejected in time');
  });

  animation.cancel();
  return Promise.race([timeout, testSuccess]);
}, 'Finished promise should be rejected after the ready promise is rejected');

promise_test(async t => {
  const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);

  // Ensure the finished promise is created
  const finished = animation.finished;

  window.addEventListener(
    'unhandledrejection',
    t.unreached_func('Should not get an unhandled rejection')
  );

  animation.cancel();

  // Wait a moment to allow a chance for the event to be dispatched.
  await waitForAnimationFrames(2);
}, 'Finished promise does not report an unhandledrejection when rejected');

</script>
</body>
