chromium/third_party/blink/web_tests/external/wpt/web-animations/timing-model/timelines/update-and-send-events-replacement.html

<!doctype html>
<meta charset=utf-8>
<title>Update animations and send events (replacement)</title>
<link rel="help" href="https://drafts.csswg.org/web-animations/#update-animations-and-send-events">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="../../testcommon.js"></script>
<style>
@keyframes opacity-animation {
  to { opacity: 1 }
}
</style>
<div id="log"></div>
<script>
'use strict';

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;

  assert_equals(animA.replaceState, 'removed');
  assert_equals(animB.replaceState, 'active');
}, 'Removes an animation when another covers the same properties');

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

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

  assert_equals(animA.replaceState, 'active');

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

  assert_equals(animA.replaceState, 'removed');
  assert_equals(animB.replaceState, 'active');
}, 'Removes an animation after another animation finishes');

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

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

  assert_equals(animA.replaceState, 'active');

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

  assert_equals(animA.replaceState, 'active');
  assert_equals(animB.replaceState, 'active');

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

  assert_equals(animA.replaceState, 'removed');
  assert_equals(animB.replaceState, 'active');
  assert_equals(animC.replaceState, 'active');
}, 'Removes an animation after multiple other animations finish');

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

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

  assert_equals(animB.replaceState, 'active');
  assert_equals(animB.replaceState, 'active');

  // Seek animA to just before it finishes since we want to test the behavior
  // when the animation finishes by the ticking of the timeline, not by seeking
  // (that is covered in a separate test).

  animA.currentTime = 99.99 * MS_PER_SEC;
  await animA.finished;

  assert_equals(animA.replaceState, 'removed');
  assert_equals(animB.replaceState, 'active');
}, 'Removes an animation after it finishes');

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: 100 * MS_PER_SEC, fill: 'forwards' }
  );
  await animA.finished;

  assert_equals(animA.replaceState, 'active');
  assert_equals(animB.replaceState, 'active');

  animB.finish();

  // Replacement should not happen until the next time the "update animations
  // and send events" procedure runs.

  assert_equals(animA.replaceState, 'active');
  assert_equals(animB.replaceState, 'active');

  await waitForNextFrame();

  assert_equals(animA.replaceState, 'removed');
  assert_equals(animB.replaceState, 'active');
}, 'Removes an animation after seeking another animation');

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

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

  assert_equals(animA.replaceState, 'active');
  assert_equals(animB.replaceState, 'active');

  animA.finish();

  // Replacement should not happen until the next time the "update animations
  // and send events" procedure runs.

  assert_equals(animA.replaceState, 'active');
  assert_equals(animB.replaceState, 'active');

  await waitForNextFrame();

  assert_equals(animA.replaceState, 'removed');
  assert_equals(animB.replaceState, 'active');
}, 'Removes an animation after seeking it');

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

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

  assert_equals(animA.replaceState, 'active');
  assert_equals(animB.replaceState, 'active');

  animB.effect.updateTiming({ fill: 'forwards' });

  // Replacement should not happen until the next time the "update animations
  // and send events" procedure runs.

  assert_equals(animA.replaceState, 'active');
  assert_equals(animB.replaceState, 'active');

  await waitForNextFrame();

  assert_equals(animA.replaceState, 'removed');
  assert_equals(animB.replaceState, 'active');
}, 'Removes an animation after updating the fill mode of another animation');

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

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

  assert_equals(animA.replaceState, 'active');
  assert_equals(animB.replaceState, 'active');

  animA.effect.updateTiming({ fill: 'forwards' });

  // Replacement should not happen until the next time the "update animations
  // and send events" procedure runs.

  assert_equals(animA.replaceState, 'active');
  assert_equals(animB.replaceState, 'active');

  await waitForNextFrame();

  assert_equals(animA.replaceState, 'removed');
  assert_equals(animB.replaceState, 'active');
}, 'Removes an animation after updating its fill mode');

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

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

  assert_equals(animA.replaceState, 'active');
  assert_equals(animB.replaceState, 'active');

  animB.effect = new KeyframeEffect(
    div,
    { opacity: 1 },
    {
      duration: 1,
      fill: 'forwards',
    }
  );

  assert_equals(animA.replaceState, 'active');
  assert_equals(animB.replaceState, 'active');

  await waitForNextFrame();

  assert_equals(animA.replaceState, 'removed');
  assert_equals(animB.replaceState, 'active');
}, "Removes an animation after updating another animation's effect to one with different timing");

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

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

  assert_equals(animA.replaceState, 'active');
  assert_equals(animB.replaceState, 'active');

  animA.effect = new KeyframeEffect(
    div,
    { opacity: 1 },
    {
      duration: 1,
      fill: 'forwards',
    }
  );

  assert_equals(animA.replaceState, 'active');
  assert_equals(animB.replaceState, 'active');

  await waitForNextFrame();

  assert_equals(animA.replaceState, 'removed');
  assert_equals(animB.replaceState, 'active');
}, 'Removes an animation after updating its effect to one with different timing');

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: 100 * MS_PER_SEC, fill: 'forwards' }
  );

  await animA.finished;

  // Set up a timeline that makes animB finished
  animB.timeline = new DocumentTimeline({
    originTime:
      document.timeline.currentTime - 100 * MS_PER_SEC - animB.startTime,
  });

  assert_equals(animA.replaceState, 'active');
  assert_equals(animB.replaceState, 'active');

  await waitForNextFrame();

  assert_equals(animA.replaceState, 'removed');
  assert_equals(animB.replaceState, 'active');
}, "Removes an animation after updating another animation's timeline");

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

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

  await animB.finished;

  // Set up a timeline that makes animA finished
  animA.timeline = new DocumentTimeline({
    originTime:
      document.timeline.currentTime - 100 * MS_PER_SEC - animA.startTime,
  });

  assert_equals(animA.replaceState, 'active');
  assert_equals(animB.replaceState, 'active');

  await waitForNextFrame();

  assert_equals(animA.replaceState, 'removed');
  assert_equals(animB.replaceState, 'active');
}, 'Removes an animation after updating its timeline');

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

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

  assert_equals(animA.replaceState, 'active');
  assert_equals(animB.replaceState, 'active');

  animB.effect.setKeyframes({ width: '100px', opacity: 1 });

  assert_equals(animA.replaceState, 'active');
  assert_equals(animB.replaceState, 'active');

  await waitForNextFrame();

  assert_equals(animA.replaceState, 'removed');
  assert_equals(animB.replaceState, 'active');
}, "Removes an animation after updating another animation's effect's properties");

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

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

  assert_equals(animA.replaceState, 'active');
  assert_equals(animB.replaceState, 'active');

  animA.effect.setKeyframes({ width: '100px' });

  assert_equals(animA.replaceState, 'active');
  assert_equals(animB.replaceState, 'active');

  await waitForNextFrame();

  assert_equals(animA.replaceState, 'removed');
  assert_equals(animB.replaceState, 'active');
}, "Removes an animation after updating its effect's properties");

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

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

  assert_equals(animA.replaceState, 'active');
  assert_equals(animB.replaceState, 'active');

  animB.effect = new KeyframeEffect(
    div,
    { width: '100px', opacity: 1 },
    { duration: 1, fill: 'forwards' }
  );

  assert_equals(animA.replaceState, 'active');
  assert_equals(animB.replaceState, 'active');

  await waitForNextFrame();

  assert_equals(animA.replaceState, 'removed');
  assert_equals(animB.replaceState, 'active');
}, "Removes an animation after updating another animation's effect to one with different properties");

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

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

  assert_equals(animA.replaceState, 'active');
  assert_equals(animB.replaceState, 'active');

  animA.effect = new KeyframeEffect(
    div,
    { width: '100px' },
    {
      duration: 1,
      fill: 'forwards',
    }
  );

  assert_equals(animA.replaceState, 'active');
  assert_equals(animB.replaceState, 'active');

  await waitForNextFrame();

  assert_equals(animA.replaceState, 'removed');
  assert_equals(animB.replaceState, 'active');
}, 'Removes an animation after updating its effect to one with different properties');

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

  const animA = div.animate(
    { marginLeft: '10px' },
    { duration: 1, fill: 'forwards' }
  );
  const animB = div.animate(
    { margin: '20px' },
    { duration: 1, fill: 'forwards' }
  );
  await animA.finished;

  assert_equals(animA.replaceState, 'removed');
  assert_equals(animB.replaceState, 'active');
}, 'Removes an animation when another animation uses a shorthand');

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

  const animA = div.animate(
    { margin: '10px' },
    { duration: 1, fill: 'forwards' }
  );
  const animB = div.animate(
    {
      marginLeft: '10px',
      marginTop: '20px',
      marginRight: '30px',
      marginBottom: '40px',
    },
    { duration: 1, fill: 'forwards' }
  );
  await animA.finished;

  assert_equals(animA.replaceState, 'removed');
  assert_equals(animB.replaceState, 'active');
}, 'Removes an animation that uses a shorthand');

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

  const animA = div.animate(
    { marginLeft: '10px' },
    { duration: 1, fill: 'forwards' }
  );
  const animB = div.animate(
    { marginInlineStart: '20px' },
    { duration: 1, fill: 'forwards' }
  );
  await animA.finished;

  assert_equals(animA.replaceState, 'removed');
  assert_equals(animB.replaceState, 'active');
}, 'Removes an animation by another animation using logical properties');

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

  const animA = div.animate(
    { marginInlineStart: '10px' },
    { duration: 1, fill: 'forwards' }
  );
  const animB = div.animate(
    { marginLeft: '20px' },
    { duration: 1, fill: 'forwards' }
  );
  await animA.finished;

  assert_equals(animA.replaceState, 'removed');
  assert_equals(animB.replaceState, 'active');
}, 'Removes an animation using logical properties');

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

  const animA = div.animate(
    { marginTop: '10px' },
    { duration: 1, fill: 'forwards' }
  );
  const animB = div.animate(
    { marginInlineStart: '20px' },
    { duration: 1, fill: 'forwards' }
  );
  await animA.finished;

  assert_equals(animA.replaceState, 'active');
  assert_equals(animB.replaceState, 'active');

  div.style.writingMode = 'vertical-rl';

  assert_equals(animA.replaceState, 'active');
  assert_equals(animB.replaceState, 'active');

  await waitForNextFrame();

  assert_equals(animA.replaceState, 'removed');
  assert_equals(animB.replaceState, 'active');
}, 'Removes an animation by another animation using logical properties after updating the context');

