diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /testing/web-platform/tests/web-animations/timing-model/timelines | |
parent | Initial commit. (diff) | |
download | firefox-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')
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> |