tor-browser

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

test_animation_observers_async.html (23808B)


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