promise_test(async t => {
  const divA = createDiv(t);
  const divB = createDiv(t);

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

  assert_equals(animA.replaceState, 'active');
  assert_equals(animB.replaceState, 'active');

  animB.effect.target = divA;

  assert_equals(animA.replaceState, 'active');
  assert_equals(animB.replaceState, 'active');

  await waitForNextFrame();

  assert_equals(animA.replaceState, 'removed');
  assert_equals(animB.replaceState, 'active');
}, "Removes an animation after updating another animation's effect's target");

promise_test(async t => {
  const divA = createDiv(t);
  const divB = createDiv(t);

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

  assert_equals(animA.replaceState, 'active');
  assert_equals(animB.replaceState, 'active');

  animA.effect.target = divB;

  assert_equals(animA.replaceState, 'active');
  assert_equals(animB.replaceState, 'active');

  await waitForNextFrame();

  assert_equals(animA.replaceState, 'removed');
  assert_equals(animB.replaceState, 'active');
}, "Removes an animation after updating its effect's target");

promise_test(async t => {
  const divA = createDiv(t);
  const divB = createDiv(t);

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

  assert_equals(animA.replaceState, 'active');
  assert_equals(animB.replaceState, 'active');

  animB.effect = new KeyframeEffect(
    divA,
    { opacity: 1 },
    {
      duration: 1,
      fill: 'forwards',
    }
  );

  assert_equals(animA.replaceState, 'active');
  assert_equals(animB.replaceState, 'active');

  await waitForNextFrame();

  assert_equals(animA.replaceState, 'removed');
  assert_equals(animB.replaceState, 'active');
}, "Removes an animation after updating another animation's effect to one with a different target");

