| <!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> |