style-change-events.html (11238B)
1 <!doctype html> 2 <meta charset=utf-8> 3 <title>Animation interface: style change events</title> 4 <link rel="help" 5 href="https://drafts.csswg.org/web-animations-1/#model-liveness"> 6 <script src="/resources/testharness.js"></script> 7 <script src="/resources/testharnessreport.js"></script> 8 <script src="../../testcommon.js"></script> 9 <body> 10 <div id="log"></div> 11 <script> 12 'use strict'; 13 14 // Test that each property defined in the Animation interface behaves as 15 // expected with regards to whether or not it produces style change events. 16 // 17 // There are two types of tests: 18 // 19 // PlayAnimationTest 20 // 21 // For properties that are able to cause the Animation to start affecting 22 // the target CSS property. 23 // 24 // This function takes either: 25 // 26 // (a) A function that simply "plays" that passed-in Animation (i.e. makes 27 // it start affecting the target CSS property. 28 // 29 // (b) An object with the following format: 30 // 31 // { 32 // setup: elem => { /* return Animation */ }, 33 // test: animation => { /* play |animation| */ }, 34 // shouldFlush: boolean /* optional, defaults to false */ 35 // } 36 // 37 // If the latter form is used, the setup function should return an Animation 38 // that does NOT (yet) have an in-effect AnimationEffect that affects the 39 // 'opacity' property. Otherwise, the transition we use to detect if a style 40 // change event has occurred will never have a chance to be triggered (since 41 // the animated style will clobber both before-change and after-change 42 // style). 43 // 44 // Examples of valid animations: 45 // 46 // - An animation that is idle, or finished but without a fill mode. 47 // - An animation with an effect that that does not affect opacity. 48 // 49 // UsePropertyTest 50 // 51 // For properties that cannot cause the Animation to start affecting the 52 // target CSS property. 53 // 54 // The shape of the parameter to the UsePropertyTest is identical to the 55 // PlayAnimationTest. The only difference is that the function (or 'test' 56 // function of the object format is used) does not need to play the 57 // animation, but simply needs to get/set the property under test. 58 59 const PlayAnimationTest = testFuncOrObj => { 60 let test, setup, shouldFlush; 61 62 if (typeof testFuncOrObj === 'function') { 63 test = testFuncOrObj; 64 shouldFlush = false; 65 } else { 66 test = testFuncOrObj.test; 67 if (typeof testFuncOrObj.setup === 'function') { 68 setup = testFuncOrObj.setup; 69 } 70 shouldFlush = !!testFuncOrObj.shouldFlush; 71 } 72 73 if (!setup) { 74 setup = elem => 75 new Animation( 76 new KeyframeEffect(elem, { opacity: [0, 1] }, 100 * MS_PER_SEC) 77 ); 78 } 79 80 return { test, setup, shouldFlush }; 81 }; 82 83 const UsePropertyTest = testFuncOrObj => { 84 const { setup, test, shouldFlush } = PlayAnimationTest(testFuncOrObj); 85 86 let coveringAnimation; 87 return { 88 setup: elem => { 89 coveringAnimation = new Animation( 90 new KeyframeEffect(elem, { opacity: [0, 1] }, 100 * MS_PER_SEC) 91 ); 92 93 return setup(elem); 94 }, 95 test: animation => { 96 test(animation); 97 coveringAnimation.play(); 98 }, 99 shouldFlush, 100 }; 101 }; 102 103 const tests = { 104 id: UsePropertyTest(animation => (animation.id = 'yer')), 105 get effect() { 106 let effect; 107 return PlayAnimationTest({ 108 setup: elem => { 109 // Create a new effect and animation but don't associate them yet 110 effect = new KeyframeEffect( 111 elem, 112 { opacity: [0.5, 1] }, 113 100 * MS_PER_SEC 114 ); 115 return elem.animate(null, 100 * MS_PER_SEC); 116 }, 117 test: animation => { 118 // Read the effect 119 animation.effect; 120 121 // Assign the effect 122 animation.effect = effect; 123 }, 124 }); 125 }, 126 timeline: PlayAnimationTest({ 127 setup: elem => { 128 // Create a new animation with no timeline 129 const animation = new Animation( 130 new KeyframeEffect(elem, { opacity: [0.5, 1] }, 100 * MS_PER_SEC), 131 null 132 ); 133 // Set the hold time so that once we assign a timeline it will begin to 134 // play. 135 animation.currentTime = 0; 136 137 return animation; 138 }, 139 test: animation => { 140 // Get the timeline 141 animation.timeline; 142 143 // Play the animation by setting the timeline 144 animation.timeline = document.timeline; 145 }, 146 }), 147 startTime: PlayAnimationTest(animation => { 148 // Get the startTime 149 animation.startTime; 150 151 // Play the animation by setting the startTime 152 animation.startTime = document.timeline.currentTime; 153 }), 154 currentTime: PlayAnimationTest(animation => { 155 // Get the currentTime 156 animation.currentTime; 157 158 // Play the animation by setting the currentTime 159 animation.currentTime = 0; 160 }), 161 playbackRate: UsePropertyTest(animation => { 162 // Get and set the playbackRate 163 animation.playbackRate = animation.playbackRate * 1.1; 164 }), 165 playState: UsePropertyTest(animation => animation.playState), 166 pending: UsePropertyTest(animation => animation.pending), 167 // Strictly speaking, rangeStart and rangeEnd can change whether the effect 168 // is active, but only if the animation has a view timeline. Otherwise, it has 169 // no effect. 170 rangeStart: UsePropertyTest(animation => animation.rangeStart), 171 rangeEnd: UsePropertyTest(animation => animation.rangeEnd), 172 overallProgress: UsePropertyTest(animation => animation.overallProgress), 173 replaceState: UsePropertyTest(animation => animation.replaceState), 174 ready: UsePropertyTest(animation => animation.ready), 175 finished: UsePropertyTest(animation => { 176 // Get the finished Promise 177 animation.finished; 178 }), 179 onfinish: UsePropertyTest(animation => { 180 // Get the onfinish member 181 animation.onfinish; 182 183 // Set the onfinish menber 184 animation.onfinish = () => {}; 185 }), 186 onremove: UsePropertyTest(animation => { 187 // Get the onremove member 188 animation.onremove; 189 190 // Set the onremove menber 191 animation.onremove = () => {}; 192 }), 193 oncancel: UsePropertyTest(animation => { 194 // Get the oncancel member 195 animation.oncancel; 196 197 // Set the oncancel menber 198 animation.oncancel = () => {}; 199 }), 200 cancel: UsePropertyTest({ 201 // Animate _something_ just to make the test more interesting 202 setup: elem => elem.animate({ color: ['green', 'blue'] }, 100 * MS_PER_SEC), 203 test: animation => { 204 animation.cancel(); 205 }, 206 }), 207 finish: PlayAnimationTest({ 208 setup: elem => 209 new Animation( 210 new KeyframeEffect( 211 elem, 212 { opacity: [0.5, 1] }, 213 { 214 duration: 100 * MS_PER_SEC, 215 fill: 'both', 216 } 217 ) 218 ), 219 test: animation => { 220 animation.finish(); 221 }, 222 }), 223 play: PlayAnimationTest(animation => animation.play()), 224 pause: PlayAnimationTest(animation => { 225 // Pause animation -- this will cause the animation to transition from the 226 // 'idle' state to the 'paused' (but pending) state with hold time zero. 227 animation.pause(); 228 }), 229 updatePlaybackRate: UsePropertyTest(animation => { 230 animation.updatePlaybackRate(1.1); 231 }), 232 // We would like to use a PlayAnimationTest here but reverse() is async and 233 // doesn't start applying its result until the animation is ready. 234 reverse: UsePropertyTest({ 235 setup: elem => { 236 // Create a new animation and seek it to the end so that it no longer 237 // affects style (since it has no fill mode). 238 const animation = elem.animate({ opacity: [0.5, 1] }, 100 * MS_PER_SEC); 239 animation.finish(); 240 return animation; 241 }, 242 test: animation => { 243 animation.reverse(); 244 }, 245 }), 246 persist: PlayAnimationTest({ 247 setup: async elem => { 248 // Create an animation whose replaceState is 'removed'. 249 const animA = elem.animate( 250 { opacity: 1 }, 251 { duration: 1, fill: 'forwards' } 252 ); 253 const animB = elem.animate( 254 { opacity: 1 }, 255 { duration: 1, fill: 'forwards' } 256 ); 257 await animA.finished; 258 animB.cancel(); 259 260 return animA; 261 }, 262 test: animation => { 263 animation.persist(); 264 }, 265 }), 266 commitStyles: PlayAnimationTest({ 267 setup: async elem => { 268 // Create an animation whose replaceState is 'removed'. 269 const animA = elem.animate( 270 // It's important to use opacity of '1' here otherwise we'll create a 271 // transition due to updating the specified style whereas the transition 272 // we want to detect is the one from flushing due to calling 273 // commitStyles. 274 { opacity: 1 }, 275 { duration: 1, fill: 'forwards' } 276 ); 277 const animB = elem.animate( 278 { opacity: 1 }, 279 { duration: 1, fill: 'forwards' } 280 ); 281 await animA.finished; 282 animB.cancel(); 283 284 return animA; 285 }, 286 test: animation => { 287 animation.commitStyles(); 288 }, 289 shouldFlush: true, 290 }), 291 get ['Animation constructor']() { 292 let originalElem; 293 return UsePropertyTest({ 294 setup: elem => { 295 originalElem = elem; 296 // Return a dummy animation so the caller has something to wait on 297 return elem.animate(null); 298 }, 299 test: () => 300 new Animation( 301 new KeyframeEffect( 302 originalElem, 303 { opacity: [0.5, 1] }, 304 100 * MS_PER_SEC 305 ) 306 ), 307 }); 308 }, 309 }; 310 311 // Check that each enumerable property and the constructor follow the 312 // expected behavior with regards to triggering style change events. 313 const properties = [ 314 ...Object.keys(Animation.prototype), 315 'Animation constructor', 316 ]; 317 318 test(() => { 319 for (const property of Object.keys(tests)) { 320 assert_in_array( 321 property, 322 properties, 323 `Test property '${property}' should be one of the properties on ` + 324 ' Animation' 325 ); 326 } 327 }, 'All property keys are recognized'); 328 329 for (const key of properties) { 330 promise_test(async t => { 331 assert_own_property(tests, key, `Should have a test for '${key}' property`); 332 const { setup, test, shouldFlush } = tests[key]; 333 334 // Setup target element 335 const div = createDiv(t); 336 let gotTransition = false; 337 div.addEventListener('transitionrun', () => { 338 gotTransition = true; 339 }); 340 341 // Setup animation 342 const animation = await setup(div); 343 344 // Setup transition start point 345 div.style.transition = 'opacity 100s'; 346 getComputedStyle(div).opacity; 347 348 // Update specified style but don't flush 349 div.style.opacity = '0.5'; 350 351 // Trigger the property 352 test(animation); 353 354 // If the test function produced a style change event it will have triggered 355 // a transition. 356 357 // Wait for the animation to start and then for at least two animation 358 // frames to give the transitionrun event a chance to be dispatched. 359 assert_true( 360 typeof animation.ready !== 'undefined', 361 'Should have a valid animation to wait on' 362 ); 363 await animation.ready; 364 await waitForAnimationFrames(2); 365 366 if (shouldFlush) { 367 assert_true(gotTransition, 'A transition should have been triggered'); 368 } else { 369 assert_false( 370 gotTransition, 371 'A transition should NOT have been triggered' 372 ); 373 } 374 }, `Animation.${key} produces expected style change events`); 375 } 376 </script> 377 </body>