promise_test(async t => {
  const divA = createDiv(t);
  const divB = createDiv(t);

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

  assert_equals(animA.replaceState, 'active');
  assert_equals(animB.replaceState, 'active');

  animA.effect = new KeyframeEffect(
    divB,
    { opacity: 1 },
    {
      duration: 1,
      fill: 'forwards',
    }
  );

  assert_equals(animA.replaceState, 'active');
  assert_equals(animB.replaceState, 'active');

  await waitForNextFrame();

  assert_equals(animA.replaceState, 'removed');
  assert_equals(animB.replaceState, 'active');
}, 'Removes an animation after updating its effect to one with a different target');

promise_test(async t => {
  const div = createDiv(t);
  div.style.animation = 'opacity-animation 1ms forwards';
  const cssAnimation = div.getAnimations()[0];

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

  assert_equals(cssAnimation.replaceState, 'active');
  assert_equals(scriptAnimation.replaceState, 'active');
}, 'Does NOT remove a CSS animation tied to markup');

promise_test(async t => {
  const div = createDiv(t);
  div.style.animation = 'opacity-animation 1ms forwards';
  const cssAnimation = div.getAnimations()[0];

  // Break tie to markup
  div.style.animationName = 'none';
  assert_equals(cssAnimation.playState, 'idle');

  // Restart animation
  cssAnimation.play();

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

  assert_equals(cssAnimation.replaceState, 'removed');
  assert_equals(scriptAnimation.replaceState, 'active');
}, 'Removes a CSS animation no longer tied to markup');

