file_restyles.html (85451B)
1 <!doctype html> 2 <head> 3 <meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1"> 4 <meta charset=utf-8> 5 <title>Tests restyles caused by animations</title> 6 <script> 7 const ok = opener.ok.bind(opener); 8 const is = opener.is.bind(opener); 9 const todo = opener.todo.bind(opener); 10 const todo_is = opener.todo_is.bind(opener); 11 const info = opener.info.bind(opener); 12 const original_finish = opener.SimpleTest.finish; 13 const SimpleTest = opener.SimpleTest; 14 const add_task = opener.add_task; 15 SimpleTest.finish = function finish() { 16 self.close(); 17 original_finish(); 18 } 19 </script> 20 <script src="/tests/SimpleTest/EventUtils.js"></script> 21 <script src="/tests/SimpleTest/paint_listener.js"></script> 22 <script src="../testcommon.js"></script> 23 <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> 24 <style> 25 @keyframes background-position { 26 0% { 27 background-position: -25px center; 28 } 29 30 40%, 31 100% { 32 background-position: 36px center; 33 } 34 } 35 @keyframes opacity { 36 from { opacity: 1; } 37 to { opacity: 0; } 38 } 39 @keyframes opacity-from-zero { 40 from { opacity: 0; } 41 to { opacity: 1; } 42 } 43 @keyframes opacity-without-end-value { 44 from { opacity: 0; } 45 } 46 @keyframes on-main-thread { 47 from { z-index: 0; } 48 to { z-index: 999; } 49 } 50 @keyframes rotate { 51 from { transform: rotate(0deg); } 52 to { transform: rotate(360deg); } 53 } 54 @keyframes move-in { 55 from { transform: translate(120%, 120%); } 56 to { transform: translate(0%, 0%); } 57 } 58 @keyframes background-color { 59 from { background-color: rgb(255, 0, 0,); } 60 to { background-color: rgb(0, 255, 0,); } 61 } 62 div { 63 /* Element needs geometry to be eligible for layerization */ 64 width: 100px; 65 height: 100px; 66 background-color: white; 67 } 68 progress:not(.stop)::-moz-progress-bar { 69 animation: on-main-thread 100s; 70 } 71 body { 72 /* 73 * set overflow:hidden to avoid accidentally unthrottling animations to update 74 * the overflow region. 75 */ 76 overflow: hidden; 77 } 78 </style> 79 </head> 80 <body> 81 <script> 82 'use strict'; 83 84 // Returns observed animation restyle markers when |funcToMakeRestyleHappen| 85 // is called. 86 // NOTE: This function is synchronous version of the above observeStyling(). 87 // Unlike the above observeStyling, this function takes a callback function, 88 // |funcToMakeRestyleHappen|, which may be expected to trigger a synchronous 89 // restyles, and returns any restyle markers produced by calling that function. 90 function observeAnimSyncStyling(funcToMakeRestyleHappen) { 91 92 let priorAnimationTriggeredRestyles = SpecialPowers.DOMWindowUtils.animationTriggeredRestyles; 93 94 funcToMakeRestyleHappen(); 95 96 const restyleCount = SpecialPowers.DOMWindowUtils.animationTriggeredRestyles - priorAnimationTriggeredRestyles; 97 98 return restyleCount; 99 } 100 101 function ensureElementRemoval(aElement) { 102 return new Promise(resolve => { 103 aElement.remove(); 104 waitForAllPaintsFlushed(resolve); 105 }); 106 } 107 108 function waitForWheelEvent(aTarget) { 109 return new Promise(resolve => { 110 // Get the scrollable target element position in this window coordinate 111 // system to send a wheel event to the element. 112 const targetRect = aTarget.getBoundingClientRect(); 113 const centerX = targetRect.left + targetRect.width / 2; 114 const centerY = targetRect.top + targetRect.height / 2; 115 116 sendWheelAndPaintNoFlush(aTarget, centerX, centerY, 117 { deltaMode: WheelEvent.DOM_DELTA_PIXEL, 118 deltaY: targetRect.height }, 119 resolve); 120 }); 121 } 122 123 const omtaEnabled = isOMTAEnabled(); 124 125 function add_task_if_omta_enabled(test) { 126 if (!omtaEnabled) { 127 info(test.name + " is skipped because OMTA is disabled"); 128 return; 129 } 130 add_task(test); 131 } 132 133 async function estimateVsyncRate() { 134 await waitForNextFrame(); 135 136 const timeAtStart = document.timeline.currentTime; 137 await waitForAnimationFrames(5); 138 return (document.timeline.currentTime - timeAtStart) / 5; 139 } 140 141 // We need to wait for all paints before running tests to avoid contaminations 142 // from styling of this document itself. 143 waitForAllPaints(async () => { 144 const vsyncRate = await estimateVsyncRate(); 145 // In this test we basically observe restyling counts in 5 frames, if it 146 // takes over 200ms during the 5 frames, this test will fail. So 147 // "200ms / 5 = 40ms" is a threshold whether the test works as expected or 148 // not. We'd take 5ms additional tolerance here. 149 // Note that the 200ms is a period we unthrottle throttled animations that 150 // at least one of the animating styles produces change hints causing 151 // overflow, the value is defined in 152 // KeyframeEffect::OverflowRegionRefreshInterval. 153 if (vsyncRate > 40 - 5) { 154 ok(true, `the vsync rate ${vsyncRate} on this machine is too slow to run this test`); 155 SimpleTest.finish(); 156 return; 157 } 158 159 add_task(async function restyling_for_main_thread_animations() { 160 const div = addDiv(null, { style: 'animation: on-main-thread 100s' }); 161 const animation = div.getAnimations()[0]; 162 163 await waitForAnimationReadyToRestyle(animation); 164 165 ok(!SpecialPowers.wrap(animation).isRunningOnCompositor); 166 167 const restyleCount = await observeStyling(5); 168 is(restyleCount, 5, 169 'CSS animations running on the main-thread should update style ' + 170 'on the main thread'); 171 await ensureElementRemoval(div); 172 }); 173 174 add_task(async function restyling_for_main_thread_animations_progress_bar_pseudo() { 175 const progress = document.createElement("progress"); 176 document.body.appendChild(progress); 177 178 await waitForNextFrame(); 179 await waitForNextFrame(); 180 181 let restyleCount; 182 restyleCount = await observeStyling(5); 183 // TODO(bug 1784931): Figure out why we only see four markers sometimes. 184 // That's not the point of this test tho. 185 let maybe_todo_is = restyleCount == 4 ? todo_is : is; 186 maybe_todo_is(restyleCount, 5, 187 'CSS animations running on the main-thread should update style ' + 188 'on the main thread on ::-moz-progress-bar'); 189 progress.classList.add("stop"); 190 await waitForNextFrame(); 191 await waitForNextFrame(); 192 193 restyleCount = await observeStyling(5); 194 is(restyleCount, 0, 'Animation is correctly removed'); 195 await ensureElementRemoval(progress); 196 }); 197 198 add_task_if_omta_enabled(async function no_restyling_for_compositor_animations() { 199 const div = addDiv(null, { style: 'animation: opacity 100s' }); 200 const animation = div.getAnimations()[0]; 201 202 await waitForAnimationReadyToRestyle(animation); 203 ok(SpecialPowers.wrap(animation).isRunningOnCompositor); 204 205 const restyleCount = await observeStyling(5); 206 is(restyleCount, 0, 207 'CSS animations running on the compositor should not update style ' + 208 'on the main thread'); 209 await ensureElementRemoval(div); 210 }); 211 212 add_task_if_omta_enabled(async function no_restyling_for_compositor_transitions() { 213 const div = addDiv(null, { style: 'transition: opacity 100s; opacity: 0' }); 214 getComputedStyle(div).opacity; 215 div.style.opacity = 1; 216 217 const animation = div.getAnimations()[0]; 218 219 await waitForAnimationReadyToRestyle(animation); 220 ok(SpecialPowers.wrap(animation).isRunningOnCompositor); 221 222 const restyleCount = await observeStyling(5); 223 is(restyleCount, 0, 224 'CSS transitions running on the compositor should not update style ' + 225 'on the main thread'); 226 await ensureElementRemoval(div); 227 }); 228 229 add_task_if_omta_enabled(async function no_restyling_when_animation_duration_is_changed() { 230 const div = addDiv(null, { style: 'animation: opacity 100s' }); 231 const animation = div.getAnimations()[0]; 232 233 await waitForAnimationReadyToRestyle(animation); 234 ok(SpecialPowers.wrap(animation).isRunningOnCompositor); 235 236 div.animationDuration = '200s'; 237 238 const restyleCount = await observeStyling(5); 239 is(restyleCount, 0, 240 'Animations running on the compositor should not update style ' + 241 'on the main thread'); 242 await ensureElementRemoval(div); 243 }); 244 245 add_task_if_omta_enabled(async function only_one_restyling_after_finish_is_called() { 246 const div = addDiv(null, { style: 'animation: opacity 100s' }); 247 const animation = div.getAnimations()[0]; 248 249 await waitForAnimationReadyToRestyle(animation); 250 ok(SpecialPowers.wrap(animation).isRunningOnCompositor); 251 252 animation.finish(); 253 254 let restyleCount; 255 restyleCount = await observeStyling(1); 256 is(restyleCount, 1, 257 'Animations running on the compositor should only update style once ' + 258 'after finish() is called'); 259 260 restyleCount = await observeStyling(1); 261 todo_is(restyleCount, 0, 262 'Bug 1415457: Animations running on the compositor should only ' + 263 'update style once after finish() is called'); 264 265 restyleCount = await observeStyling(5); 266 is(restyleCount, 0, 267 'Finished animations should never update style after one ' + 268 'restyle happened for finish()'); 269 270 await ensureElementRemoval(div); 271 }); 272 273 add_task(async function no_restyling_mouse_movement_on_finished_transition() { 274 const div = addDiv(null, { style: 'transition: opacity 1ms; opacity: 0' }); 275 getComputedStyle(div).opacity; 276 div.style.opacity = 1; 277 278 const animation = div.getAnimations()[0]; 279 const initialRect = div.getBoundingClientRect(); 280 281 await animation.finished; 282 let restyleCount; 283 restyleCount = await observeStyling(1); 284 is(restyleCount, 1, 285 'Finished transitions should restyle once after Animation.finished ' + 286 'was fulfilled'); 287 288 let mouseX = initialRect.left + initialRect.width / 2; 289 let mouseY = initialRect.top + initialRect.height / 2; 290 restyleCount = await observeStyling(5, () => { 291 // We can't use synthesizeMouse here since synthesizeMouse causes 292 // layout flush. 293 synthesizeMouseAtPoint(mouseX++, mouseY++, 294 { type: 'mousemove' }, window); 295 }); 296 297 is(restyleCount, 0, 298 'Finished transitions should never cause restyles when mouse is moved ' + 299 'on the transitions'); 300 await ensureElementRemoval(div); 301 }); 302 303 add_task(async function no_restyling_mouse_movement_on_finished_animation() { 304 const div = addDiv(null, { style: 'animation: opacity 1ms' }); 305 const animation = div.getAnimations()[0]; 306 307 const initialRect = div.getBoundingClientRect(); 308 309 await animation.finished; 310 let restyleCount; 311 restyleCount = await observeStyling(1); 312 is(restyleCount, 1, 313 'Finished animations should restyle once after Animation.finished ' + 314 'was fulfilled'); 315 316 let mouseX = initialRect.left + initialRect.width / 2; 317 let mouseY = initialRect.top + initialRect.height / 2; 318 restyleCount = await observeStyling(5, () => { 319 // We can't use synthesizeMouse here since synthesizeMouse causes 320 // layout flush. 321 synthesizeMouseAtPoint(mouseX++, mouseY++, 322 { type: 'mousemove' }, window); 323 }); 324 325 is(restyleCount, 0, 326 'Finished animations should never cause restyles when mouse is moved ' + 327 'on the animations'); 328 await ensureElementRemoval(div); 329 }); 330 331 add_task_if_omta_enabled(async function no_restyling_compositor_animations_out_of_view_element() { 332 const div = addDiv(null, 333 { style: 'animation: opacity 100s; transform: translateY(-400px);' }); 334 const animation = div.getAnimations()[0]; 335 336 await waitForAnimationReadyToRestyle(animation); 337 ok(!SpecialPowers.wrap(animation).isRunningOnCompositor); 338 339 const restyleCount = await observeStyling(5); 340 341 is(restyleCount, 0, 342 'Animations running on the compositor in an out-of-view element ' + 343 'should never cause restyles'); 344 await ensureElementRemoval(div); 345 }); 346 347 add_task(async function no_restyling_main_thread_animations_out_of_view_element() { 348 const div = addDiv(null, 349 { style: 'animation: on-main-thread 100s; transform: translateY(-400px);' }); 350 const animation = div.getAnimations()[0]; 351 352 await waitForAnimationReadyToRestyle(animation); 353 const restyleCount = await observeStyling(5); 354 355 is(restyleCount, 0, 356 'Animations running on the main-thread in an out-of-view element ' + 357 'should never cause restyles'); 358 await ensureElementRemoval(div); 359 }); 360 361 add_task_if_omta_enabled(async function no_restyling_compositor_animations_in_scrolled_out_element() { 362 const parentElement = addDiv(null, 363 { style: 'overflow-y: scroll; height: 20px;' }); 364 const div = addDiv(null, 365 { style: 'animation: opacity 100s; position: relative; top: 100px;' }); 366 parentElement.appendChild(div); 367 const animation = div.getAnimations()[0]; 368 369 await waitForAnimationReadyToRestyle(animation); 370 371 const restyleCount = await observeStyling(5); 372 373 is(restyleCount, 0, 374 'Animations running on the compositor for elements ' + 375 'which are scrolled out should never cause restyles'); 376 377 await ensureElementRemoval(parentElement); 378 }); 379 380 add_task( 381 async function no_restyling_missing_keyframe_opacity_animations_on_scrolled_out_element() { 382 const parentElement = addDiv(null, 383 { style: 'overflow-y: scroll; height: 20px;' }); 384 const div = addDiv(null, 385 { style: 'animation: opacity-without-end-value 100s; ' + 386 'position: relative; top: 100px;' }); 387 parentElement.appendChild(div); 388 const animation = div.getAnimations()[0]; 389 await waitForAnimationReadyToRestyle(animation); 390 391 const restyleCount = await observeStyling(5); 392 393 is(restyleCount, 0, 394 'Opacity animations on scrolled out elements should never cause ' + 395 'restyles even if the animation has missing keyframes'); 396 397 await ensureElementRemoval(parentElement); 398 } 399 ); 400 401 add_task( 402 async function restyling_transform_animations_in_scrolled_out_element() { 403 // Make sure we start from the state right after requestAnimationFrame. 404 await waitForFrame(); 405 406 const parentElement = addDiv(null, 407 { style: 'overflow-y: scroll; height: 20px;' }); 408 const div = addDiv(null, 409 { style: 'animation: rotate 100s infinite; position: relative; top: 100px;' }); 410 parentElement.appendChild(div); 411 const animation = div.getAnimations()[0]; 412 let timeAtStart = document.timeline.currentTime; 413 414 ok(!animation.isRunningOnCompositor, 415 'The transform animation is not running on the compositor'); 416 417 let restyleCount 418 let now; 419 let elapsed; 420 while (true) { 421 now = document.timeline.currentTime; 422 elapsed = (now - timeAtStart); 423 restyleCount = await observeStyling(1); 424 if (restyleCount) { 425 break; 426 } 427 } 428 // If the current time has elapsed over 200ms since the animation was 429 // created, it means that the animation should have already 430 // unthrottled in this tick, let's see what we observe in this tick's 431 // restyling process. 432 // We use toPrecision here and below so 199.99999999999977 will turn into 200. 433 ok(elapsed.toPrecision(10) >= 200, 434 'Transform animation running on the element which is scrolled out ' + 435 'should be throttled until 200ms is elapsed. now: ' + 436 now + ' start time: ' + timeAtStart + ' elapsed:' + elapsed); 437 438 timeAtStart = document.timeline.currentTime; 439 restyleCount = await observeStyling(1); 440 now = document.timeline.currentTime; 441 elapsed = (now - timeAtStart); 442 443 let expectedMarkersLengthValid; 444 // On the fence of 200 ms, we probably have 1 marker; but if we hit a bad rounding 445 // we might still have 0. But if it's > 200, we should have 1; and less we should have 0. 446 if (elapsed.toPrecision(10) == 200) 447 expectedMarkersLengthValid = restyleCount < 2; 448 else if (elapsed.toPrecision(10) > 200) 449 expectedMarkersLengthValid = restyleCount == 1; 450 else 451 expectedMarkersLengthValid = !restyleCount; 452 ok(expectedMarkersLengthValid, 453 'Transform animation running on the element which is scrolled out ' + 454 'should be unthrottled after around 200ms have elapsed. now: ' + 455 now + ' start time: ' + timeAtStart + ' elapsed: ' + elapsed); 456 457 await ensureElementRemoval(parentElement); 458 } 459 ); 460 461 add_task( 462 async function restyling_out_of_view_transform_animations_in_another_element() { 463 // Make sure we start from the state right after requestAnimationFrame. 464 await waitForFrame(); 465 466 const parentElement = addDiv(null, 467 { style: 'overflow: hidden;' }); 468 const div = addDiv(null, 469 { style: 'animation: move-in 100s infinite;' }); 470 parentElement.appendChild(div); 471 const animation = div.getAnimations()[0]; 472 let timeAtStart = document.timeline.currentTime; 473 474 ok(!animation.isRunningOnCompositor, 475 'The transform animation on out of view element ' + 476 'is not running on the compositor'); 477 478 // Structure copied from restyling_transform_animations_in_scrolled_out_element 479 let restyleCount 480 let now; 481 let elapsed; 482 while (true) { 483 now = document.timeline.currentTime; 484 elapsed = (now - timeAtStart); 485 restyleCount = await observeStyling(1); 486 if (restyleCount) { 487 break; 488 } 489 } 490 491 ok(elapsed.toPrecision(10) >= 200, 492 'Transform animation running on out of view element ' + 493 'should be throttled until 200ms is elapsed. now: ' + 494 now + ' start time: ' + timeAtStart + ' elapsed:' + elapsed); 495 496 timeAtStart = document.timeline.currentTime; 497 restyleCount = await observeStyling(1); 498 now = document.timeline.currentTime; 499 elapsed = (now - timeAtStart); 500 501 let expectedMarkersLengthValid; 502 // On the fence of 200 ms, we probably have 1 marker; but if we hit a bad rounding 503 // we might still have 0. But if it's > 200, we should have 1; and less we should have 0. 504 if (elapsed.toPrecision(10) == 200) 505 expectedMarkersLengthValid = restyleCount < 2; 506 else if (elapsed.toPrecision(10) > 200) 507 expectedMarkersLengthValid = restyleCount == 1; 508 else 509 expectedMarkersLengthValid = !restyleCount; 510 ok(expectedMarkersLengthValid, 511 'Transform animation running on out of view element ' + 512 'should be unthrottled after around 200ms have elapsed. now: ' + 513 now + ' start time: ' + timeAtStart + ' elapsed: ' + elapsed); 514 515 await ensureElementRemoval(parentElement); 516 } 517 ); 518 519 add_task(async function finite_transform_animations_in_out_of_view_element() { 520 const parentElement = addDiv(null, { style: 'overflow: hidden;' }); 521 const div = addDiv(null); 522 const animation = 523 div.animate({ transform: [ 'translateX(120%)', 'translateX(100%)' ] }, 524 // This animation will move a bit but 525 // will remain out-of-view. 526 100 * MS_PER_SEC); 527 parentElement.appendChild(div); 528 529 await waitForAnimationReadyToRestyle(animation); 530 ok(!SpecialPowers.wrap(animation).isRunningOnCompositor, "Should not be running in compositor"); 531 532 const restyleCount = await observeStyling(20); 533 is(restyleCount, 20, 534 'Finite transform animation in out-of-view element should never be ' + 535 'throttled'); 536 537 await ensureElementRemoval(parentElement); 538 }); 539 540 add_task(async function restyling_main_thread_animations_in_scrolled_out_element() { 541 const parentElement = addDiv(null, 542 { style: 'overflow-y: scroll; height: 20px;' }); 543 const div = addDiv(null, 544 { style: 'animation: on-main-thread 100s; position: relative; top: 20px;' }); 545 parentElement.appendChild(div); 546 const animation = div.getAnimations()[0]; 547 548 await waitForAnimationReadyToRestyle(animation); 549 let restyleCount; 550 restyleCount = await observeStyling(5); 551 552 is(restyleCount, 0, 553 'Animations running on the main-thread for elements ' + 554 'which are scrolled out should never cause restyles'); 555 556 await waitForWheelEvent(parentElement); 557 558 // Make sure we are ready to restyle before counting restyles. 559 await waitForFrame(); 560 561 restyleCount = await observeStyling(5); 562 is(restyleCount, 5, 563 'Animations running on the main-thread which were in scrolled out ' + 564 'elements should update restyling soon after the element moved in ' + 565 'view by scrolling'); 566 567 await ensureElementRemoval(parentElement); 568 }); 569 570 add_task(async function restyling_main_thread_animations_in_nested_scrolled_out_element() { 571 const grandParent = addDiv(null, 572 { style: 'overflow-y: scroll; height: 20px;' }); 573 const parentElement = addDiv(null, 574 { style: 'overflow-y: scroll; height: 100px;' }); 575 const div = addDiv(null, 576 { style: 'animation: on-main-thread 100s; ' + 577 'position: relative; ' + 578 'top: 20px;' }); // This element is in-view in the parent, but 579 // out of view in the grandparent. 580 grandParent.appendChild(parentElement); 581 parentElement.appendChild(div); 582 const animation = div.getAnimations()[0]; 583 584 await waitForAnimationReadyToRestyle(animation); 585 let restyleCount; 586 restyleCount = await observeStyling(5); 587 588 is(restyleCount, 0, 589 'Animations running on the main-thread which are in nested elements ' + 590 'which are scrolled out should never cause restyles'); 591 592 await waitForWheelEvent(grandParent); 593 594 await waitForFrame(); 595 596 restyleCount = await observeStyling(5); 597 is(restyleCount, 5, 598 'Animations running on the main-thread which were in nested scrolled ' + 599 'out elements should update restyle soon after the element moved ' + 600 'in view by scrolling'); 601 602 await ensureElementRemoval(grandParent); 603 }); 604 605 add_task_if_omta_enabled(async function no_restyling_compositor_animations_in_visibility_hidden_element() { 606 const div = addDiv(null, 607 { style: 'animation: opacity 100s; visibility: hidden' }); 608 const animation = div.getAnimations()[0]; 609 610 await waitForAnimationReadyToRestyle(animation); 611 ok(!SpecialPowers.wrap(animation).isRunningOnCompositor); 612 613 const restyleCount = await observeStyling(5); 614 615 is(restyleCount, 0, 616 'Animations running on the compositor in visibility hidden element ' + 617 'should never cause restyles'); 618 await ensureElementRemoval(div); 619 }); 620 621 add_task(async function restyling_main_thread_animations_move_out_of_view_by_scrolling() { 622 const parentElement = addDiv(null, 623 { style: 'overflow-y: scroll; height: 200px;' }); 624 const div = addDiv(null, 625 { style: 'animation: on-main-thread 100s;' }); 626 const pad = addDiv(null, 627 { style: 'height: 400px;' }); 628 parentElement.appendChild(div); 629 parentElement.appendChild(pad); 630 const animation = div.getAnimations()[0]; 631 632 await waitForAnimationReadyToRestyle(animation); 633 634 await waitForWheelEvent(parentElement); 635 636 await waitForFrame(); 637 638 const restyleCount = await observeStyling(5); 639 640 // FIXME: We should reduce a redundant restyle here. 641 ok(restyleCount >= 0, 642 'Animations running on the main-thread which are in scrolled out ' + 643 'elements should throttle restyling'); 644 645 await ensureElementRemoval(parentElement); 646 }); 647 648 add_task(async function restyling_main_thread_animations_moved_in_view_by_resizing() { 649 const parentElement = addDiv(null, 650 { style: 'overflow-y: scroll; height: 20px;' }); 651 const div = addDiv(null, 652 { style: 'animation: on-main-thread 100s; position: relative; top: 100px;' }); 653 parentElement.appendChild(div); 654 const animation = div.getAnimations()[0]; 655 656 await waitForAnimationReadyToRestyle(animation); 657 658 let restyleCount; 659 restyleCount = await observeStyling(5); 660 is(restyleCount, 0, 661 'Animations running on the main-thread which is in scrolled out ' + 662 'elements should not update restyling'); 663 664 parentElement.style.height = '100px'; 665 restyleCount = await observeStyling(1); 666 667 is(restyleCount, 1, 668 'Animations running on the main-thread which was in scrolled out ' + 669 'elements should update restyling soon after the element moved in ' + 670 'view by resizing'); 671 672 await ensureElementRemoval(parentElement); 673 }); 674 675 add_task( 676 async function restyling_animations_on_visibility_changed_element_having_child() { 677 const div = addDiv(null, 678 { style: 'animation: on-main-thread 100s;' }); 679 const childElement = addDiv(null); 680 div.appendChild(childElement); 681 682 const animation = div.getAnimations()[0]; 683 684 await waitForAnimationReadyToRestyle(animation); 685 686 // We don't check the animation causes restyles here since we already 687 // check it in the first test case. 688 689 div.style.visibility = 'hidden'; 690 await waitForNextFrame(); 691 692 const restyleCount = await observeStyling(5); 693 todo_is(restyleCount, 0, 694 'Animations running on visibility hidden element which ' + 695 'has a child whose visiblity is inherited from the element and ' + 696 'the element was initially visible'); 697 698 await ensureElementRemoval(div); 699 } 700 ); 701 702 add_task( 703 async function restyling_animations_on_visibility_hidden_element_which_gets_visible() { 704 const div = addDiv(null, 705 { style: 'animation: on-main-thread 100s; visibility: hidden' }); 706 const animation = div.getAnimations()[0]; 707 708 709 await waitForAnimationReadyToRestyle(animation); 710 let restyleCount; 711 restyleCount = await observeStyling(5); 712 713 is(restyleCount, 0, 714 'Animations running on visibility hidden element should never ' + 715 'cause restyles'); 716 717 div.style.visibility = 'visible'; 718 await waitForNextFrame(); 719 720 restyleCount = await observeStyling(5); 721 is(restyleCount, 5, 722 'Animations running that was on visibility hidden element which ' + 723 'gets visible should not throttle restyling any more'); 724 725 await ensureElementRemoval(div); 726 } 727 ); 728 729 add_task(async function restyling_animations_in_visibility_changed_parent() { 730 const parentDiv = addDiv(null, { style: 'visibility: hidden' }); 731 const div = addDiv(null, { style: 'animation: on-main-thread 100s;' }); 732 parentDiv.appendChild(div); 733 734 const animation = div.getAnimations()[0]; 735 736 await waitForAnimationReadyToRestyle(animation); 737 let restyleCount; 738 restyleCount = await observeStyling(5); 739 740 is(restyleCount, 0, 741 'Animations running in visibility hidden parent should never cause ' + 742 'restyles'); 743 744 parentDiv.style.visibility = 'visible'; 745 await waitForNextFrame(); 746 747 restyleCount = await observeStyling(5); 748 is(restyleCount, 5, 749 'Animations that was in visibility hidden parent should not ' + 750 'throttle restyling any more'); 751 752 parentDiv.style.visibility = 'hidden'; 753 await waitForNextFrame(); 754 755 restyleCount = await observeStyling(5); 756 is(restyleCount, 0, 757 'Animations that the parent element became visible should throttle ' + 758 'restyling again'); 759 760 await ensureElementRemoval(parentDiv); 761 }); 762 763 add_task( 764 async function restyling_animations_on_visibility_hidden_element_with_visibility_changed_children() { 765 const div = addDiv(null, 766 { style: 'animation: on-main-thread 100s; visibility: hidden' }); 767 const animation = div.getAnimations()[0]; 768 769 await waitForAnimationReadyToRestyle(animation); 770 let restyleCount; 771 restyleCount = await observeStyling(5); 772 773 is(restyleCount, 0, 774 'Animations on visibility hidden element having no visible children ' + 775 'should never cause restyles'); 776 777 const childElement = addDiv(null, { style: 'visibility: visible' }); 778 div.appendChild(childElement); 779 await waitForNextFrame(); 780 781 restyleCount = await observeStyling(5); 782 is(restyleCount, 5, 783 'Animations running on visibility hidden element but the element has ' + 784 'a visible child should not throttle restyling'); 785 786 childElement.style.visibility = 'hidden'; 787 await waitForNextFrame(); 788 789 restyleCount = await observeStyling(5); 790 todo_is(restyleCount, 0, 791 'Animations running on visibility hidden element that a child ' + 792 'has become invisible should throttle restyling'); 793 794 childElement.style.visibility = 'visible'; 795 await waitForNextFrame(); 796 797 restyleCount = await observeStyling(5); 798 is(restyleCount, 5, 799 'Animations running on visibility hidden element should not throttle ' + 800 'restyling after the invisible element changed to visible'); 801 802 childElement.remove(); 803 await waitForNextFrame(); 804 805 restyleCount = await observeStyling(5); 806 todo_is(restyleCount, 0, 807 'Animations running on visibility hidden element should throttle ' + 808 'restyling again after all visible descendants were removed'); 809 810 await ensureElementRemoval(div); 811 } 812 ); 813 814 add_task( 815 async function restyling_animations_on_visiblity_hidden_element_having_oof_child() { 816 const div = addDiv(null, 817 { style: 'animation: on-main-thread 100s; position: absolute' }); 818 const childElement = addDiv(null, 819 { style: 'float: left; visibility: hidden' }); 820 div.appendChild(childElement); 821 822 const animation = div.getAnimations()[0]; 823 824 await waitForAnimationReadyToRestyle(animation); 825 826 // We don't check the animation causes restyles here since we already 827 // check it in the first test case. 828 829 div.style.visibility = 'hidden'; 830 await waitForNextFrame(); 831 832 const restyleCount = await observeStyling(5); 833 is(restyleCount, 0, 834 'Animations running on visibility hidden element which has an ' + 835 'out-of-flow child should throttle restyling'); 836 837 await ensureElementRemoval(div); 838 } 839 ); 840 841 add_task( 842 async function restyling_animations_on_visibility_hidden_element_having_grandchild() { 843 // element tree: 844 // 845 // root(visibility:hidden) 846 // / \ 847 // childA childB 848 // / \ / \ 849 // AA AB BA BB 850 851 const div = addDiv(null, 852 { style: 'animation: on-main-thread 100s; visibility: hidden' }); 853 854 const childA = addDiv(null); 855 div.appendChild(childA); 856 const childB = addDiv(null); 857 div.appendChild(childB); 858 859 const grandchildAA = addDiv(null); 860 childA.appendChild(grandchildAA); 861 const grandchildAB = addDiv(null); 862 childA.appendChild(grandchildAB); 863 864 const grandchildBA = addDiv(null); 865 childB.appendChild(grandchildBA); 866 const grandchildBB = addDiv(null); 867 childB.appendChild(grandchildBB); 868 869 const animation = div.getAnimations()[0]; 870 871 await waitForAnimationReadyToRestyle(animation); 872 let restyleCount; 873 restyleCount = await observeStyling(5); 874 is(restyleCount, 0, 875 'Animations on visibility hidden element having no visible ' + 876 'descendants should never cause restyles'); 877 878 childA.style.visibility = 'visible'; 879 grandchildAA.style.visibility = 'visible'; 880 grandchildAB.style.visibility = 'visible'; 881 await waitForNextFrame(); 882 883 restyleCount = await observeStyling(5); 884 is(restyleCount, 5, 885 'Animations running on visibility hidden element but the element has ' + 886 'visible children should not throttle restyling'); 887 888 // Make childA hidden again but both of grandchildAA and grandchildAB are 889 // still visible. 890 childA.style.visibility = 'hidden'; 891 await waitForNextFrame(); 892 893 restyleCount = await observeStyling(5); 894 is(restyleCount, 5, 895 'Animations running on visibility hidden element that a child has ' + 896 'become invisible again but there are still visible children should ' + 897 'not throttle restyling'); 898 899 // Make grandchildAA hidden but grandchildAB is still visible. 900 grandchildAA.style.visibility = 'hidden'; 901 await waitForNextFrame(); 902 903 restyleCount = await observeStyling(5); 904 is(restyleCount, 5, 905 'Animations running on visibility hidden element that a grandchild ' + 906 'become invisible again but another grandchild is still visible ' + 907 'should not throttle restyling'); 908 909 910 // Make childB and grandchildBA visible. 911 childB.style.visibility = 'visible'; 912 grandchildBA.style.visibility = 'visible'; 913 await waitForNextFrame(); 914 915 restyleCount = await observeStyling(5); 916 is(restyleCount, 5, 917 'Animations running on visibility hidden element but the element has ' + 918 'visible descendants should not throttle restyling'); 919 920 // Make childB hidden but grandchildAB and grandchildBA are still visible. 921 childB.style.visibility = 'hidden'; 922 await waitForNextFrame(); 923 924 restyleCount = await observeStyling(5); 925 is(restyleCount, 5, 926 'Animations running on visibility hidden element but the element has ' + 927 'visible grandchildren should not throttle restyling'); 928 929 // Make grandchildAB hidden but grandchildBA is still visible. 930 grandchildAB.style.visibility = 'hidden'; 931 await waitForNextFrame(); 932 933 restyleCount = await observeStyling(5); 934 is(restyleCount, 5, 935 'Animations running on visibility hidden element but the element has ' + 936 'a visible grandchild should not throttle restyling'); 937 938 // Make grandchildBA hidden. Now all descedants are invisible. 939 grandchildBA.style.visibility = 'hidden'; 940 await waitForNextFrame(); 941 942 restyleCount = await observeStyling(5); 943 todo_is(restyleCount, 0, 944 'Animations on visibility hidden element that all descendants have ' + 945 'become invisible again should never cause restyles'); 946 947 // Make childB visible. 948 childB.style.visibility = 'visible'; 949 await waitForNextFrame(); 950 951 restyleCount = await observeStyling(5); 952 is(restyleCount, 5, 953 'Animations on visibility hidden element that has a visible child ' + 954 'should never cause restyles'); 955 956 // Make childB invisible again 957 childB.style.visibility = 'hidden'; 958 await waitForNextFrame(); 959 960 restyleCount = await observeStyling(5); 961 todo_is(restyleCount, 0, 962 'Animations on visibility hidden element that the visible child ' + 963 'has become invisible again should never cause restyles'); 964 965 await ensureElementRemoval(div); 966 } 967 ); 968 969 add_task_if_omta_enabled(async function no_restyling_compositor_animations_after_pause_is_called() { 970 const div = addDiv(null, { style: 'animation: opacity 100s' }); 971 const animation = div.getAnimations()[0]; 972 973 await waitForAnimationReadyToRestyle(animation); 974 ok(SpecialPowers.wrap(animation).isRunningOnCompositor); 975 976 animation.pause(); 977 978 await animation.ready; 979 let restyleCount = await observeStyling(5); 980 is(restyleCount, 0, 981 'Paused animations running on the compositor should never cause ' + 982 'restyles'); 983 await ensureElementRemoval(div); 984 }); 985 986 add_task(async function no_restyling_main_thread_animations_after_pause_is_called() { 987 const div = addDiv(null, { style: 'animation: on-main-thread 100s' }); 988 const animation = div.getAnimations()[0]; 989 990 await waitForAnimationReadyToRestyle(animation); 991 992 animation.pause(); 993 994 await animation.ready; 995 let restyleCount = await observeStyling(5); 996 is(restyleCount, 0, 997 'Paused animations running on the main-thread should never cause ' + 998 'restyles'); 999 await ensureElementRemoval(div); 1000 }); 1001 1002 add_task_if_omta_enabled(async function only_one_restyling_when_current_time_is_set_to_middle_of_duration() { 1003 const div = addDiv(null, { style: 'animation: opacity 100s' }); 1004 const animation = div.getAnimations()[0]; 1005 1006 await waitForAnimationReadyToRestyle(animation); 1007 1008 animation.currentTime = 50 * MS_PER_SEC; 1009 1010 const restyleCount = await observeStyling(5); 1011 is(restyleCount, 1, 1012 'Bug 1235478: Animations running on the compositor should only once ' + 1013 'update style when currentTime is set to middle of duration time'); 1014 await ensureElementRemoval(div); 1015 }); 1016 1017 add_task_if_omta_enabled(async function change_duration_and_currenttime() { 1018 const div = addDiv(null); 1019 const animation = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC); 1020 1021 await waitForAnimationReadyToRestyle(animation); 1022 ok(SpecialPowers.wrap(animation).isRunningOnCompositor); 1023 1024 // Set currentTime to a time longer than duration. 1025 animation.currentTime = 500 * MS_PER_SEC; 1026 1027 // Now the animation immediately get back from compositor. 1028 ok(!SpecialPowers.wrap(animation).isRunningOnCompositor); 1029 1030 // Extend the duration. 1031 animation.effect.updateTiming({ duration: 800 * MS_PER_SEC }); 1032 const restyleCount = await observeStyling(5); 1033 is(restyleCount, 1, 1034 'Animations running on the compositor should update style ' + 1035 'when duration is made longer than the current time'); 1036 1037 await ensureElementRemoval(div); 1038 }); 1039 1040 add_task(async function script_animation_on_display_none_element() { 1041 const div = addDiv(null); 1042 const animation = div.animate({ zIndex: [ '0', '999' ] }, 1043 100 * MS_PER_SEC); 1044 1045 await waitForAnimationReadyToRestyle(animation); 1046 1047 div.style.display = 'none'; 1048 1049 // We need to wait a frame to apply display:none style. 1050 await waitForNextFrame(); 1051 1052 is(animation.playState, 'running', 1053 'Script animations keep running even when the target element has ' + 1054 '"display: none" style'); 1055 1056 ok(!SpecialPowers.wrap(animation).isRunningOnCompositor, 1057 'Script animations on "display:none" element should not run on the ' + 1058 'compositor'); 1059 1060 let restyleCount; 1061 restyleCount = await observeStyling(5); 1062 is(restyleCount, 0, 1063 'Script animations on "display: none" element should not update styles'); 1064 1065 div.style.display = ''; 1066 1067 restyleCount = await observeStyling(5); 1068 is(restyleCount, 5, 1069 'Script animations restored from "display: none" state should update ' + 1070 'styles'); 1071 1072 await ensureElementRemoval(div); 1073 }); 1074 1075 add_task_if_omta_enabled(async function compositable_script_animation_on_display_none_element() { 1076 const div = addDiv(null); 1077 const animation = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC); 1078 1079 await waitForAnimationReadyToRestyle(animation); 1080 1081 div.style.display = 'none'; 1082 1083 // We need to wait a frame to apply display:none style. 1084 await waitForNextFrame(); 1085 1086 is(animation.playState, 'running', 1087 'Opacity script animations keep running even when the target element ' + 1088 'has "display: none" style'); 1089 1090 ok(!SpecialPowers.wrap(animation).isRunningOnCompositor, 1091 'Opacity script animations on "display:none" element should not ' + 1092 'run on the compositor'); 1093 1094 let restyleCount; 1095 restyleCount = await observeStyling(5); 1096 is(restyleCount, 0, 1097 'Opacity script animations on "display: none" element should not ' + 1098 'update styles'); 1099 1100 div.style.display = ''; 1101 1102 restyleCount = await observeStyling(1); 1103 is(restyleCount, 1, 1104 'Script animations restored from "display: none" state should update ' + 1105 'styles soon'); 1106 1107 ok(SpecialPowers.wrap(animation).isRunningOnCompositor, 1108 'Opacity script animations restored from "display: none" should be ' + 1109 'run on the compositor in the next frame'); 1110 1111 await ensureElementRemoval(div); 1112 }); 1113 1114 add_task(async function restyling_for_empty_keyframes() { 1115 const div = addDiv(null); 1116 const animation = div.animate({ }, 100 * MS_PER_SEC); 1117 1118 await waitForAnimationReadyToRestyle(animation); 1119 let restyleCount; 1120 restyleCount = await observeStyling(5); 1121 1122 is(restyleCount, 0, 1123 'Animations with no keyframes should not cause restyles'); 1124 1125 animation.effect.setKeyframes({ zIndex: ['0', '999'] }); 1126 restyleCount = await observeStyling(5); 1127 1128 is(restyleCount, 5, 1129 'Setting valid keyframes should cause regular animation restyles to ' + 1130 'occur'); 1131 1132 animation.effect.setKeyframes({ }); 1133 restyleCount = await observeStyling(5); 1134 1135 is(restyleCount, 1, 1136 'Setting an empty set of keyframes should trigger a single restyle ' + 1137 'to remove the previous animated style'); 1138 1139 await ensureElementRemoval(div); 1140 }); 1141 1142 add_task_if_omta_enabled(async function no_restyling_when_animation_style_when_re_setting_same_animation_property() { 1143 const div = addDiv(null, { style: 'animation: opacity 100s' }); 1144 const animation = div.getAnimations()[0]; 1145 await waitForAnimationReadyToRestyle(animation); 1146 ok(SpecialPowers.wrap(animation).isRunningOnCompositor); 1147 // Apply the same animation style 1148 div.style.animation = 'opacity 100s'; 1149 const restyleCount = await observeStyling(5); 1150 is(restyleCount, 0, 1151 'Applying same animation style ' + 1152 'should never cause restyles'); 1153 await ensureElementRemoval(div); 1154 }); 1155 1156 add_task(async function necessary_update_should_be_invoked() { 1157 const div = addDiv(null, { style: 'animation: on-main-thread 100s' }); 1158 const animation = div.getAnimations()[0]; 1159 await waitForAnimationReadyToRestyle(animation); 1160 await waitForAnimationFrames(5); 1161 // Apply another animation style 1162 div.style.animation = 'on-main-thread 110s'; 1163 const restyleCount = await observeStyling(1); 1164 // There should be two restyles. 1165 // 1) Animation-only restyle for before applying the new animation style 1166 // 2) Animation-only restyle for after applying the new animation style 1167 is(restyleCount, 2, 1168 'Applying animation style with different duration ' + 1169 'should restyle twice'); 1170 await ensureElementRemoval(div); 1171 }); 1172 1173 add_task_if_omta_enabled( 1174 async function changing_cascading_result_for_main_thread_animation() { 1175 const div = addDiv(null, { style: 'on-main-thread: blue' }); 1176 const animation = div.animate({ opacity: [0, 1], 1177 zIndex: ['0', '999'] }, 1178 100 * MS_PER_SEC); 1179 await waitForAnimationReadyToRestyle(animation); 1180 ok(SpecialPowers.wrap(animation).isRunningOnCompositor, 1181 'The opacity animation is running on the compositor'); 1182 // Make the z-index style as !important to cause an update 1183 // to the cascade. 1184 // Bug 1300982: The z-index animation should be no longer 1185 // running on the main thread. 1186 div.style.setProperty('z-index', '0', 'important'); 1187 const restyleCount = await observeStyling(5); 1188 todo_is(restyleCount, 0, 1189 'Changing cascading result for the property running on the main ' + 1190 'thread does not cause synchronization layer of opacity animation ' + 1191 'running on the compositor'); 1192 await ensureElementRemoval(div); 1193 } 1194 ); 1195 1196 add_task_if_omta_enabled( 1197 async function animation_visibility_and_opacity() { 1198 const div = addDiv(null); 1199 const animation1 = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC); 1200 const animation2 = div.animate({ visibility: ['hidden', 'visible'] }, 100 * MS_PER_SEC); 1201 await waitForAnimationReadyToRestyle(animation1); 1202 await waitForAnimationReadyToRestyle(animation2); 1203 const restyleCount = await observeStyling(5); 1204 is(restyleCount, 5, 'The animation should not be throttled'); 1205 await ensureElementRemoval(div); 1206 } 1207 ); 1208 1209 add_task(async function restyling_for_animation_on_orphaned_element() { 1210 const div = addDiv(null); 1211 const animation = div.animate({ marginLeft: [ '0px', '100px' ] }, 1212 100 * MS_PER_SEC); 1213 1214 await waitForAnimationReadyToRestyle(animation); 1215 1216 div.remove(); 1217 let restyleCount; 1218 restyleCount = await observeStyling(5); 1219 is(restyleCount, 0, 1220 'Animation on orphaned element should not cause restyles'); 1221 1222 document.body.appendChild(div); 1223 1224 await waitForNextFrame(); 1225 restyleCount = await observeStyling(5); 1226 1227 is(restyleCount, 5, 1228 'Animation on re-attached to the document begins to update style, got ' + restyleCount); 1229 1230 await ensureElementRemoval(div); 1231 }); 1232 1233 add_task_if_omta_enabled( 1234 // Tests that if we remove an element from the document whose animation 1235 // cascade needs recalculating, that it is correctly updated when it is 1236 // re-attached to the document. 1237 async function restyling_for_opacity_animation_on_re_attached_element() { 1238 const div = addDiv(null, { style: 'opacity: 1 ! important' }); 1239 const animation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC); 1240 1241 await waitForAnimationReadyToRestyle(animation); 1242 ok(!SpecialPowers.wrap(animation).isRunningOnCompositor, 1243 'The opacity animation overridden by an !important rule is NOT ' + 1244 'running on the compositor'); 1245 1246 // Drop the !important rule to update the cascade. 1247 div.style.setProperty('opacity', '1', ''); 1248 1249 div.remove(); 1250 1251 const restyleCount = await observeStyling(5); 1252 is(restyleCount, 0, 1253 'Opacity animation on orphaned element should not cause restyles'); 1254 1255 document.body.appendChild(div); 1256 1257 // Need a frame to give the animation a chance to be sent to the 1258 // compositor. 1259 await waitForNextFrame(); 1260 1261 ok(SpecialPowers.wrap(animation).isRunningOnCompositor, 1262 'The opacity animation which is no longer overridden by the ' + 1263 '!important rule begins running on the compositor even if the ' + 1264 '!important rule had been dropped before the target element was ' + 1265 'removed'); 1266 1267 await ensureElementRemoval(div); 1268 } 1269 ); 1270 1271 add_task( 1272 async function no_throttling_additive_animations_out_of_view_element() { 1273 const div = addDiv(null, { style: 'transform: translateY(-400px);' }); 1274 const animation = 1275 div.animate([{ visibility: 'visible' }], 1276 { duration: 100 * MS_PER_SEC, composite: 'add' }); 1277 1278 await waitForAnimationReadyToRestyle(animation); 1279 1280 const restyleCount = await observeStyling(5); 1281 1282 is(restyleCount, 0, 1283 'Additive animation has no keyframe whose offset is 0 or 1 in an ' + 1284 'out-of-view element should be throttled'); 1285 await ensureElementRemoval(div); 1286 } 1287 ); 1288 1289 // Tests that missing keyframes animations don't throttle at all. 1290 add_task(async function no_throttling_animations_out_of_view_element() { 1291 const div = addDiv(null, { style: 'transform: translateY(-400px);' }); 1292 const animation = 1293 div.animate([{ visibility: 'visible' }], 100 * MS_PER_SEC); 1294 1295 await waitForAnimationReadyToRestyle(animation); 1296 1297 const restyleCount = await observeStyling(5); 1298 1299 is(restyleCount, 0, 1300 'Discrete animation has has no keyframe whose offset is 0 or 1 in an ' + 1301 'out-of-view element should be throttled'); 1302 await ensureElementRemoval(div); 1303 }); 1304 1305 // Tests that missing keyframes animation on scrolled out element that the 1306 // animation is not able to be throttled. 1307 add_task( 1308 async function no_throttling_missing_keyframe_animations_out_of_view_element() { 1309 const div = 1310 addDiv(null, { style: 'transform: translateY(-400px);' + 1311 'visibility: collapse;' }); 1312 const animation = 1313 div.animate([{ visibility: 'visible' }], 100 * MS_PER_SEC); 1314 await waitForAnimationReadyToRestyle(animation); 1315 1316 const restyleCount = await observeStyling(5); 1317 is(restyleCount, 0, 1318 'visibility animation has no keyframe whose offset is 0 or 1 in an ' + 1319 'out-of-view element should be throttled'); 1320 await ensureElementRemoval(div); 1321 } 1322 ); 1323 1324 // Counter part of the above test. 1325 add_task(async function no_restyling_discrete_animations_out_of_view_element() { 1326 const div = addDiv(null, { style: 'transform: translateY(-400px);' }); 1327 const animation = 1328 div.animate({ visibility: ['visible', 'hidden'] }, 100 * MS_PER_SEC); 1329 1330 await waitForAnimationReadyToRestyle(animation); 1331 1332 const restyleCount = await observeStyling(5); 1333 1334 is(restyleCount, 0, 1335 'Discrete animation running on the main-thread in an out-of-view ' + 1336 'element should never cause restyles'); 1337 await ensureElementRemoval(div); 1338 }); 1339 1340 add_task(async function no_restyling_while_computed_timing_is_not_changed() { 1341 const div = addDiv(null); 1342 const animation = div.animate({ zIndex: [ '0', '999' ] }, 1343 { duration: 100 * MS_PER_SEC, 1344 easing: 'step-end' }); 1345 1346 await waitForAnimationReadyToRestyle(animation); 1347 1348 const restyleCount = await observeStyling(5); 1349 1350 // We possibly expect one restyle from the initial animation compose, in 1351 // order to update animations, but nothing else. 1352 ok(restyleCount <= 1, 1353 'Animation running on the main-thread while computed timing is not ' + 1354 'changed should not cause extra restyles, got ' + restyleCount); 1355 await ensureElementRemoval(div); 1356 }); 1357 1358 add_task(async function no_throttling_animations_in_view_svg() { 1359 const div = addDiv(null, { style: 'overflow: scroll;' + 1360 'height: 100px; width: 100px;' }); 1361 const svg = addSVGElement(div, 'svg', { viewBox: '-10 -10 0.1 0.1', 1362 width: '50px', 1363 height: '50px' }); 1364 const rect = addSVGElement(svg, 'rect', { x: '-10', 1365 y: '-10', 1366 width: '10', 1367 height: '10', 1368 fill: 'red' }); 1369 const animation = rect.animate({ fill: ['blue', 'lime'] }, 100 * MS_PER_SEC); 1370 await waitForAnimationReadyToRestyle(animation); 1371 1372 const restyleCount = await observeStyling(5); 1373 is(restyleCount, 5, 1374 'CSS animations on an in-view svg element with post-transform should ' + 1375 'not be throttled.'); 1376 1377 await ensureElementRemoval(div); 1378 }); 1379 1380 add_task_if_omta_enabled(async function svg_non_scaling_stroke_animation() { 1381 const div = addDiv(null, { style: 'overflow: scroll;' + 1382 'height: 100px; width: 100px;' }); 1383 const svg = addSVGElement(div, 'svg', { viewBox: '0 0 250 250', 1384 width: '40px', 1385 height: '40px' }); 1386 const rect = addSVGElement(svg, 'rect', { x: '0', 1387 y: '0', 1388 width: '250', 1389 height: '250', 1390 fill: 'red', 1391 style: 'vector-effect: non-scaling-stroke; animation: rotate 100s infinite;'}); 1392 const animation = rect.getAnimations()[0]; 1393 await waitForAnimationReadyToRestyle(animation); 1394 1395 ok(!SpecialPowers.wrap(animation).isRunningOnCompositor, 1396 'The animation of a non-scaling-stroke element is not running on the compositor'); 1397 1398 await ensureElementRemoval(div); 1399 }); 1400 1401 add_task(async function no_throttling_animations_in_transformed_parent() { 1402 const div = addDiv(null, { style: 'overflow: scroll;' + 1403 'transform: translateX(50px);' }); 1404 const svg = addSVGElement(div, 'svg', { viewBox: '0 0 1250 1250', 1405 width: '40px', 1406 height: '40px' }); 1407 const rect = addSVGElement(svg, 'rect', { x: '0', 1408 y: '0', 1409 width: '1250', 1410 height: '1250', 1411 fill: 'red' }); 1412 const animation = rect.animate({ fill: ['blue', 'lime'] }, 100 * MS_PER_SEC); 1413 await waitForAnimationReadyToRestyle(animation); 1414 1415 const restyleCount = await observeStyling(5); 1416 is(restyleCount, 5, 1417 'CSS animations on an in-view svg element which is inside transformed ' + 1418 'parent should not be throttled.'); 1419 1420 await ensureElementRemoval(div); 1421 }); 1422 1423 add_task(async function throttling_animations_out_of_view_svg() { 1424 const div = addDiv(null, { style: 'overflow: scroll;' + 1425 'height: 100px; width: 100px;' }); 1426 const svg = addSVGElement(div, 'svg', { viewBox: '-10 -10 0.1 0.1', 1427 width: '50px', 1428 height: '50px' }); 1429 const rect = addSVGElement(svg, 'rect', { width: '10', 1430 height: '10', 1431 fill: 'red' }); 1432 1433 const animation = rect.animate({ fill: ['blue', 'lime'] }, 100 * MS_PER_SEC); 1434 await waitForAnimationReadyToRestyle(animation); 1435 1436 const restyleCount = await observeStyling(5); 1437 is(restyleCount, 0, 1438 'CSS animations on an out-of-view svg element with post-transform ' + 1439 'should be throttled.'); 1440 1441 await ensureElementRemoval(div); 1442 }); 1443 1444 add_task(async function no_throttling_animations_in_view_css_transform() { 1445 const scrollDiv = addDiv(null, { style: 'overflow: scroll; ' + 1446 'height: 100px; width: 100px;' }); 1447 const targetDiv = addDiv(null, 1448 { style: 'animation: on-main-thread 100s;' + 1449 'transform: translate(-50px, -50px);' }); 1450 scrollDiv.appendChild(targetDiv); 1451 1452 const animation = targetDiv.getAnimations()[0]; 1453 await waitForAnimationReadyToRestyle(animation); 1454 1455 const restyleCount = await observeStyling(5); 1456 is(restyleCount, 5, 1457 'CSS animation on an in-view element with pre-transform should not ' + 1458 'be throttled.'); 1459 1460 await ensureElementRemoval(scrollDiv); 1461 }); 1462 1463 add_task(async function throttling_animations_out_of_view_css_transform() { 1464 const scrollDiv = addDiv(null, { style: 'overflow: scroll;' + 1465 'height: 100px; width: 100px;' }); 1466 const targetDiv = addDiv(null, 1467 { style: 'animation: on-main-thread 100s;' + 1468 'transform: translate(100px, 100px);' }); 1469 scrollDiv.appendChild(targetDiv); 1470 1471 const animation = targetDiv.getAnimations()[0]; 1472 await waitForAnimationReadyToRestyle(animation); 1473 1474 const restyleCount = await observeStyling(5); 1475 is(restyleCount, 0, 1476 'CSS animation on an out-of-view element with pre-transform should be ' + 1477 'throttled.'); 1478 1479 await ensureElementRemoval(scrollDiv); 1480 }); 1481 1482 add_task( 1483 async function throttling_animations_in_out_of_view_position_absolute_element() { 1484 const parentDiv = addDiv(null, 1485 { style: 'position: absolute; top: -1000px;' }); 1486 const targetDiv = addDiv(null, 1487 { style: 'animation: on-main-thread 100s;' }); 1488 parentDiv.appendChild(targetDiv); 1489 1490 const animation = targetDiv.getAnimations()[0]; 1491 await waitForAnimationReadyToRestyle(animation); 1492 1493 const restyleCount = await observeStyling(5); 1494 is(restyleCount, 0, 1495 'CSS animation in an out-of-view position absolute element should ' + 1496 'be throttled'); 1497 1498 await ensureElementRemoval(parentDiv); 1499 } 1500 ); 1501 1502 add_task( 1503 async function throttling_animations_on_out_of_view_position_absolute_element() { 1504 const div = addDiv(null, 1505 { style: 'animation: on-main-thread 100s; ' + 1506 'position: absolute; top: -1000px;' }); 1507 1508 const animation = div.getAnimations()[0]; 1509 await waitForAnimationReadyToRestyle(animation); 1510 1511 const restyleCount = await observeStyling(5); 1512 is(restyleCount, 0, 1513 'CSS animation on an out-of-view position absolute element should ' + 1514 'be throttled'); 1515 1516 await ensureElementRemoval(div); 1517 } 1518 ); 1519 1520 add_task( 1521 async function throttling_animations_in_out_of_view_position_fixed_element() { 1522 const parentDiv = addDiv(null, 1523 { style: 'position: fixed; top: -1000px;' }); 1524 const targetDiv = addDiv(null, 1525 { style: 'animation: on-main-thread 100s;' }); 1526 parentDiv.appendChild(targetDiv); 1527 1528 const animation = targetDiv.getAnimations()[0]; 1529 await waitForAnimationReadyToRestyle(animation); 1530 1531 const restyleCount = await observeStyling(5); 1532 is(restyleCount, 0, 1533 'CSS animation on an out-of-view position:fixed element should be ' + 1534 'throttled'); 1535 1536 await ensureElementRemoval(parentDiv); 1537 } 1538 ); 1539 1540 add_task( 1541 async function throttling_animations_on_out_of_view_position_fixed_element() { 1542 const div = addDiv(null, 1543 { style: 'animation: on-main-thread 100s; ' + 1544 'position: fixed; top: -1000px;' }); 1545 1546 const animation = div.getAnimations()[0]; 1547 await waitForAnimationReadyToRestyle(animation); 1548 1549 const restyleCount = await observeStyling(5); 1550 is(restyleCount, 0, 1551 'CSS animation on an out-of-view position:fixed element should be ' + 1552 'throttled'); 1553 1554 await ensureElementRemoval(div); 1555 } 1556 ); 1557 1558 add_task( 1559 async function no_throttling_animations_in_view_position_fixed_element() { 1560 const iframe = document.createElement('iframe'); 1561 iframe.setAttribute('srcdoc', '<div id="target"></div>'); 1562 document.documentElement.appendChild(iframe); 1563 1564 await new Promise(resolve => { 1565 iframe.addEventListener('load', () => { 1566 resolve(); 1567 }); 1568 }); 1569 1570 const target = iframe.contentDocument.getElementById('target'); 1571 target.style= 'position: fixed; top: 20px; width: 100px; height: 100px;'; 1572 1573 const animation = target.animate({ zIndex: [ '0', '999' ] }, 1574 { duration: 100 * MS_PER_SEC, 1575 iterations: Infinity }); 1576 await waitForAnimationReadyToRestyle(animation); 1577 1578 const restyleCount = await observeStylingInTargetWindow(iframe.contentWindow, 5); 1579 is(restyleCount, 5, 1580 'CSS animation on an in-view position:fixed element should NOT be ' + 1581 'throttled'); 1582 1583 await ensureElementRemoval(iframe); 1584 } 1585 ); 1586 1587 add_task( 1588 async function throttling_position_absolute_animations_in_collapsed_iframe() { 1589 const iframe = document.createElement('iframe'); 1590 iframe.setAttribute('srcdoc', '<div id="target"></div>'); 1591 iframe.style.height = '0px'; 1592 document.documentElement.appendChild(iframe); 1593 1594 await new Promise(resolve => { 1595 iframe.addEventListener('load', () => { 1596 resolve(); 1597 }); 1598 }); 1599 1600 const target = iframe.contentDocument.getElementById("target"); 1601 target.style= 'position: absolute; top: 50%; width: 100px; height: 100px'; 1602 1603 const animation = target.animate({ opacity: [0, 1] }, 1604 { duration: 100 * MS_PER_SEC, 1605 iterations: Infinity }); 1606 await waitForAnimationReadyToRestyle(animation); 1607 1608 const restyleCount = await observeStylingInTargetWindow(iframe.contentWindow, 5); 1609 is(restyleCount, 0, 1610 'Animation on position:absolute element in collapsed iframe should ' + 1611 'be throttled'); 1612 1613 await ensureElementRemoval(iframe); 1614 } 1615 ); 1616 1617 add_task( 1618 async function position_absolute_animations_in_collapsed_element() { 1619 const parent = addDiv(null, { style: 'overflow: scroll; height: 0px;' }); 1620 const target = addDiv(null, 1621 { style: 'animation: on-main-thread 100s infinite;' + 1622 'position: absolute; top: 50%;' + 1623 'width: 100px; height: 100px;' }); 1624 parent.appendChild(target); 1625 1626 const animation = target.getAnimations()[0]; 1627 await waitForAnimationReadyToRestyle(animation); 1628 1629 const restyleCount = await observeStyling(5); 1630 is(restyleCount, 5, 1631 'Animation on position:absolute element in collapsed element ' + 1632 'should not be throttled'); 1633 1634 await ensureElementRemoval(parent); 1635 } 1636 ); 1637 1638 add_task( 1639 async function throttling_position_absolute_animations_in_collapsed_element() { 1640 const parent = addDiv(null, { style: 'overflow: scroll; height: 0px;' }); 1641 const target = addDiv(null, 1642 { style: 'animation: on-main-thread 100s infinite;' + 1643 'position: absolute; top: 50%;' }); 1644 parent.appendChild(target); 1645 1646 const animation = target.getAnimations()[0]; 1647 await waitForAnimationReadyToRestyle(animation); 1648 1649 const restyleCount = await observeStyling(5); 1650 todo_is(restyleCount, 0, 1651 'Animation on collapsed position:absolute element in collapsed ' + 1652 'element should be throttled'); 1653 1654 await ensureElementRemoval(parent); 1655 } 1656 ); 1657 1658 add_task_if_omta_enabled( 1659 async function no_restyling_for_compositor_animation_on_unrelated_style_change() { 1660 const div = addDiv(null); 1661 const animation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC); 1662 1663 await waitForAnimationReadyToRestyle(animation); 1664 ok(SpecialPowers.wrap(animation).isRunningOnCompositor, 1665 'The opacity animation is running on the compositor'); 1666 1667 div.style.setProperty('color', 'blue', ''); 1668 const restyleCount = await observeStyling(5); 1669 is(restyleCount, 0, 1670 'The opacity animation keeps running on the compositor when ' + 1671 'color style is changed'); 1672 await ensureElementRemoval(div); 1673 } 1674 ); 1675 1676 add_task( 1677 async function no_overflow_transform_animations_in_scrollable_element() { 1678 const parentElement = addDiv(null, 1679 { style: 'overflow-y: scroll; height: 100px;' }); 1680 const div = addDiv(null); 1681 const animation = 1682 div.animate({ transform: [ 'translateY(10px)', 'translateY(10px)' ] }, 1683 100 * MS_PER_SEC); 1684 parentElement.appendChild(div); 1685 1686 await waitForAnimationReadyToRestyle(animation); 1687 ok(SpecialPowers.wrap(animation).isRunningOnCompositor); 1688 1689 const restyleCount = await observeStyling(20); 1690 is(restyleCount, 0, 1691 'No-overflow transform animations running on the compositor should ' + 1692 'never update style on the main thread'); 1693 1694 await ensureElementRemoval(parentElement); 1695 } 1696 ); 1697 1698 add_task(async function no_flush_on_getAnimations() { 1699 const div = addDiv(null); 1700 const animation = 1701 div.animate({ opacity: [ '0', '1' ] }, 100 * MS_PER_SEC); 1702 await waitForAnimationReadyToRestyle(animation); 1703 1704 ok(SpecialPowers.wrap(animation).isRunningOnCompositor); 1705 1706 const restyleCount = observeAnimSyncStyling(() => { 1707 is(div.getAnimations().length, 1, 'There should be one animation'); 1708 }); 1709 is(restyleCount, 0, 1710 'Element.getAnimations() should not flush throttled animation style'); 1711 1712 await ensureElementRemoval(div); 1713 }); 1714 1715 add_task(async function restyling_for_throttled_animation_on_getAnimations() { 1716 const div = addDiv(null, { style: 'animation: opacity 100s' }); 1717 const animation = div.getAnimations()[0]; 1718 1719 await waitForAnimationReadyToRestyle(animation); 1720 ok(SpecialPowers.wrap(animation).isRunningOnCompositor); 1721 1722 const restyleCount = observeAnimSyncStyling(() => { 1723 div.style.animationDuration = '0s'; 1724 is(div.getAnimations().length, 0, 'There should be no animation'); 1725 }); 1726 1727 is(restyleCount, 1, // For discarding the throttled animation. 1728 'Element.getAnimations() should flush throttled animation style so ' + 1729 'that the throttled animation is discarded'); 1730 1731 await ensureElementRemoval(div); 1732 }); 1733 1734 add_task( 1735 async function no_restyling_for_throttled_animation_on_querying_play_state() { 1736 const div = addDiv(null, { style: 'animation: opacity 100s' }); 1737 const animation = div.getAnimations()[0]; 1738 const sibling = addDiv(null); 1739 1740 await waitForAnimationReadyToRestyle(animation); 1741 ok(SpecialPowers.wrap(animation).isRunningOnCompositor); 1742 1743 const restyleCount = observeAnimSyncStyling(() => { 1744 sibling.style.opacity = '0.5'; 1745 is(animation.playState, 'running', 1746 'Animation.playState should be running'); 1747 }); 1748 is(restyleCount, 0, 1749 'Animation.playState should not flush throttled animation in the ' + 1750 'case where there are only style changes that don\'t affect the ' + 1751 'throttled animation'); 1752 1753 await ensureElementRemoval(div); 1754 await ensureElementRemoval(sibling); 1755 } 1756 ); 1757 1758 add_task( 1759 async function restyling_for_throttled_animation_on_querying_play_state() { 1760 const div = addDiv(null, { style: 'animation: opacity 100s' }); 1761 const animation = div.getAnimations()[0]; 1762 1763 await waitForAnimationReadyToRestyle(animation); 1764 ok(SpecialPowers.wrap(animation).isRunningOnCompositor); 1765 1766 const restyleCount = observeAnimSyncStyling(() => { 1767 div.style.animationPlayState = 'paused'; 1768 is(animation.playState, 'paused', 1769 'Animation.playState should be reflected by pending style'); 1770 }); 1771 1772 is(restyleCount, 1, 1773 'Animation.playState should flush throttled animation style that ' + 1774 'affects the throttled animation'); 1775 1776 await ensureElementRemoval(div); 1777 } 1778 ); 1779 1780 add_task( 1781 async function no_restyling_for_throttled_transition_on_querying_play_state() { 1782 const div = addDiv(null, { style: 'transition: opacity 100s; opacity: 0' }); 1783 const sibling = addDiv(null); 1784 1785 getComputedStyle(div).opacity; 1786 div.style.opacity = 1; 1787 1788 const transition = div.getAnimations()[0]; 1789 1790 await waitForAnimationReadyToRestyle(transition); 1791 ok(SpecialPowers.wrap(transition).isRunningOnCompositor); 1792 1793 const restyleCount = observeAnimSyncStyling(() => { 1794 sibling.style.opacity = '0.5'; 1795 is(transition.playState, 'running', 1796 'Animation.playState should be running'); 1797 }); 1798 1799 is(restyleCount, 0, 1800 'Animation.playState should not flush throttled transition in the ' + 1801 'case where there are only style changes that don\'t affect the ' + 1802 'throttled animation'); 1803 1804 await ensureElementRemoval(div); 1805 await ensureElementRemoval(sibling); 1806 } 1807 ); 1808 1809 add_task( 1810 async function restyling_for_throttled_transition_on_querying_play_state() { 1811 const div = addDiv(null, { style: 'transition: opacity 100s; opacity: 0' }); 1812 getComputedStyle(div).opacity; 1813 div.style.opacity = '1'; 1814 1815 const transition = div.getAnimations()[0]; 1816 1817 await waitForAnimationReadyToRestyle(transition); 1818 ok(SpecialPowers.wrap(transition).isRunningOnCompositor); 1819 1820 const restyleCount = observeAnimSyncStyling(() => { 1821 div.style.transitionProperty = 'none'; 1822 is(transition.playState, 'idle', 1823 'Animation.playState should be reflected by pending style change ' + 1824 'which cancel the transition'); 1825 }); 1826 1827 is(restyleCount, 1, 1828 'Animation.playState should flush throttled transition style that ' + 1829 'affects the throttled animation'); 1830 1831 await ensureElementRemoval(div); 1832 } 1833 ); 1834 1835 add_task(async function restyling_visibility_animations_on_in_view_element() { 1836 const div = addDiv(null); 1837 const animation = 1838 div.animate({ visibility: ['hidden', 'visible'] }, 100 * MS_PER_SEC); 1839 1840 await waitForAnimationReadyToRestyle(animation); 1841 1842 const restyleCount = await observeStyling(5); 1843 1844 is(restyleCount, 5, 1845 'Visibility animation running on the main-thread on in-view element ' + 1846 'should not be throttled'); 1847 await ensureElementRemoval(div); 1848 }); 1849 1850 add_task(async function restyling_outline_offset_animations_on_invisible_element() { 1851 const div = addDiv(null, 1852 { style: 'visibility: hidden; ' + 1853 'outline-style: solid; ' + 1854 'outline-width: 1px;' }); 1855 const animation = 1856 div.animate({ outlineOffset: [ '0px', '10px' ] }, 1857 { duration: 100 * MS_PER_SEC, 1858 iterations: Infinity }); 1859 1860 await waitForAnimationReadyToRestyle(animation); 1861 1862 const restyleCount = await observeStyling(5); 1863 1864 is(restyleCount, 0, 1865 'Outline offset animation running on the main-thread on invisible ' + 1866 'element should be throttled'); 1867 await ensureElementRemoval(div); 1868 }); 1869 1870 add_task(async function restyling_transform_animations_on_invisible_element() { 1871 const div = addDiv(null, { style: 'visibility: hidden;' }); 1872 1873 const animation = 1874 div.animate({ transform: [ 'none', 'rotate(360deg)' ] }, 1875 { duration: 100 * MS_PER_SEC, 1876 iterations: Infinity }); 1877 1878 await waitForAnimationReadyToRestyle(animation); 1879 1880 ok(!SpecialPowers.wrap(animation).isRunningOnCompositor); 1881 1882 const restyleCount = await observeStyling(5); 1883 1884 is(restyleCount, 0, 1885 'Transform animations on visibility hidden element should be throttled'); 1886 await ensureElementRemoval(div); 1887 }); 1888 1889 add_task(async function restyling_transform_animations_on_invisible_element() { 1890 const div = addDiv(null, { style: 'visibility: hidden;' }); 1891 1892 const animation = 1893 div.animate([ { transform: 'rotate(360deg)' } ], 1894 { duration: 100 * MS_PER_SEC, 1895 iterations: Infinity }); 1896 1897 await waitForAnimationReadyToRestyle(animation); 1898 1899 ok(!SpecialPowers.wrap(animation).isRunningOnCompositor); 1900 1901 const restyleCount = await observeStyling(5); 1902 1903 is(restyleCount, 0, 1904 'Transform animations without 100% keyframe on visibility hidden ' + 1905 'element should be throttled'); 1906 await ensureElementRemoval(div); 1907 }); 1908 1909 add_task(async function restyling_translate_animations_on_invisible_element() { 1910 const div = addDiv(null, { style: 'visibility: hidden;' }); 1911 1912 const animation = 1913 div.animate([ { translate: '100px' } ], 1914 { duration: 100 * MS_PER_SEC, 1915 iterations: Infinity }); 1916 1917 await waitForAnimationReadyToRestyle(animation); 1918 1919 ok(!SpecialPowers.wrap(animation).isRunningOnCompositor); 1920 1921 const restyleCount = await observeStyling(5); 1922 1923 is(restyleCount, 0, 1924 'Translate animations without 100% keyframe on visibility hidden ' + 1925 'element should be throttled'); 1926 await ensureElementRemoval(div); 1927 }); 1928 1929 add_task(async function restyling_rotate_animations_on_invisible_element() { 1930 const div = addDiv(null, { style: 'visibility: hidden;' }); 1931 1932 const animation = 1933 div.animate([ { rotate: '45deg' } ], 1934 { duration: 100 * MS_PER_SEC, 1935 iterations: Infinity }); 1936 1937 await waitForAnimationReadyToRestyle(animation); 1938 1939 ok(!SpecialPowers.wrap(animation).isRunningOnCompositor); 1940 1941 const restyleCount = await observeStyling(5); 1942 1943 is(restyleCount, 0, 1944 'Rotate animations without 100% keyframe on visibility hidden ' + 1945 'element should be throttled'); 1946 await ensureElementRemoval(div); 1947 }); 1948 1949 add_task(async function restyling_scale_animations_on_invisible_element() { 1950 const div = addDiv(null, { style: 'visibility: hidden;' }); 1951 1952 const animation = 1953 div.animate([ { scale: '2 2' } ], 1954 { duration: 100 * MS_PER_SEC, 1955 iterations: Infinity }); 1956 1957 await waitForAnimationReadyToRestyle(animation); 1958 1959 ok(!SpecialPowers.wrap(animation).isRunningOnCompositor); 1960 1961 const restyleCount = await observeStyling(5); 1962 1963 is(restyleCount, 0, 1964 'Scale animations without 100% keyframe on visibility hidden ' + 1965 'element should be throttled'); 1966 await ensureElementRemoval(div); 1967 }); 1968 1969 add_task( 1970 async function restyling_transform_animations_having_abs_pos_child_on_invisible_element() { 1971 const div = addDiv(null, { style: 'visibility: hidden;' }); 1972 const child = addDiv(null, { style: 'position: absolute; top: 100px;' }); 1973 div.appendChild(child); 1974 1975 const animation = 1976 div.animate({ transform: [ 'none', 'rotate(360deg)' ] }, 1977 { duration: 100 * MS_PER_SEC, 1978 iterations: Infinity }); 1979 1980 await waitForAnimationReadyToRestyle(animation); 1981 1982 ok(!SpecialPowers.wrap(animation).isRunningOnCompositor); 1983 1984 const restyleCount = await observeStyling(5); 1985 1986 is(restyleCount, 0, 1987 'Transform animation having an absolutely positioned child on ' + 1988 'visibility hidden element should be throttled'); 1989 await ensureElementRemoval(div); 1990 }); 1991 1992 add_task(async function no_restyling_animations_in_out_of_view_iframe() { 1993 const div = addDiv(null, { style: 'overflow-y: scroll; height: 100px;' }); 1994 1995 const iframe = document.createElement('iframe'); 1996 iframe.setAttribute( 1997 'srcdoc', 1998 '<div style="height: 100px;"></div><div id="target"></div>'); 1999 div.appendChild(iframe); 2000 2001 await new Promise(resolve => { 2002 iframe.addEventListener('load', () => { 2003 resolve(); 2004 }); 2005 }); 2006 2007 const target = iframe.contentDocument.getElementById("target"); 2008 target.style= 'width: 100px; height: 100px;'; 2009 2010 const animation = target.animate({ zIndex: [ '0', '999' ] }, 2011 100 * MS_PER_SEC); 2012 await waitForAnimationReadyToRestyle(animation); 2013 2014 const restyleCount = await observeStylingInTargetWindow(iframe.contentWindow, 5); 2015 is(restyleCount, 0, 2016 'Animation in out-of-view iframe should be throttled'); 2017 2018 await ensureElementRemoval(div); 2019 }); 2020 2021 // Tests that transform animations are not able to run on the compositor due 2022 // to layout restrictions (e.g. animations on a large size frame) doesn't 2023 // flush layout at all. 2024 add_task(async function flush_layout_for_transform_animations() { 2025 // Set layout.animation.prerender.partial to disallow transform animations 2026 // on large frames to be sent to the compositor. 2027 await SpecialPowers.pushPrefEnv({ 2028 set: [['layout.animation.prerender.partial', false]] }); 2029 const div = addDiv(null, { style: 'width: 10000px; height: 10000px;' }); 2030 2031 const animation = div.animate([ { transform: 'rotate(360deg)', } ], 2032 { duration: 100 * MS_PER_SEC, 2033 // Set step-end to skip further restyles. 2034 easing: 'step-end' }); 2035 2036 const FLUSH_LAYOUT = SpecialPowers.DOMWindowUtils.FLUSH_LAYOUT; 2037 ok(SpecialPowers.DOMWindowUtils.needsFlush(FLUSH_LAYOUT), 2038 'Flush is needed for the appended div'); 2039 2040 await waitForAnimationReadyToRestyle(animation); 2041 2042 ok(!SpecialPowers.wrap(animation).isRunningOnCompositor, "Shouldn't be running in the compositor"); 2043 2044 // We expect one restyle from the initial animation compose. 2045 await waitForNextFrame(); 2046 2047 ok(!SpecialPowers.wrap(animation).isRunningOnCompositor, "Still shouldn't be running in the compositor"); 2048 ok(!SpecialPowers.DOMWindowUtils.needsFlush(FLUSH_LAYOUT), 2049 'No further layout flush needed'); 2050 2051 await ensureElementRemoval(div); 2052 }); 2053 2054 add_task(async function partial_prerendered_transform_animations() { 2055 await SpecialPowers.pushPrefEnv({ 2056 set: [['layout.animation.prerender.partial', true]] }); 2057 const div = addDiv(null, { style: 'width: 10000px; height: 10000px;' }); 2058 2059 const animation = div.animate( 2060 // Use the same value both for `from` and `to` to avoid jank on the 2061 // compositor. 2062 { transform: ['rotate(0deg)', 'rotate(0deg)'] }, 2063 100 * MS_PER_SEC 2064 ); 2065 2066 await waitForAnimationReadyToRestyle(animation); 2067 2068 const restyleCount = await observeStyling(5) 2069 is(restyleCount, 0, 2070 'Transform animation with partial pre-rendered should never cause ' + 2071 'restyles'); 2072 2073 await ensureElementRemoval(div); 2074 }); 2075 2076 add_task(async function restyling_on_create_animation() { 2077 const div = addDiv(); 2078 let priorAnimationTriggeredRestyles = SpecialPowers.DOMWindowUtils.animationTriggeredRestyles; 2079 2080 const animationA = div.animate( 2081 { transform: ['none', 'rotate(360deg)'] }, 2082 100 * MS_PER_SEC 2083 ); 2084 const animationB = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC); 2085 const animationC = div.animate( 2086 { color: ['blue', 'green'] }, 2087 100 * MS_PER_SEC 2088 ); 2089 const animationD = div.animate( 2090 { width: ['100px', '200px'] }, 2091 100 * MS_PER_SEC 2092 ); 2093 const animationE = div.animate( 2094 { height: ['100px', '200px'] }, 2095 100 * MS_PER_SEC 2096 ); 2097 2098 const restyleCount = SpecialPowers.DOMWindowUtils.animationTriggeredRestyles - priorAnimationTriggeredRestyles; 2099 2100 is(restyleCount, 0, 'Creating animations should not flush styles'); 2101 2102 await ensureElementRemoval(div); 2103 }); 2104 2105 add_task(async function out_of_view_background_position() { 2106 const div = addDiv(null, { 2107 style: ` 2108 background-image: linear-gradient(90deg, rgb(224, 224, 224), rgb(241, 241, 241) 30%, rgb(224, 224, 224) 60%); 2109 background-size: 80px; 2110 animation: background-position 100s infinite; 2111 transform: translateY(-400px); 2112 `, 2113 }) 2114 2115 const animation = div.getAnimations()[0]; 2116 2117 await waitForAnimationReadyToRestyle(animation); 2118 2119 const restyleCount = await observeStyling(5); 2120 2121 is(restyleCount, 0, 'background-position animations can be throttled'); 2122 await ensureElementRemoval(div); 2123 }); 2124 2125 add_task_if_omta_enabled(async function no_restyling_animations_in_opacity_zero_element() { 2126 const div = addDiv(null, { style: 'animation: on-main-thread 100s infinite; opacity: 0' }); 2127 const animation = div.getAnimations()[0]; 2128 2129 await waitForAnimationReadyToRestyle(animation); 2130 const restyleCount = await observeStyling(5); 2131 is(restyleCount, 0, 2132 'Animations running on the main thread in opacity: 0 element ' + 2133 'should never cause restyles'); 2134 await ensureElementRemoval(div); 2135 }); 2136 2137 add_task_if_omta_enabled(async function no_restyling_compositor_animations_in_opacity_zero_descendant() { 2138 const container = addDiv(null, { style: 'opacity: 0' }); 2139 const child = addDiv(null, { style: 'animation: background-color 100s infinite;' }); 2140 container.appendChild(child); 2141 2142 const animation = child.getAnimations()[0]; 2143 await waitForAnimationReadyToRestyle(animation); 2144 ok(!SpecialPowers.wrap(animation).isRunningOnCompositor); 2145 2146 const restyleCount = await observeStyling(5); 2147 2148 is(restyleCount, 0, 2149 'Animations running on the compositor in opacity zero descendant element ' + 2150 'should never cause restyles'); 2151 await ensureElementRemoval(container); 2152 }); 2153 2154 add_task_if_omta_enabled(async function no_restyling_compositor_animations_in_opacity_zero_descendant_abspos() { 2155 const container = addDiv(null, { style: 'opacity: 0' }); 2156 const child = addDiv(null, { style: 'position: absolute; animation: background-color 100s infinite;' }); 2157 container.appendChild(child); 2158 2159 const animation = child.getAnimations()[0]; 2160 await waitForAnimationReadyToRestyle(animation); 2161 ok(!SpecialPowers.wrap(animation).isRunningOnCompositor); 2162 2163 const restyleCount = await observeStyling(5); 2164 2165 is(restyleCount, 0, 2166 'Animations running on the compositor in opacity zero abspos descendant element ' + 2167 'should never cause restyles'); 2168 await ensureElementRemoval(container); 2169 }); 2170 2171 add_task_if_omta_enabled(async function no_restyling_compositor_animations_in_opacity_zero_element() { 2172 const child = addDiv(null, { style: 'animation: background-color 100s infinite; opacity: 0' }); 2173 2174 const animation = child.getAnimations()[0]; 2175 await waitForAnimationReadyToRestyle(animation); 2176 ok(!SpecialPowers.wrap(animation).isRunningOnCompositor); 2177 2178 const restyleCount = await observeStyling(5); 2179 2180 is(restyleCount, 0, 2181 'Animations running on the compositor in opacity zero element ' + 2182 'should never cause restyles'); 2183 await ensureElementRemoval(child); 2184 }); 2185 2186 add_task_if_omta_enabled(async function restyling_main_thread_animations_in_opacity_zero_descendant_after_root_opacity_animation() { 2187 const container = addDiv(null, { style: 'opacity: 0' }); 2188 2189 const child = addDiv(null, { style: 'animation: on-main-thread 100s infinite;' }); 2190 container.appendChild(child); 2191 2192 // Animate the container from 1 to zero opacity and ensure the child animation is throttled then. 2193 const containerAnimation = container.animate({ opacity: [ '1', '0' ] }, 100); 2194 await containerAnimation.finished; 2195 2196 const animation = child.getAnimations()[0]; 2197 await waitForAnimationReadyToRestyle(animation); 2198 2199 const restyleCount = await observeStyling(5); 2200 2201 is(restyleCount, 0, 2202 'Animations running on the compositor in opacity zero descendant element ' + 2203 'should never cause restyles after root animation has finished'); 2204 await ensureElementRemoval(container); 2205 }); 2206 2207 add_task_if_omta_enabled(async function restyling_main_thread_animations_in_opacity_zero_descendant_during_root_opacity_animation() { 2208 const container = addDiv(null, { style: 'opacity: 0; animation: opacity-from-zero 100s infinite' }); 2209 2210 const child = addDiv(null, { style: 'animation: on-main-thread 100s infinite;' }); 2211 container.appendChild(child); 2212 2213 const animation = child.getAnimations()[0]; 2214 await waitForAnimationReadyToRestyle(animation); 2215 2216 const restyleCount = await observeStyling(5); 2217 2218 is(restyleCount, 5, 2219 'Animations in opacity zero descendant element ' + 2220 'should not be throttled if root is animating opacity'); 2221 await ensureElementRemoval(container); 2222 }); 2223 2224 add_task_if_omta_enabled(async function restyling_omt_animations_in_opacity_zero_descendant_during_root_opacity_animation() { 2225 const container = addDiv(null, { style: 'opacity: 0; animation: opacity-from-zero 100s infinite' }); 2226 2227 const child = addDiv(null, { style: 'animation: rotate 100s infinite' }); 2228 container.appendChild(child); 2229 2230 const animation = child.getAnimations()[0]; 2231 await waitForAnimationReadyToRestyle(animation); 2232 ok(SpecialPowers.wrap(animation).isRunningOnCompositor); 2233 2234 await ensureElementRemoval(container); 2235 }); 2236 2237 add_task_if_omta_enabled(async function transparent_background_color_animations() { 2238 const div = addDiv(null); 2239 const animation = 2240 div.animate({ backgroundColor: [ 'rgb(0, 200, 0, 0)', 2241 'rgb(200, 0, 0, 0.1)' ] }, 2242 { duration: 100 * MS_PER_SEC, 2243 // An easing function producing zero in the first half of 2244 // the duration. 2245 easing: 'cubic-bezier(1, 0, 1, 0)' }); 2246 await waitForAnimationReadyToRestyle(animation); 2247 2248 ok(SpecialPowers.wrap(animation).isRunningOnCompositor); 2249 2250 const restyleCount = await observeStyling(5); 2251 is(restyleCount, 0, 2252 'transparent background-color animation should not update styles on ' + 2253 'the main thread'); 2254 2255 await ensureElementRemoval(div); 2256 }); 2257 2258 add_task_if_omta_enabled(async function transform_animation_on_collapsed_element() { 2259 const iframe = document.createElement('iframe'); 2260 document.body.appendChild(iframe); 2261 2262 // Load a cross origin iframe. 2263 const targetURL = SimpleTest.getTestFileURL("empty.html") 2264 .replace(window.location.origin, "http://example.com/"); 2265 iframe.src = targetURL; 2266 await new Promise(resolve => { 2267 iframe.onload = resolve; 2268 }); 2269 2270 await SpecialPowers.spawn(iframe, [MS_PER_SEC], async (MS_PER_SEC) => { 2271 // Create a flex item with "preserve-3d" having an abs-pos child inside 2272 // a grid container. 2273 // These styles make the flex item size (0x0). 2274 const gridContainer = content.document.createElement("div"); 2275 gridContainer.style.display = "grid"; 2276 gridContainer.style.placeItems = "center"; 2277 2278 const target = content.document.createElement("div"); 2279 target.style.display = "flex"; 2280 target.style.transformStyle = "preserve-3d"; 2281 gridContainer.appendChild(target); 2282 2283 const child = content.document.createElement("div"); 2284 child.style.position = "absolute"; 2285 child.style.transform = "rotateY(0deg)"; 2286 child.style.width = "100px"; 2287 child.style.height = "100px"; 2288 child.style.backgroundColor = "green"; 2289 target.appendChild(child); 2290 2291 content.document.body.appendChild(gridContainer); 2292 2293 const animation = 2294 target.animate({ transform: [ "rotateY(0deg)", "rotateY(360deg)" ] }, 2295 { duration: 100 * MS_PER_SEC, 2296 id: "test", 2297 easing: 'step-end' }); 2298 await content.wrappedJSObject.waitForAnimationReadyToRestyle(animation); 2299 ok(SpecialPowers.wrap(animation).isRunningOnCompositor, 2300 'transform animation on a collapsed element should run on the ' + 2301 'compositor'); 2302 2303 const restyleCount = await content.wrappedJSObject.observeStyling(5); 2304 is(restyleCount, 0, 2305 'transform animation on a collapsed element animation should not ' + 2306 'update styles on the main thread'); 2307 }); 2308 2309 await ensureElementRemoval(iframe); 2310 }); 2311 }); 2312 2313 </script> 2314 </body>