test_animation_observers_async.html (23808B)
1 <!DOCTYPE html> 2 <meta charset=utf-8> 3 <title> 4 Test chrome-only MutationObserver animation notifications (async tests) 5 </title> 6 <!-- 7 8 This file contains tests for animation mutation observers that require 9 some asynchronous steps (e.g. waiting for animation events). 10 11 Where possible, however, we prefer to write synchronous tests since they are 12 less to timeout when run on automation. These synchronous tests are located 13 in test_animation_observers_sync.html. 14 15 --> 16 <script type="application/javascript" src="../testharness.js"></script> 17 <script type="application/javascript" src="../testharnessreport.js"></script> 18 <script src="../testcommon.js"></script> 19 <div id="log"></div> 20 <style> 21 @keyframes anim { 22 to { transform: translate(100px); } 23 } 24 @keyframes anotherAnim { 25 to { transform: translate(0px); } 26 } 27 #target { 28 width: 100px; 29 height: 100px; 30 background-color: yellow; 31 line-height: 16px; 32 } 33 </style> 34 <div id=container><div id=target></div></div> 35 <script> 36 var div = document.getElementById("target"); 37 var gRecords = []; 38 var gObserver = new MutationObserver(newRecords => { 39 gRecords.push(...newRecords); 40 }); 41 42 function setupAsynchronousObserver(t, options) { 43 44 gRecords = []; 45 t.add_cleanup(() => { 46 gObserver.disconnect(); 47 }); 48 gObserver.observe(options.subtree ? div.parentNode : div, 49 { animations: true, subtree: options.subtree }); 50 } 51 52 // Adds an event listener and returns a Promise that is resolved when the 53 // event listener is called. 54 function await_event(aElement, aEventName) { 55 return new Promise(aResolve => { 56 function listener(aEvent) { 57 aElement.removeEventListener(aEventName, listener); 58 aResolve(); 59 } 60 aElement.addEventListener(aEventName, listener); 61 }); 62 } 63 64 function assert_record_list(actual, expected, desc, index, listName) { 65 assert_equals(actual.length, expected.length, 66 `${desc} - record[${index}].${listName} length`); 67 if (actual.length != expected.length) { 68 return; 69 } 70 for (var i = 0; i < actual.length; i++) { 71 assert_not_equals(actual.indexOf(expected[i]), -1, 72 `${desc} - record[${index}].${listName} contains expected Animation`); 73 } 74 } 75 76 function assert_records(expected, desc) { 77 var records = gRecords; 78 gRecords = []; 79 assert_equals(records.length, expected.length, `${desc} - number of records`); 80 if (records.length != expected.length) { 81 return; 82 } 83 for (var i = 0; i < records.length; i++) { 84 assert_record_list(records[i].addedAnimations, expected[i].added, desc, i, "addedAnimations"); 85 assert_record_list(records[i].changedAnimations, expected[i].changed, desc, i, "changedAnimations"); 86 assert_record_list(records[i].removedAnimations, expected[i].removed, desc, i, "removedAnimations"); 87 } 88 } 89 90 function assert_records_any_order(expected, desc) { 91 // Generate a unique label for each Animation object. 92 let animation_labels = new Map(); 93 let animation_counter = 0; 94 for (let record of gRecords) { 95 for (let a of [...record.addedAnimations, ...record.changedAnimations, ...record.removedAnimations]) { 96 if (!animation_labels.has(a)) { 97 animation_labels.set(a, ++animation_counter); 98 } 99 } 100 } 101 for (let record of expected) { 102 for (let a of [...record.added, ...record.changed, ...record.removed]) { 103 if (!animation_labels.has(a)) { 104 animation_labels.set(a, ++animation_counter); 105 } 106 } 107 } 108 109 function record_label(record) { 110 // Generate a label of the form: 111 // 112 // <added-animations>:<changed-animations>:<removed-animations> 113 let added = record.addedAnimations || record.added; 114 let changed = record.changedAnimations || record.changed; 115 let removed = record.removedAnimations || record.removed; 116 return [added .map(a => animation_labels.get(a)).sort().join(), 117 changed.map(a => animation_labels.get(a)).sort().join(), 118 removed.map(a => animation_labels.get(a)).sort().join()] 119 .join(":"); 120 } 121 122 // Sort records by their label. 123 gRecords.sort((a, b) => record_label(a) < record_label(b)); 124 expected.sort((a, b) => record_label(a) < record_label(b)); 125 126 // Assert the sorted record lists are equal. 127 assert_records(expected, desc); 128 } 129 130 // -- Tests ------------------------------------------------------------------ 131 132 // We run all tests first targeting the div and observing the div, then again 133 // targeting the div and observing its parent while using the subtree:true 134 // MutationObserver option. 135 136 function runTest() { 137 [ 138 { observe: div, target: div, subtree: false }, 139 { observe: div.parentNode, target: div, subtree: true }, 140 ].forEach(aOptions => { 141 142 var e = aOptions.target; 143 144 promise_test(t => { 145 setupAsynchronousObserver(t, aOptions); 146 // Clear all styles once test finished since we re-use the same element 147 // in all test cases. 148 t.add_cleanup(() => { 149 e.style = ""; 150 flushComputedStyle(e); 151 }); 152 153 // Start a transition. 154 e.style = "transition: background-color 100s; background-color: lime;"; 155 156 // Register for the end of the transition. 157 var transitionEnd = await_event(e, "transitionend"); 158 159 // The transition should cause the creation of a single Animation. 160 var animations = e.getAnimations(); 161 assert_equals(animations.length, 1, 162 "getAnimations().length after transition start"); 163 164 // Wait for the single MutationRecord for the Animation addition to 165 // be delivered. 166 return waitForFrame().then(() => { 167 assert_records([{ added: animations, changed: [], removed: [] }], 168 "records after transition start"); 169 170 // Advance until near the end of the transition, then wait for it to 171 // finish. 172 animations[0].currentTime = 99900; 173 }).then(() => { 174 return transitionEnd; 175 }).then(() => { 176 // After the transition has finished, the Animation should disappear. 177 assert_equals(e.getAnimations().length, 0, 178 "getAnimations().length after transition end"); 179 180 // Wait for the change MutationRecord for seeking the Animation to be 181 // delivered, followed by the removal MutationRecord. 182 return waitForFrame(); 183 }).then(() => { 184 assert_records([{ added: [], changed: animations, removed: [] }, 185 { added: [], changed: [], removed: animations }], 186 "records after transition end"); 187 }); 188 }, `single_transition ${aOptions.subtree ? ': subtree' : ''}`); 189 190 // Test that starting a single animation that completes normally 191 // dispatches an added notification and then a removed notification. 192 promise_test(t => { 193 setupAsynchronousObserver(t, aOptions); 194 t.add_cleanup(() => { 195 e.style = ""; 196 flushComputedStyle(e); 197 }); 198 199 // Start an animation. 200 e.style = "animation: anim 100s;"; 201 202 // Register for the end of the animation. 203 var animationEnd = await_event(e, "animationend"); 204 205 // The animation should cause the creation of a single Animation. 206 var animations = e.getAnimations(); 207 assert_equals(animations.length, 1, 208 "getAnimations().length after animation start"); 209 210 // Wait for the single MutationRecord for the Animation addition to 211 // be delivered. 212 return waitForFrame().then(() => { 213 assert_records([{ added: animations, changed: [], removed: [] }], 214 "records after animation start"); 215 216 // Advance until near the end of the animation, then wait for it to finish. 217 animations[0].currentTime = 99900; 218 return animationEnd; 219 }).then(() => { 220 // After the animation has finished, the Animation should disappear. 221 assert_equals(e.getAnimations().length, 0, 222 "getAnimations().length after animation end"); 223 224 // Wait for the change MutationRecord from seeking the Animation to 225 // be delivered, followed by a further MutationRecord for the Animation 226 // removal. 227 return waitForFrame(); 228 }).then(() => { 229 assert_records([{ added: [], changed: animations, removed: [] }, 230 { added: [], changed: [], removed: animations }], 231 "records after animation end"); 232 }); 233 }, `single_animation ${aOptions.subtree ? ': subtree' : ''}`); 234 235 // Test that starting a single animation that is cancelled by updating 236 // the animation-fill-mode property dispatches an added notification and 237 // then a removed notification. 238 promise_test(t => { 239 setupAsynchronousObserver(t, aOptions); 240 t.add_cleanup(() => { 241 e.style = ""; 242 flushComputedStyle(e); 243 }); 244 245 // Start a short, filled animation. 246 e.style = "animation: anim 100s forwards;"; 247 248 // Register for the end of the animation. 249 var animationEnd = await_event(e, "animationend"); 250 251 // The animation should cause the creation of a single Animation. 252 var animations = e.getAnimations(); 253 assert_equals(animations.length, 1, 254 "getAnimations().length after animation start"); 255 256 // Wait for the single MutationRecord for the Animation addition to 257 // be delivered. 258 return waitForFrame().then(() => { 259 assert_records([{ added: animations, changed: [], removed: [] }], 260 "records after animation start"); 261 262 // Advance until near the end of the animation, then wait for it to finish. 263 animations[0].currentTime = 99900; 264 return animationEnd; 265 }).then(() => { 266 // The only MutationRecord at this point should be the change from 267 // seeking the Animation. 268 assert_records([{ added: [], changed: animations, removed: [] }], 269 "records after animation starts filling"); 270 271 // Cancel the animation by setting animation-fill-mode. 272 e.style.animationFillMode = "none"; 273 // Explicitly flush style to make sure the above style change happens. 274 // Normally we don't need explicit style flush if there is a waitForFrame() 275 // call but in this particular case we are in the middle of animation events' 276 // callback handling and requestAnimationFrame handling so that we have no 277 // chance to process styling even after the requestAnimationFrame handling. 278 flushComputedStyle(e); 279 280 // Wait for the single MutationRecord for the Animation removal to 281 // be delivered. 282 return waitForFrame(); 283 }).then(() => { 284 assert_records([{ added: [], changed: [], removed: animations }], 285 "records after animation end"); 286 }); 287 }, `single_animation_cancelled_fill ${aOptions.subtree ? ': subtree' : ''}`); 288 289 // Test that calling finish() on a paused (but otherwise finished) animation 290 // dispatches a changed notification. 291 promise_test(t => { 292 setupAsynchronousObserver(t, aOptions); 293 t.add_cleanup(() => { 294 e.style = ""; 295 flushComputedStyle(e); 296 }); 297 298 // Start a long animation 299 e.style = "animation: anim 100s forwards"; 300 301 // The animation should cause the creation of a single Animation. 302 var animations = e.getAnimations(); 303 assert_equals(animations.length, 1, 304 "getAnimations().length after animation start"); 305 306 // Wait for the single MutationRecord for the Animation addition to 307 // be delivered. 308 return waitForFrame().then(() => { 309 assert_records([{ added: animations, changed: [], removed: [] }], 310 "records after animation start"); 311 312 // Wait until the animation is playing. 313 return animations[0].ready; 314 }).then(() => { 315 // Finish and pause. 316 animations[0].finish(); 317 animations[0].pause(); 318 319 // Wait for the pause to complete. 320 return animations[0].ready; 321 }).then(() => { 322 assert_true( 323 !animations[0].pending && animations[0].playState === "paused", 324 "playState after finishing and pausing"); 325 326 // We should have two MutationRecords for the Animation changes: 327 // one for the finish, one for the pause. 328 assert_records([{ added: [], changed: animations, removed: [] }, 329 { added: [], changed: animations, removed: [] }], 330 "records after finish() and pause()"); 331 332 // Call finish() again. 333 animations[0].finish(); 334 assert_equals(animations[0].playState, "finished", 335 "playState after finishing from paused state"); 336 337 // Wait for the single MutationRecord for the Animation change to 338 // be delivered. Even though the currentTime does not change, the 339 // playState will change. 340 return waitForFrame(); 341 }).then(() => { 342 assert_records([{ added: [], changed: animations, removed: [] }], 343 "records after finish() and pause()"); 344 345 // Cancel the animation. 346 e.style = ""; 347 348 // Wait for the single removal notification. 349 return waitForFrame(); 350 }).then(() => { 351 assert_records([{ added: [], changed: [], removed: animations }], 352 "records after animation end"); 353 }); 354 }, `finish_from_pause ${aOptions.subtree ? ': subtree' : ''}`); 355 356 // Test that calling play() on a paused Animation dispatches a changed 357 // notification. 358 promise_test(t => { 359 setupAsynchronousObserver(t, aOptions); 360 t.add_cleanup(() => { 361 e.style = ""; 362 flushComputedStyle(e); 363 }); 364 365 // Start a long, paused animation 366 e.style = "animation: anim 100s paused"; 367 368 // The animation should cause the creation of a single Animation. 369 var animations = e.getAnimations(); 370 assert_equals(animations.length, 1, 371 "getAnimations().length after animation start"); 372 373 // Wait for the single MutationRecord for the Animation addition to 374 // be delivered. 375 return waitForFrame().then(() => { 376 assert_records([{ added: animations, changed: [], removed: [] }], 377 "records after animation start"); 378 379 // Wait until the animation is ready 380 return animations[0].ready; 381 }).then(() => { 382 // Play 383 animations[0].play(); 384 385 // Wait for the single MutationRecord for the Animation change to 386 // be delivered. 387 return animations[0].ready; 388 }).then(() => { 389 assert_records([{ added: [], changed: animations, removed: [] }], 390 "records after play()"); 391 392 // Redundant play 393 animations[0].play(); 394 395 // Wait to ensure no change is dispatched 396 return waitForFrame(); 397 }).then(() => { 398 assert_records([], "records after redundant play()"); 399 400 // Cancel the animation. 401 e.style = ""; 402 403 // Wait for the single removal notification. 404 return waitForFrame(); 405 }).then(() => { 406 assert_records([{ added: [], changed: [], removed: animations }], 407 "records after animation end"); 408 }); 409 }, `play ${aOptions.subtree ? ': subtree' : ''}`); 410 411 // Test that a non-cancelling change to an animation followed immediately by a 412 // cancelling change will only send an animation removal notification. 413 promise_test(t => { 414 setupAsynchronousObserver(t, aOptions); 415 t.add_cleanup(() => { 416 e.style = ""; 417 flushComputedStyle(e); 418 }); 419 420 // Start a long animation. 421 e.style = "animation: anim 100s;"; 422 423 // The animation should cause the creation of a single Animation. 424 var animations = e.getAnimations(); 425 assert_equals(animations.length, 1, 426 "getAnimations().length after animation start"); 427 428 // Wait for the single MutationRecord for the Animation addition to 429 // be delivered. 430 return waitForFrame().then(() => {; 431 assert_records([{ added: animations, changed: [], removed: [] }], 432 "records after animation start"); 433 434 // Update the animation's delay such that it is still running. 435 e.style.animationDelay = "-1s"; 436 437 // Then cancel the animation by updating its duration. 438 e.style.animationDuration = "0.5s"; 439 440 // We should get a single removal notification. 441 return waitForFrame(); 442 }).then(() => { 443 assert_records([{ added: [], changed: [], removed: animations }], 444 "records after animation end"); 445 }); 446 }, `coalesce_change_cancel ${aOptions.subtree ? ': subtree' : ''}`); 447 448 }); 449 } 450 451 promise_test(async t => { 452 setupAsynchronousObserver(t, { observe: div, subtree: true }); 453 t.add_cleanup(() => { 454 div.style = ""; 455 flushComputedStyle(div); 456 }); 457 458 // Add style for pseudo elements 459 var extraStyle = document.createElement('style'); 460 document.head.appendChild(extraStyle); 461 var sheet = extraStyle.sheet; 462 var rules = { ".before::before": "animation: anim 100s; content: '';", 463 ".after::after" : "animation: anim 100s, anim 100s; " + 464 "content: '';"}; 465 for (var selector in rules) { 466 sheet.insertRule(selector + '{' + rules[selector] + '}', 467 sheet.cssRules.length); 468 } 469 470 // Create a tree with two children: 471 // 472 // div 473 // (::before) 474 // (::after) 475 // / \ 476 // childA childB(::before) 477 var childA = document.createElement("div"); 478 var childB = document.createElement("div"); 479 480 div.appendChild(childA); 481 div.appendChild(childB); 482 483 // Start an animation on each (using order: childB, div, childA) 484 // 485 // We include multiple animations on some nodes so that we can test batching 486 // works as expected later in this test. 487 childB.style = "animation: anim 100s"; 488 div.style = "animation: anim 100s, anim 100s, anim 100s"; 489 childA.style = "animation: anim 100s, anim 100s"; 490 491 // Start animations targeting to pseudo element of div and childB. 492 childB.classList.add("before"); 493 div.classList.add("after"); 494 div.classList.add("before"); 495 496 // Check all animations we have in this document 497 var docAnims = document.getAnimations(); 498 assert_equals(docAnims.length, 10, "total animations"); 499 500 var divAnimations = div.getAnimations(); 501 var childAAnimations = childA.getAnimations(); 502 var childBAnimations = childB.getAnimations(); 503 504 var divBeforeAnimations = 505 docAnims.filter(x => (x.effect.target == div && 506 x.effect.pseudoElement == "::before")); 507 var divAfterAnimations = 508 docAnims.filter(x => (x.effect.target == div && 509 x.effect.pseudoElement == "::after")); 510 var childBPseudoAnimations = 511 docAnims.filter(x => (x.effect.target == childB && 512 x.effect.pseudoElement == "::before")); 513 514 var seekRecords; 515 // The order in which we get the corresponding records is currently 516 // based on the order we visit these nodes when updating styles. 517 // 518 // That is because we don't do any document-level batching of animation 519 // mutation records when we flush styles. We may introduce that in the 520 // future but for now all we are interested in testing here is that the 521 // right records are generated, but we allow them to occur in any order. 522 await waitForFrame(); 523 524 assert_records_any_order( 525 [{ added: divAfterAnimations, changed: [], removed: [] }, 526 { added: childAAnimations, changed: [], removed: [] }, 527 { added: childBAnimations, changed: [], removed: [] }, 528 { added: childBPseudoAnimations, changed: [], removed: [] }, 529 { added: divAnimations, changed: [], removed: [] }, 530 { added: divBeforeAnimations, changed: [], removed: [] }], 531 "records after simultaneous animation start"); 532 533 // The one case where we *do* currently perform document-level (or actually 534 // timeline-level) batching is when animations are updated from a refresh 535 // driver tick. In particular, this means that when animations finish 536 // naturally the removed records should be dispatched according to the 537 // position of the elements in the tree. 538 539 // First, flatten the set of animations. we put the animations targeting to 540 // pseudo elements last. (Actually, we don't care the order in the list.) 541 var animations = [ ...divAnimations, 542 ...childAAnimations, 543 ...childBAnimations, 544 ...divBeforeAnimations, 545 ...divAfterAnimations, 546 ...childBPseudoAnimations ]; 547 548 await Promise.all(animations.map(animation => animation.ready)); 549 550 // Fast-forward to *just* before the end of the animation. 551 animations.forEach(animation => animation.currentTime = 99999); 552 553 // Prepare the set of expected change MutationRecords, one for each 554 // animation that was seeked. 555 seekRecords = animations.map( 556 p => ({ added: [], changed: [p], removed: [] }) 557 ); 558 559 await Promise.all(animations.map(animation => animation.finished)); 560 561 // After the changed notifications, which will be dispatched in the order that 562 // the animations were seeked, we should get removal MutationRecords in order 563 // (div, div::before, div::after), childA, (childB, childB::before). 564 // Note: The animations targeting to the pseudo element are appended after 565 // the animations of its parent element. 566 divAnimations = [ ...divAnimations, 567 ...divBeforeAnimations, 568 ...divAfterAnimations ]; 569 childBAnimations = [ ...childBAnimations, ...childBPseudoAnimations ]; 570 assert_records(seekRecords.concat( 571 { added: [], changed: [], removed: divAnimations }, 572 { added: [], changed: [], removed: childAAnimations }, 573 { added: [], changed: [], removed: childBAnimations }), 574 "records after finishing"); 575 576 // Clean up 577 div.classList.remove("before"); 578 div.classList.remove("after"); 579 div.style = ""; 580 childA.remove(); 581 childB.remove(); 582 extraStyle.remove(); 583 }, "tree_ordering: subtree"); 584 585 // Test that animations removed by auto-removal trigger an event 586 promise_test(async t => { 587 setupAsynchronousObserver(t, { observe: div, subtree: false }); 588 589 // Start two animations such that one will be auto-removed 590 const animA = div.animate( 591 { opacity: 1 }, 592 { duration: 100 * MS_PER_SEC, fill: 'forwards' } 593 ); 594 const animB = div.animate( 595 { opacity: 1 }, 596 { duration: 100 * MS_PER_SEC, fill: 'forwards' } 597 ); 598 599 // Wait for the MutationRecords corresponding to each addition. 600 await waitForNextFrame(); 601 602 assert_records( 603 [ 604 { added: [animA], changed: [], removed: [] }, 605 { added: [animB], changed: [], removed: [] }, 606 ], 607 'records after animation start' 608 ); 609 610 // Finish the animations -- this should cause animA to be replaced, and 611 // automatically removed. 612 animA.finish(); 613 animB.finish(); 614 615 // Wait for the MutationRecords corresponding to the timing changes and the 616 // subsequent removal to be delivered. 617 await waitForNextFrame(); 618 619 assert_records( 620 [ 621 { added: [], changed: [animA], removed: [] }, 622 { added: [], changed: [animB], removed: [] }, 623 { added: [], changed: [], removed: [animA] }, 624 ], 625 'records after finishing' 626 ); 627 628 // Restore animA. 629 animA.persist(); 630 631 // Wait for the MutationRecord corresponding to the re-addition of animA. 632 await waitForNextFrame(); 633 634 assert_records( 635 [{ added: [animA], changed: [], removed: [] }], 636 'records after persisting' 637 ); 638 639 // Tidy up 640 animA.cancel(); 641 animB.cancel(); 642 643 await waitForNextFrame(); 644 645 assert_records( 646 [ 647 { added: [], changed: [], removed: [animA] }, 648 { added: [], changed: [], removed: [animB] }, 649 ], 650 'records after tidying up end' 651 ); 652 }, 'Animations automatically removed are reported'); 653 runTest(); 654 </script>