promise_test(async t => {
  // Setup transition
  const div = createDiv(t);
  div.style.opacity = '0';
  div.style.transition = 'opacity 1ms';
  getComputedStyle(div).opacity;
  div.style.opacity = '1';
  const cssTransition = div.getAnimations()[0];
  cssTransition.effect.updateTiming({ fill: 'forwards' });

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

  assert_equals(cssTransition.replaceState, 'active');
  assert_equals(scriptAnimation.replaceState, 'active');
}, 'Does NOT remove a CSS transition tied to markup');

promise_test(async t => {
  // Setup transition
  const div = createDiv(t);
  div.style.opacity = '0';
  div.style.transition = 'opacity 1ms';
  getComputedStyle(div).opacity;
  div.style.opacity = '1';
  const cssTransition = div.getAnimations()[0];
  cssTransition.effect.updateTiming({ fill: 'forwards' });

  // Break tie to markup
  div.style.transitionProperty = 'none';
  assert_equals(cssTransition.playState, 'idle');

  // Restart transition
  cssTransition.play();

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

  assert_equals(cssTransition.replaceState, 'removed');
  assert_equals(scriptAnimation.replaceState, 'active');
}, 'Removes a CSS transition no longer tied to markup');

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' });
  const eventWatcher = new EventWatcher(t, animA, 'remove');

  const event = await eventWatcher.wait_for('remove');

  assert_times_equal(event.timelineTime, document.timeline.currentTime);
  assert_times_equal(event.currentTime, 1);
}, 'Dispatches an event when removing');

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' });
  const eventWatcher = new EventWatcher(t, animA, 'remove');

  await eventWatcher.wait_for('remove');

  // Check we don't get another event
  animA.addEventListener(
    'remove',
    t.step_func(() => {
      assert_unreached('remove event should not be fired a second time');
    })
  );

  // Restart animation
  animA.play();

  await waitForNextFrame();

  // Finish animation
  animA.finish();

  await waitForNextFrame();
}, 'Does NOT dispatch a remove event twice');

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: 100 * MS_PER_SEC, fill: 'forwards' }
  );
  await animA.finished;

  assert_equals(animA.replaceState, 'active');

  animB.finish();
  animB.currentTime = 0;

  await waitForNextFrame();

  assert_equals(animA.replaceState, 'active');
}, "Does NOT remove an animation after making a redundant change to another animation's current time");

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

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

  assert_equals(animA.replaceState, 'active');

  animA.finish();
  animA.currentTime = 0;

  await waitForNextFrame();

  assert_equals(animA.replaceState, 'active');
}, 'Does NOT remove an animation after making a redundant change to its 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: 100 * MS_PER_SEC, fill: 'forwards' }
  );
  await animA.finished;

  assert_equals(animA.replaceState, 'active');

  // Set up a timeline that makes animB finished but then restore it
  animB.timeline = new DocumentTimeline({
    originTime:
      document.timeline.currentTime - 100 * MS_PER_SEC - animB.startTime,
  });
  animB.timeline = document.timeline;

  await waitForNextFrame();

  assert_equals(animA.replaceState, 'active');
}, "Does NOT remove an animation after making a redundant change to another animation's timeline");

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

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

  assert_equals(animA.replaceState, 'active');

  // Set up a timeline that makes animA finished but then restore it
  animA.timeline = new DocumentTimeline({
    originTime:
      document.timeline.currentTime - 100 * MS_PER_SEC - animA.startTime,
  });
  animA.timeline = document.timeline;

  await waitForNextFrame();

  assert_equals(animA.replaceState, 'active');
}, 'Does NOT remove an animation after making a redundant change to its timeline');

