update-and-send-events.html (8690B)
1 <!doctype html> 2 <meta charset=utf-8> 3 <title>Update animations and send events</title> 4 <meta name="timeout" content="long"> 5 <link rel="help" href="https://drafts.csswg.org/web-animations/#update-animations-and-send-events"> 6 <script src="/resources/testharness.js"></script> 7 <script src="/resources/testharnessreport.js"></script> 8 <script src="../../testcommon.js"></script> 9 <div id="log"></div> 10 <script> 11 'use strict'; 12 13 promise_test(async t => { 14 const div = createDiv(t); 15 const animation = div.animate(null, 100 * MS_PER_SEC); 16 17 // The ready promise should be resolved as part of micro-task checkpoint 18 // after updating the current time of all timeslines in the procedure to 19 // "update animations and send events". 20 await animation.ready; 21 22 let rAFReceived = false; 23 requestAnimationFrame(() => rAFReceived = true); 24 25 const eventWatcher = new EventWatcher(t, animation, 'cancel'); 26 animation.cancel(); 27 28 await eventWatcher.wait_for('cancel'); 29 30 assert_false(rAFReceived, 31 'cancel event should be fired before requestAnimationFrame'); 32 }, 'Fires cancel event before requestAnimationFrame'); 33 34 promise_test(async t => { 35 const div = createDiv(t); 36 const animation = div.animate(null, 100 * MS_PER_SEC); 37 38 // Like the above test, the ready promise should be resolved micro-task 39 // checkpoint after updating the current time of all timeslines in the 40 // procedure to "update animations and send events". 41 await animation.ready; 42 43 let rAFReceived = false; 44 requestAnimationFrame(() => rAFReceived = true); 45 46 const eventWatcher = new EventWatcher(t, animation, 'finish'); 47 animation.finish(); 48 49 await eventWatcher.wait_for('finish'); 50 51 assert_false(rAFReceived, 52 'finish event should be fired before requestAnimationFrame'); 53 }, 'Fires finish event before requestAnimationFrame'); 54 55 function animationType(anim) { 56 if (anim instanceof CSSAnimation) { 57 return 'CSSAnimation'; 58 } else if (anim instanceof CSSTransition) { 59 return 'CSSTransition'; 60 } else { 61 return 'ScriptAnimation'; 62 } 63 } 64 65 promise_test(async t => { 66 createStyle(t, { '@keyframes anim': '' }); 67 const div = createDiv(t); 68 69 getComputedStyle(div).marginLeft; 70 div.style = 'animation: anim 100s; ' + 71 'transition: margin-left 100s; ' + 72 'margin-left: 100px;'; 73 div.animate(null, 100 * MS_PER_SEC); 74 const animations = div.getAnimations(); 75 76 let receivedEvents = []; 77 animations.forEach(anim => { 78 anim.onfinish = event => { 79 receivedEvents.push({ 80 type: animationType(anim) + ':' + event.type, 81 timeStamp: event.timeStamp 82 }); 83 }; 84 }); 85 86 await Promise.all(animations.map(anim => anim.ready)); 87 88 // Setting current time to the time just before the effect end. 89 animations.forEach(anim => anim.currentTime = 100 * MS_PER_SEC - 1); 90 91 await waitForNextFrame(); 92 93 assert_array_equals(receivedEvents.map(event => event.type), 94 [ 'CSSTransition:finish', 'CSSAnimation:finish', 95 'ScriptAnimation:finish' ], 96 'finish events for various animation type should be sorted by composite ' + 97 'order'); 98 }, 'Sorts finish events by composite order'); 99 100 promise_test(async t => { 101 createStyle(t, { '@keyframes anim': '' }); 102 const div = createDiv(t); 103 104 let receivedEvents = []; 105 function receiveEvent(type, timeStamp) { 106 receivedEvents.push({ type, timeStamp }); 107 } 108 109 div.onanimationcancel = event => receiveEvent(event.type, event.timeStamp); 110 div.ontransitioncancel = event => receiveEvent(event.type, event.timeStamp); 111 112 getComputedStyle(div).marginLeft; 113 div.style = 'animation: anim 100s; ' + 114 'transition: margin-left 100s; ' + 115 'margin-left: 100px;'; 116 div.animate(null, 100 * MS_PER_SEC); 117 const animations = div.getAnimations(); 118 119 animations.forEach(anim => { 120 anim.oncancel = event => { 121 receiveEvent(animationType(anim) + ':' + event.type, event.timeStamp); 122 }; 123 }); 124 125 await Promise.all(animations.map(anim => anim.ready)); 126 127 const timeInAnimationReady = document.timeline.currentTime; 128 129 // Call cancel() in reverse composite order. I.e. canceling for script 130 // animation happen first, then for CSS animation and CSS transition. 131 // 'cancel' events for these animations should be sorted by composite 132 // order. 133 animations.reverse().forEach(anim => anim.cancel()); 134 135 // requestAnimationFrame callback which is actually the _same_ frame since we 136 // are currently operating in the `ready` callbac of the animations which 137 // happens as part of the "Update animations and send events" procedure 138 // _before_ we run animation frame callbacks. 139 await waitForAnimationFrames(1); 140 141 assert_times_equal(timeInAnimationReady, document.timeline.currentTime, 142 'A rAF callback should happen in the same frame'); 143 144 assert_array_equals(receivedEvents.map(event => event.type), 145 // This ordering needs more clarification in the spec, but the intention is 146 // that the cancel playback event fires before the equivalent CSS cancel 147 // event in each case. 148 [ 'CSSTransition:cancel', 'CSSAnimation:cancel', 'ScriptAnimation:cancel', 149 'transitioncancel', 'animationcancel' ], 150 'cancel events should be sorted by composite order'); 151 }, 'Sorts cancel events by composite order'); 152 153 promise_test(async t => { 154 const div = createDiv(t); 155 getComputedStyle(div).marginLeft; 156 div.style = 'transition: margin-left 100s; margin-left: 100px;'; 157 const anim = div.getAnimations()[0]; 158 159 let receivedEvents = []; 160 anim.oncancel = event => receivedEvents.push(event); 161 162 const eventWatcher = new EventWatcher(t, div, 'transitionstart'); 163 await eventWatcher.wait_for('transitionstart'); 164 165 const timeInEventCallback = document.timeline.currentTime; 166 167 // Calling cancel() queues a cancel event 168 anim.cancel(); 169 170 await waitForAnimationFrames(1); 171 assert_times_equal(timeInEventCallback, document.timeline.currentTime, 172 'A rAF callback should happen in the same frame'); 173 174 assert_array_equals(receivedEvents, [], 175 'The queued cancel event shouldn\'t be dispatched in the same frame'); 176 177 await waitForAnimationFrames(1); 178 assert_array_equals(receivedEvents.map(event => event.type), ['cancel'], 179 'The cancel event should be dispatched in a later frame'); 180 }, 'Queues a cancel event in transitionstart event callback'); 181 182 promise_test(async t => { 183 const div = createDiv(t); 184 getComputedStyle(div).marginLeft; 185 div.style = 'transition: margin-left 100s; margin-left: 100px;'; 186 const anim = div.getAnimations()[0]; 187 188 let receivedEvents = []; 189 anim.oncancel = event => receivedEvents.push(event); 190 div.ontransitioncancel = event => receivedEvents.push(event); 191 192 await anim.ready; 193 194 anim.cancel(); 195 196 await waitForAnimationFrames(1); 197 198 assert_array_equals(receivedEvents.map(event => event.type), 199 [ 'cancel', 'transitioncancel' ], 200 'Playback and CSS events for the same transition should be sorted by ' + 201 'schedule event time and composite order'); 202 }, 'Sorts events for the same transition'); 203 204 promise_test(async t => { 205 const div = createDiv(t); 206 const anim = div.animate(null, 100 * MS_PER_SEC); 207 208 let receivedEvents = []; 209 anim.oncancel = event => receivedEvents.push(event); 210 anim.onfinish = event => receivedEvents.push(event); 211 212 await anim.ready; 213 214 anim.finish(); 215 anim.cancel(); 216 217 await waitForAnimationFrames(1); 218 219 assert_array_equals(receivedEvents.map(event => event.type), 220 [ 'finish', 'cancel' ], 221 'Calling finish() synchronously queues a finish event when updating the ' + 222 'finish state so it should appear before the cancel event'); 223 }, 'Playback events with the same timeline retain the order in which they are' + 224 'queued'); 225 226 promise_test(async t => { 227 const div = createDiv(t); 228 229 // Create two animations with separate timelines 230 231 const timelineA = document.timeline; 232 const animA = div.animate(null, 100 * MS_PER_SEC); 233 234 const timelineB = new DocumentTimeline(); 235 const animB = new Animation( 236 new KeyframeEffect(div, null, 100 * MS_PER_SEC), 237 timelineB 238 ); 239 animB.play(); 240 241 animA.currentTime = 99.9 * MS_PER_SEC; 242 animB.currentTime = 99.9 * MS_PER_SEC; 243 244 // When the next tick happens both animations should be updated, and we will 245 // notice that they are now finished. As a result their finished promise 246 // callbacks should be queued. All of that should happen before we run the 247 // next microtask checkpoint and actually run the promise callbacks and 248 // hence the calls to cancel should not stop the existing callbacks from 249 // being run. 250 251 animA.finished.then(() => { animB.cancel() }); 252 animB.finished.then(() => { animA.cancel() }); 253 254 await Promise.all([animA.finished, animB.finished]); 255 }, 'All timelines are updated before running microtasks'); 256 257 </script>