summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/web-animations/timing-model/timelines
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
commit43a97878ce14b72f0981164f87f2e35e14151312 (patch)
tree620249daf56c0258faa40cbdcf9cfba06de2a846 /testing/web-platform/tests/web-animations/timing-model/timelines
parentInitial commit. (diff)
downloadfirefox-upstream.tar.xz
firefox-upstream.zip
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/web-animations/timing-model/timelines')
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/timelines/document-timelines.html50
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/timelines/resources/target-frame.html76
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/timelines/resources/timeline-frame.html20
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/timelines/sibling-iframe-timeline.html58
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/timelines/timelines.html112
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/timelines/update-and-send-events-replacement.html1017
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/timelines/update-and-send-events.html257
7 files changed, 1590 insertions, 0 deletions
diff --git a/testing/web-platform/tests/web-animations/timing-model/timelines/document-timelines.html b/testing/web-platform/tests/web-animations/timing-model/timelines/document-timelines.html
new file mode 100644
index 0000000000..c71d73331c
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/timelines/document-timelines.html
@@ -0,0 +1,50 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Document timelines</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#document-timelines">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+
+async_test(t => {
+ assert_greater_than_equal(document.timeline.currentTime, 0,
+ 'The current time is initially is positive or zero');
+
+ // document.timeline.currentTime should be set even before document
+ // load fires. We expect this code to be run before document load and hence
+ // the above assertion is sufficient.
+ // If the following assertion fails, this test needs to be redesigned.
+ assert_not_equals(document.readyState, 'complete',
+ 'Test is running prior to document load');
+
+ // Test that the document timeline's current time is measured from
+ // navigationStart.
+ //
+ // We can't just compare document.timeline.currentTime to
+ // window.performance.now() because currentTime is only updated on a sample
+ // so we use requestAnimationFrame instead.
+ window.requestAnimationFrame(rafTime => {
+ t.step(() => {
+ assert_equals(document.timeline.currentTime, rafTime,
+ 'The current time matches requestAnimationFrame time');
+ });
+ t.done();
+ });
+}, 'Document timelines report current time relative to navigationStart');
+
+async_test(t => {
+ window.requestAnimationFrame(rafTime => {
+ t.step(() => {
+ const iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ assert_greater_than_equal(iframe.contentDocument.timeline.currentTime, 0,
+ 'The current time of a new iframe is initially is positive or zero');
+ });
+ t.done();
+ });
+}, 'Child frames do not report negative initial times');
+
+</script>
diff --git a/testing/web-platform/tests/web-animations/timing-model/timelines/resources/target-frame.html b/testing/web-platform/tests/web-animations/timing-model/timelines/resources/target-frame.html
new file mode 100644
index 0000000000..18ee4fd8a2
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/timelines/resources/target-frame.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <style type="text/css">
+ #target {
+ background: green;
+ height: 50px;
+ width: 50px;
+ }
+ </style>
+</head>
+<body>
+ <div id="target"></div>
+</body>
+<script src="../../../testcommon.js"></script>
+<script type="text/javascript">
+ function sendResult(message) {
+ top.postMessage(message, '*');
+ }
+
+ function waitForAnimationReady(anim) {
+ // Resolution of the ready promise, though UA dependent, is expected to
+ // happen within a few frames. Throttling rendering of the frame owning
+ // the animation's timeline may delay resolution of the ready promise,
+ // resulting in a test failure.
+ let frameTimeout = 10;
+ let resolved = false;
+ return new Promise((resolve, reject) => {
+ anim.ready.then(() => {
+ resolved = true;
+ resolve('PASS');
+ });
+ const tick = () => {
+ requestAnimationFrame(() => {
+ if (!resolved) {
+ if (--frameTimeout == 0)
+ resolve('FAIL: Animation is still pending');
+ else
+ tick();
+ }
+ });
+ };
+ tick();
+ });
+ }
+
+ function verifyAnimationIsUpdating() {
+ return new Promise(resolve => {
+ waitForAnimationFrames(3).then(() => {
+ const opacity = getComputedStyle(target).opacity;
+ const result =
+ (opacity == 1) ? 'FAIL: opacity remained unchanged' : 'PASS';
+ resolve(result);
+ });
+ });
+ }
+
+ async function runTest() {
+ const anim = document.getAnimations()[0];
+ if (!anim) {
+ setResult('FAIL: Failed to create animation');
+ return;
+ }
+ waitForAnimationReady(anim).then(result => {
+ if (result != 'PASS') {
+ sendResult(result);
+ return;
+ }
+ verifyAnimationIsUpdating().then(result => {
+ sendResult(result);
+ });
+ });
+ }
+</script>
+</html>
diff --git a/testing/web-platform/tests/web-animations/timing-model/timelines/resources/timeline-frame.html b/testing/web-platform/tests/web-animations/timing-model/timelines/resources/timeline-frame.html
new file mode 100644
index 0000000000..9c8cdabc9d
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/timelines/resources/timeline-frame.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+</head>
+<script type="text/javascript">
+ const targetWindow = window.top.a;
+ const element = targetWindow.document.getElementById('target');
+ const keyframes = { opacity: [1, 0.2] };
+ const options = {
+ duration: 1000,
+ // Use this document's timeline rather then the timeline of the
+ // element's document.
+ timeline: document.timeline,
+ fill: 'forwards'
+ };
+ element.animate(keyframes, options);
+ targetWindow.runTest();
+</script>
+</html>
diff --git a/testing/web-platform/tests/web-animations/timing-model/timelines/sibling-iframe-timeline.html b/testing/web-platform/tests/web-animations/timing-model/timelines/sibling-iframe-timeline.html
new file mode 100644
index 0000000000..8a611c8579
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/timelines/sibling-iframe-timeline.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Animate using sibling iframe's timeline</title>
+</head>
+<body></body>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script type="text/javascript">
+ 'use strict';
+
+ function crossSiteUrl(filename) {
+ const url =
+ get_host_info().HTTP_REMOTE_ORIGIN +
+ '/web-animations/timing-model/timelines/resources/' +
+ filename;
+ return url;
+ }
+
+ function loadFrame(name, path, hidden) {
+ return new Promise(resolve => {
+ const frame = document.createElement('iframe');
+ if (hidden)
+ frame.style = 'visibility: hidden;';
+ frame.name = name;
+ document.body.appendChild(frame);
+ frame.onload = () => {
+ resolve();
+ }
+ frame.src = crossSiteUrl(path);
+ });
+ }
+
+ function waitForTestResults() {
+ return new Promise(resolve => {
+ const listener = (evt) => {
+ window.removeEventListener('message', listener);
+ resolve(evt.data);
+ };
+ window.addEventListener('message', listener);
+ });
+ }
+
+ promise_test(async t => {
+ const promise = waitForTestResults().then((data) => {
+ assert_equals(data, 'PASS');
+ });
+ // Animate an element in frame A.
+ await loadFrame('a', 'target-frame.html', false);
+ // Animation's timeline is in hidden frame B.
+ await loadFrame('b', 'timeline-frame.html', true);
+
+ return promise;
+ }, 'animation tied to another frame\'s timeline runs properly');
+</script>
+</html>
diff --git a/testing/web-platform/tests/web-animations/timing-model/timelines/timelines.html b/testing/web-platform/tests/web-animations/timing-model/timelines/timelines.html
new file mode 100644
index 0000000000..d570eed5c2
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/timelines/timelines.html
@@ -0,0 +1,112 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Timelines</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#timelines">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<style>
+@keyframes opacity-animation {
+ from { opacity: 1; }
+ to { opacity: 0; }
+}
+</style>
+<div id="log"></div>
+<script>
+'use strict';
+
+promise_test(t => {
+ const valueAtStart = document.timeline.currentTime;
+ const timeAtStart = window.performance.now();
+ while (window.performance.now() - timeAtStart < 50) {
+ // Wait 50ms
+ }
+ assert_equals(document.timeline.currentTime, valueAtStart,
+ 'Timeline time does not change within an animation frame');
+ return waitForAnimationFrames(1).then(() => {
+ assert_greater_than(document.timeline.currentTime, valueAtStart,
+ 'Timeline time increases between animation frames');
+ });
+}, 'Timeline time increases once per animation frame');
+
+async_test(t => {
+ const iframe = document.createElement('iframe');
+ iframe.width = 10;
+ iframe.height = 10;
+
+ iframe.addEventListener('load', t.step_func(() => {
+ const iframeTimeline = iframe.contentDocument.timeline;
+ const valueAtStart = iframeTimeline.currentTime;
+ const timeAtStart = window.performance.now();
+ while (iframe.contentWindow.performance.now() - timeAtStart < 50) {
+ // Wait 50ms
+ }
+ assert_equals(iframeTimeline.currentTime, valueAtStart,
+ 'Timeline time within an iframe does not change within an '
+ + ' animation frame');
+
+ iframe.contentWindow.requestAnimationFrame(t.step_func_done(() => {
+ assert_greater_than(iframeTimeline.currentTime, valueAtStart,
+ 'Timeline time within an iframe increases between animation frames');
+ iframe.remove();
+ }));
+ }));
+
+ document.body.appendChild(iframe);
+}, 'Timeline time increases once per animation frame in an iframe');
+
+async_test(t => {
+ const startTime = document.timeline.currentTime;
+ let firstRafTime;
+
+ requestAnimationFrame(() => {
+ t.step(() => {
+ assert_greater_than_equal(document.timeline.currentTime, startTime,
+ 'Timeline time should have progressed');
+ firstRafTime = document.timeline.currentTime;
+ });
+ });
+
+ requestAnimationFrame(() => {
+ t.step(() => {
+ assert_equals(document.timeline.currentTime, firstRafTime,
+ 'Timeline time should be the same');
+ });
+ t.done();
+ });
+}, 'Timeline time should be the same for all RAF callbacks in an animation'
+ + ' frame');
+
+async_test(t => {
+ const div = createDiv(t);
+ const animation = div.animate(null, 100 * MS_PER_SEC);
+
+ animation.ready.then(t.step_func(() => {
+ const readyTimelineTime = document.timeline.currentTime;
+ requestAnimationFrame(t.step_func_done(() => {
+ assert_equals(readyTimelineTime, document.timeline.currentTime,
+ 'There should be a microtask checkpoint');
+ }));
+ }));
+}, 'Performs a microtask checkpoint after updating timelins');
+
+async_test(t => {
+ const div = createDiv(t);
+ let readyPromiseRan = false;
+ let finishedPromiseRan = false;
+ div.style.animation = 'opacity-animation 1ms';
+ let anim = div.getAnimations()[0];
+ anim.ready.then(t.step_func(() => {
+ readyPromiseRan = true;
+ }));
+ div.addEventListener('animationstart', t.step_func(() => {
+ assert_true(readyPromiseRan, 'It should run ready promise before animationstart event');
+ }));
+ anim.finished.then(t.step_func(() => {
+ finishedPromiseRan = true;
+ }));
+ div.addEventListener('animationend', t.step_func_done(() => {
+ assert_true(finishedPromiseRan, 'It should run finished promise before animationend event');
+ }));
+}, 'Runs finished promise before animation events');
+</script>
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>
diff --git a/testing/web-platform/tests/web-animations/timing-model/timelines/update-and-send-events.html b/testing/web-platform/tests/web-animations/timing-model/timelines/update-and-send-events.html
new file mode 100644
index 0000000000..255e013f27
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/timelines/update-and-send-events.html
@@ -0,0 +1,257 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Update animations and send events</title>
+<meta name="timeout" content="long">
+<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>
+<div id="log"></div>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const div = createDiv(t);
+ const animation = div.animate(null, 100 * MS_PER_SEC);
+
+ // The ready promise should be resolved as part of micro-task checkpoint
+ // after updating the current time of all timeslines in the procedure to
+ // "update animations and send events".
+ await animation.ready;
+
+ let rAFReceived = false;
+ requestAnimationFrame(() => rAFReceived = true);
+
+ const eventWatcher = new EventWatcher(t, animation, 'cancel');
+ animation.cancel();
+
+ await eventWatcher.wait_for('cancel');
+
+ assert_false(rAFReceived,
+ 'cancel event should be fired before requestAnimationFrame');
+}, 'Fires cancel event before requestAnimationFrame');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ const animation = div.animate(null, 100 * MS_PER_SEC);
+
+ // Like the above test, the ready promise should be resolved micro-task
+ // checkpoint after updating the current time of all timeslines in the
+ // procedure to "update animations and send events".
+ await animation.ready;
+
+ let rAFReceived = false;
+ requestAnimationFrame(() => rAFReceived = true);
+
+ const eventWatcher = new EventWatcher(t, animation, 'finish');
+ animation.finish();
+
+ await eventWatcher.wait_for('finish');
+
+ assert_false(rAFReceived,
+ 'finish event should be fired before requestAnimationFrame');
+}, 'Fires finish event before requestAnimationFrame');
+
+function animationType(anim) {
+ if (anim instanceof CSSAnimation) {
+ return 'CSSAnimation';
+ } else if (anim instanceof CSSTransition) {
+ return 'CSSTransition';
+ } else {
+ return 'ScriptAnimation';
+ }
+}
+
+promise_test(async t => {
+ createStyle(t, { '@keyframes anim': '' });
+ const div = createDiv(t);
+
+ getComputedStyle(div).marginLeft;
+ div.style = 'animation: anim 100s; ' +
+ 'transition: margin-left 100s; ' +
+ 'margin-left: 100px;';
+ div.animate(null, 100 * MS_PER_SEC);
+ const animations = div.getAnimations();
+
+ let receivedEvents = [];
+ animations.forEach(anim => {
+ anim.onfinish = event => {
+ receivedEvents.push({
+ type: animationType(anim) + ':' + event.type,
+ timeStamp: event.timeStamp
+ });
+ };
+ });
+
+ await Promise.all(animations.map(anim => anim.ready));
+
+ // Setting current time to the time just before the effect end.
+ animations.forEach(anim => anim.currentTime = 100 * MS_PER_SEC - 1);
+
+ await waitForNextFrame();
+
+ assert_array_equals(receivedEvents.map(event => event.type),
+ [ 'CSSTransition:finish', 'CSSAnimation:finish',
+ 'ScriptAnimation:finish' ],
+ 'finish events for various animation type should be sorted by composite ' +
+ 'order');
+}, 'Sorts finish events by composite order');
+
+promise_test(async t => {
+ createStyle(t, { '@keyframes anim': '' });
+ const div = createDiv(t);
+
+ let receivedEvents = [];
+ function receiveEvent(type, timeStamp) {
+ receivedEvents.push({ type, timeStamp });
+ }
+
+ div.onanimationcancel = event => receiveEvent(event.type, event.timeStamp);
+ div.ontransitioncancel = event => receiveEvent(event.type, event.timeStamp);
+
+ getComputedStyle(div).marginLeft;
+ div.style = 'animation: anim 100s; ' +
+ 'transition: margin-left 100s; ' +
+ 'margin-left: 100px;';
+ div.animate(null, 100 * MS_PER_SEC);
+ const animations = div.getAnimations();
+
+ animations.forEach(anim => {
+ anim.oncancel = event => {
+ receiveEvent(animationType(anim) + ':' + event.type, event.timeStamp);
+ };
+ });
+
+ await Promise.all(animations.map(anim => anim.ready));
+
+ const timeInAnimationReady = document.timeline.currentTime;
+
+ // Call cancel() in reverse composite order. I.e. canceling for script
+ // animation happen first, then for CSS animation and CSS transition.
+ // 'cancel' events for these animations should be sorted by composite
+ // order.
+ animations.reverse().forEach(anim => anim.cancel());
+
+ // requestAnimationFrame callback which is actually the _same_ frame since we
+ // are currently operating in the `ready` callbac of the animations which
+ // happens as part of the "Update animations and send events" procedure
+ // _before_ we run animation frame callbacks.
+ await waitForAnimationFrames(1);
+
+ assert_times_equal(timeInAnimationReady, document.timeline.currentTime,
+ 'A rAF callback should happen in the same frame');
+
+ assert_array_equals(receivedEvents.map(event => event.type),
+ // This ordering needs more clarification in the spec, but the intention is
+ // that the cancel playback event fires before the equivalent CSS cancel
+ // event in each case.
+ [ 'CSSTransition:cancel', 'CSSAnimation:cancel', 'ScriptAnimation:cancel',
+ 'transitioncancel', 'animationcancel' ],
+ 'cancel events should be sorted by composite order');
+}, 'Sorts cancel events by composite order');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ getComputedStyle(div).marginLeft;
+ div.style = 'transition: margin-left 100s; margin-left: 100px;';
+ const anim = div.getAnimations()[0];
+
+ let receivedEvents = [];
+ anim.oncancel = event => receivedEvents.push(event);
+
+ const eventWatcher = new EventWatcher(t, div, 'transitionstart');
+ await eventWatcher.wait_for('transitionstart');
+
+ const timeInEventCallback = document.timeline.currentTime;
+
+ // Calling cancel() queues a cancel event
+ anim.cancel();
+
+ await waitForAnimationFrames(1);
+ assert_times_equal(timeInEventCallback, document.timeline.currentTime,
+ 'A rAF callback should happen in the same frame');
+
+ assert_array_equals(receivedEvents, [],
+ 'The queued cancel event shouldn\'t be dispatched in the same frame');
+
+ await waitForAnimationFrames(1);
+ assert_array_equals(receivedEvents.map(event => event.type), ['cancel'],
+ 'The cancel event should be dispatched in a later frame');
+}, 'Queues a cancel event in transitionstart event callback');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ getComputedStyle(div).marginLeft;
+ div.style = 'transition: margin-left 100s; margin-left: 100px;';
+ const anim = div.getAnimations()[0];
+
+ let receivedEvents = [];
+ anim.oncancel = event => receivedEvents.push(event);
+ div.ontransitioncancel = event => receivedEvents.push(event);
+
+ await anim.ready;
+
+ anim.cancel();
+
+ await waitForAnimationFrames(1);
+
+ assert_array_equals(receivedEvents.map(event => event.type),
+ [ 'cancel', 'transitioncancel' ],
+ 'Playback and CSS events for the same transition should be sorted by ' +
+ 'schedule event time and composite order');
+}, 'Sorts events for the same transition');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ const anim = div.animate(null, 100 * MS_PER_SEC);
+
+ let receivedEvents = [];
+ anim.oncancel = event => receivedEvents.push(event);
+ anim.onfinish = event => receivedEvents.push(event);
+
+ await anim.ready;
+
+ anim.finish();
+ anim.cancel();
+
+ await waitForAnimationFrames(1);
+
+ assert_array_equals(receivedEvents.map(event => event.type),
+ [ 'finish', 'cancel' ],
+ 'Calling finish() synchronously queues a finish event when updating the ' +
+ 'finish state so it should appear before the cancel event');
+}, 'Playback events with the same timeline retain the order in which they are' +
+ 'queued');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ // Create two animations with separate timelines
+
+ const timelineA = document.timeline;
+ const animA = div.animate(null, 100 * MS_PER_SEC);
+
+ const timelineB = new DocumentTimeline();
+ const animB = new Animation(
+ new KeyframeEffect(div, null, 100 * MS_PER_SEC),
+ timelineB
+ );
+ animB.play();
+
+ animA.currentTime = 99.9 * MS_PER_SEC;
+ animB.currentTime = 99.9 * MS_PER_SEC;
+
+ // When the next tick happens both animations should be updated, and we will
+ // notice that they are now finished. As a result their finished promise
+ // callbacks should be queued. All of that should happen before we run the
+ // next microtask checkpoint and actually run the promise callbacks and
+ // hence the calls to cancel should not stop the existing callbacks from
+ // being run.
+
+ animA.finished.then(() => { animB.cancel() });
+ animB.finished.then(() => { animA.cancel() });
+
+ await Promise.all([animA.finished, animB.finished]);
+}, 'All timelines are updated before running microtasks');
+
+</script>