promise_test(async t => {
  const div = createDiv(t);
  const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
  const animB = div.animate(
    { marginLeft: '100px' },
    {
      duration: 1,
      fill: 'forwards',
    }
  );
  await animA.finished;

  assert_equals(animA.replaceState, 'active');

  // Redundant change
  animB.effect.setKeyframes({ marginLeft: '100px', opacity: 1 });
  animB.effect.setKeyframes({ marginLeft: '100px' });

  await waitForNextFrame();

  assert_equals(animA.replaceState, 'active');
}, "Does NOT remove an animation after making a redundant change to another animation's effect's properties");

promise_test(async t => {
  const div = createDiv(t);
  const animA = div.animate(
    { marginLeft: '100px' },
    {
      duration: 1,
      fill: 'forwards',
    }
  );
  const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
  await animA.finished;

  assert_equals(animA.replaceState, 'active');

  // Redundant change
  animA.effect.setKeyframes({ opacity: 1 });
  animA.effect.setKeyframes({ marginLeft: '100px' });

  await waitForNextFrame();

  assert_equals(animA.replaceState, 'active');
}, "Does NOT remove an animation after making a redundant change to its effect's properties");

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' });
  animB.timeline = new DocumentTimeline();

  await animA.finished;

  // If, for example, we only update the timeline for animA before checking
  // replacement state, then animB will not be finished and animA will not be
  // replaced.

  assert_equals(animA.replaceState, 'removed');
  assert_equals(animB.replaceState, 'active');
}, 'Updates ALL timelines before checking for replacement');

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' });

  const events = [];
  const logEvent = (targetName, eventType) => {
    events.push(`${targetName}:${eventType}`);
  };

  animA.addEventListener('finish', () => logEvent('animA', 'finish'));
  animA.addEventListener('remove', () => logEvent('animA', 'remove'));
  animB.addEventListener('finish', () => logEvent('animB', 'finish'));
  animB.addEventListener('remove', () => logEvent('animB', 'remove'));

  await animA.finished;

  // Allow all events to be dispatched

  await waitForNextFrame();

  assert_array_equals(events, [
    'animA:finish',
    'animB:finish',
    'animA:remove',
  ]);
}, 'Dispatches remove events after finish events');

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' });

  const eventWatcher = new EventWatcher(t, animA, 'remove');

  await animA.finished;

  let rAFReceived = false;
  requestAnimationFrame(() => (rAFReceived = true));

  await eventWatcher.wait_for('remove');

  assert_false(
    rAFReceived,
    'remove event should be fired before requestAnimationFrame'
  );
}, 'Fires remove event before requestAnimationFrame');

promise_test(async t => {
  const div = createDiv(t);
  const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
  const animB = div.animate(
    { width: '100px' },
    { duration: 1, fill: 'forwards' }
  );
  const animC = div.animate(
    { opacity: 0.5, width: '200px' },
    { duration: 1, fill: 'forwards' }
  );

  // In the event handler for animA (which should be fired before that of animB)
  // we make a change to animC so that it no longer covers animB.
  //
  // If the remove event for animB is not already queued by this point, it will
  // fail to fire.
  animA.addEventListener('remove', () => {
    animC.effect.setKeyframes({
      opacity: 0.5,
    });
  });

  const eventWatcher = new EventWatcher(t, animB, 'remove');
  await eventWatcher.wait_for('remove');

  assert_equals(animA.replaceState, 'removed');
  assert_equals(animB.replaceState, 'removed');
  assert_equals(animC.replaceState, 'active');
}, 'Queues all remove events before running them');

promise_test(async t => {
  const outerIframe = document.createElement('iframe');
  outerIframe.width = 10;
  outerIframe.height = 10;
  await insertFrameAndAwaitLoad(t, outerIframe, document);

  const innerIframe = document.createElement('iframe');
  innerIframe.width = 10;
  innerIframe.height = 10;
  await insertFrameAndAwaitLoad(t, innerIframe, outerIframe.contentDocument);

  const div = createDiv(t, innerIframe.contentDocument);

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

  // Sanity check: The timeline for these animations should be the default
  // document timeline for div.
  assert_equals(animA.timeline, innerIframe.contentDocument.timeline);
  assert_equals(animB.timeline, innerIframe.contentDocument.timeline);

  await animA.finished;

  assert_equals(animA.replaceState, 'removed');
  assert_equals(animB.replaceState, 'active');
}, 'Performs removal in deeply nested iframes');

</script>