summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/web-animations/timing-model/timelines/update-and-send-events-replacement.html
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/web-animations/timing-model/timelines/update-and-send-events-replacement.html')
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/timelines/update-and-send-events-replacement.html1017
1 files changed, 1017 insertions, 0 deletions
diff --git a/testing/web-platform/tests/web-animations/timing-model/timelines/update-and-send-events-replacement.html b/testing/web-platform/tests/web-animations/timing-model/timelines/update-and-send-events-replacement.html
new file mode 100644
index 0000000000..d6ed734831
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/timelines/update-and-send-events-replacement.html
@@ -0,0 +1,1017 @@
+<!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>