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

test(t => {
  const div = createDiv(t);
  assert_array_equals(div.getAnimations(), []);
}, 'Returns an empty array for an element with no animations');

test(t => {
  const div = createDiv(t);
  const animationA = div.animate(null, 100 * MS_PER_SEC);
  const animationB = div.animate(null, 100 * MS_PER_SEC);
  assert_array_equals(div.getAnimations(), [animationA, animationB]);
}, 'Returns both animations for an element with two animations');

test(t => {
  const divA = createDiv(t);
  const divB = createDiv(t);
  const animationA = divA.animate(null, 100 * MS_PER_SEC);
  const animationB = divB.animate(null, 100 * MS_PER_SEC);
  assert_array_equals(divA.getAnimations(), [animationA], 'divA');
  assert_array_equals(divB.getAnimations(), [animationB], 'divB');
}, 'Returns only the animations specific to each sibling element');

test(t => {
  const divParent = createDiv(t);
  const divChild = createDiv(t);
  divParent.appendChild(divChild);
  const animationParent = divParent.animate(null, 100 * MS_PER_SEC);
  const animationChild = divChild.animate(null, 100 * MS_PER_SEC);
  assert_array_equals(divParent.getAnimations(), [animationParent],
                      'divParent');
  assert_array_equals(divChild.getAnimations(), [animationChild], 'divChild');
}, 'Returns only the animations specific to each parent/child element');

test(t => {
  const divParent = createDiv(t);
  const divChild = createDiv(t);
  divParent.appendChild(divChild);
  const divGrandChildA = createDiv(t);
  const divGrandChildB = createDiv(t);
  divChild.appendChild(divGrandChildA);
  divChild.appendChild(divGrandChildB);

  // Trigger the animations in a somewhat random order
  const animGrandChildB = divGrandChildB.animate(null, 100 * MS_PER_SEC);
  const animChild = divChild.animate(null, 100 * MS_PER_SEC);
  const animGrandChildA = divGrandChildA.animate(null, 100 * MS_PER_SEC);

  assert_array_equals(
    divParent.getAnimations({ subtree: true }),
    [animGrandChildB, animChild, animGrandChildA],
    'Returns expected animations from parent'
  );
  assert_array_equals(
    divChild.getAnimations({ subtree: true }),
    [animGrandChildB, animChild, animGrandChildA],
    'Returns expected animations from child'
  );
  assert_array_equals(
    divGrandChildA.getAnimations({ subtree: true }),
    [animGrandChildA],
    'Returns expected animations from grandchild A'
  );
}, 'Returns animations on descendants when subtree: true is specified');

test(t => {
  createStyle(t, {
    '@keyframes anim': '',
    [`.pseudo::before`]: 'animation: anim 100s; ' + "content: '';",
  });
  const div = createDiv(t);
  div.classList.add('pseudo');

  assert_equals(
    div.getAnimations().length,
    0,
    'Returns no animations when subtree is false'
  );
  assert_equals(
    div.getAnimations({ subtree: true }).length,
    1,
    'Returns one animation when subtree is true'
  );
}, 'Returns animations on pseudo-elements when subtree: true is specified');

test(t => {
  const host = createDiv(t);
  const shadow = host.attachShadow({ mode: 'open' });

  const elem = createDiv(t);
  shadow.appendChild(elem);

  const elemChild = createDiv(t);
  elem.appendChild(elemChild);

  elemChild.animate(null, 100 * MS_PER_SEC);

  assert_equals(
    host.getAnimations({ subtree: true }).length,
    0,
    'Returns no animations with subtree:true when called on the host'
  );
  assert_equals(
    elem.getAnimations({ subtree: true }).length,
    1,
    'Returns one animation when called on a parent in the shadow tree'
  );
}, 'Does NOT cross shadow-tree boundaries when subtree: true is specified');

test(t => {
  const foreignElement
    = document.createElementNS('http://example.org/test', 'test');
  document.body.appendChild(foreignElement);
  t.add_cleanup(() => {
    foreignElement.remove();
  });

  const animation = foreignElement.animate(null, 100 * MS_PER_SEC);
  assert_array_equals(foreignElement.getAnimations(), [animation]);
}, 'Returns animations for a foreign element');

test(t => {
  const div = createDiv(t);
  const animation = div.animate(null, 100 * MS_PER_SEC);
  animation.finish();
  assert_array_equals(div.getAnimations(), []);
}, 'Does not return finished animations that do not fill forwards');

test(t => {
  const div = createDiv(t);
  const animation = div.animate(null, {
    duration: 100 * MS_PER_SEC,
    fill: 'forwards',
  });
  animation.finish();
  assert_array_equals(div.getAnimations(), [animation]);
}, 'Returns finished animations that fill forwards');

test(t => {
  const div = createDiv(t);
  const animation = div.animate(null, {
    duration: 100 * MS_PER_SEC,
    delay: 100 * MS_PER_SEC,
  });
  assert_array_equals(div.getAnimations(), [animation]);
}, 'Returns animations yet to reach their active phase');

test(t => {
  const div = createDiv(t);
  const animation = div.animate(null, 100 * MS_PER_SEC);
  animation.playbackRate = -1;
  assert_array_equals(div.getAnimations(), []);
}, 'Does not return reversed finished animations that do not fill backwards');

test(t => {
  const div = createDiv(t);
  const animation = div.animate(null, {
    duration: 100 * MS_PER_SEC,
    fill: 'backwards',
  });
  animation.playbackRate = -1;
  assert_array_equals(div.getAnimations(), [animation]);
}, 'Returns reversed finished animations that fill backwards');

