tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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>