test(t => {
  const div = createDiv(t);
  const animation = div.animate(null, 100 * MS_PER_SEC);
  animation.playbackRate = -1;
  animation.currentTime = 200 * MS_PER_SEC;
  assert_array_equals(div.getAnimations(), [animation]);
}, 'Returns reversed animations yet to reach their active phase');

test(t => {
  const div = createDiv(t);
  const animation = div.animate(null, {
    duration: 100 * MS_PER_SEC,
    delay: 100 * MS_PER_SEC,
  });
  animation.playbackRate = 0;
  assert_array_equals(div.getAnimations(), []);
}, 'Does not return animations with zero playback rate in before phase');

test(t => {
  const div = createDiv(t);
  const animation = div.animate(null, 100 * MS_PER_SEC);
  animation.finish();
  animation.playbackRate = 0;
  animation.currentTime = 200 * MS_PER_SEC;
  assert_array_equals(div.getAnimations(), []);
}, 'Does not return animations with zero playback rate in after phase');

test(t => {
  const div = createDiv(t);
  const effect = new KeyframeEffect(div, {}, 225);
  const animation = new Animation(effect, new DocumentTimeline());
  animation.reverse();
  animation.pause();
  animation.playbackRate = -1;;
  animation.updatePlaybackRate(1);
  assert_array_equals(div.getAnimations(), []);
}, 'Does not return an animation that has recently been made not current by setting the playback rate');

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

  animation.finish();
  assert_array_equals(div.getAnimations(), [],
                      'Animation should not be returned when it is finished');

  animation.effect.updateTiming({
    duration: animation.effect.getTiming().duration + 100 * MS_PER_SEC,
  });
  assert_array_equals(div.getAnimations(), [animation],
                      'Animation should be returned after extending the'
                      + ' duration');

  animation.effect.updateTiming({ duration: 0 });
  assert_array_equals(div.getAnimations(), [],
                      'Animation should not be returned after setting the'
                      + ' duration to zero');
}, 'Returns animations based on dynamic changes to individual'
   + ' animations\' duration');

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

  animation.effect.updateTiming({ endDelay: -200 * MS_PER_SEC });
  assert_array_equals(div.getAnimations(), [],
                      'Animation should not be returned after setting a'
                      + ' negative end delay such that the end time is less'
                      + ' than the current time');

  animation.effect.updateTiming({ endDelay: 100 * MS_PER_SEC });
  assert_array_equals(div.getAnimations(), [animation],
                      'Animation should be returned after setting a positive'
                      + ' end delay such that the end time is more than the'
                      + ' current time');
}, 'Returns animations based on dynamic changes to individual'
   + ' animations\' end delay');

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

  animation.finish();
  assert_array_equals(div.getAnimations(), [],
                      'Animation should not be returned when it is finished');

  animation.effect.updateTiming({ iterations: 10 });
  assert_array_equals(div.getAnimations(), [animation],
                      'Animation should be returned after inreasing the'
                      + ' number of iterations');

  animation.effect.updateTiming({ iterations: 0 });
  assert_array_equals(div.getAnimations(), [],
                      'Animations should not be returned after setting the'
                      + ' iteration count to zero');

  animation.effect.updateTiming({ iterations: Infinity });
  assert_array_equals(div.getAnimations(), [animation],
                      'Animation should be returned after inreasing the'
                      + ' number of iterations to infinity');
}, 'Returns animations based on dynamic changes to individual'
   + ' animations\' iteration count');

test(t => {
  const div = createDiv(t);
  const animation = div.animate(null,
                                { duration: 100 * MS_PER_SEC,
                                  delay: 50 * MS_PER_SEC,
                                  endDelay: -50 * MS_PER_SEC });

  assert_array_equals(div.getAnimations(), [animation],
                      'Animation should be returned at during delay phase');

  animation.currentTime = 50 * MS_PER_SEC;
  assert_array_equals(div.getAnimations(), [animation],
                      'Animation should be returned after seeking to the start'
                      + ' of the active interval');

  animation.currentTime = 100 * MS_PER_SEC;
  assert_array_equals(div.getAnimations(), [],
                      'Animation should not be returned after seeking to the'
                      + ' clipped end of the active interval');
}, 'Returns animations based on dynamic changes to individual'
   + ' animations\' current time');

promise_test(async t => {
  const div = createDiv(t);

  const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
  const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
  await animA.finished;
  // It is not guaranteed that the mircrotask PerformCheckpoint() happens before
  // the animation finish promised got resolved, because the microtask
  // checkpoint could also be triggered from other source such as the event_loop
  // Thus we wait for one animation frame to make sure the finished animation is
  // properly removed.
  await waitForNextFrame(1);
  assert_array_equals(div.getAnimations(), [animB]);
}, 'Does not return an animation that has been removed');

promise_test(async t => {
  const div = createDiv(t);

  const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
  const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
  await animA.finished;

  animA.persist();

  assert_array_equals(div.getAnimations(), [animA, animB]);
}, 'Returns an animation that has been persisted');

promise_test(async t => {
  const div = createDiv(t);
  const watcher = EventWatcher(t, div, 'transitionrun');

  // Create a covering animation to prevent transitions from firing after
  // calling getAnimations().
  const coveringAnimation = new Animation(
    new KeyframeEffect(div, { opacity: [0, 1] }, 100 * MS_PER_SEC)
  );

  // Setup transition start point.
  div.style.transition = 'opacity 100s';
  getComputedStyle(div).opacity;

  // Update specified style but don't flush style.
  div.style.opacity = '0.5';

  // Fetch animations
  div.getAnimations();

  // Play the covering animation to ensure that only the call to
  // getAnimations() has a chance to trigger transitions.
  coveringAnimation.play();

  // If getAnimations() flushed style, we should get a transitionrun event.
  await watcher.wait_for('transitionrun');
}, 'Triggers a style change event');

</script>
</body>
