tor-browser

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

test_animation_observers_sync.html (60674B)


      1 <!DOCTYPE html>
      2 <meta charset=utf-8>
      3 <title>
      4 Test chrome-only MutationObserver animation notifications (sync tests)
      5 </title>
      6 <!--
      7 
      8  This file contains synchronous tests for animation mutation observers.
      9 
     10  In general we prefer to write synchronous tests since they are less likely to
     11  timeout when run on automation. Tests that require asynchronous steps (e.g.
     12  waiting on events) should be added to test_animations_observers_async.html
     13  instead.
     14 
     15 -->
     16 <script type="application/javascript" src="../testharness.js"></script>
     17 <script type="application/javascript" src="../testharnessreport.js"></script>
     18 <script type="application/javascript" 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 </style>
     28 <script>
     29 
     30 /**
     31 * Return a new MutationObserver which observing |target| element
     32 * with { animations: true, subtree: |subtree| } option.
     33 *
     34 * NOTE: This observer should be used only with takeRecords(). If any of
     35 * MutationRecords are observed in the callback of the MutationObserver,
     36 * it will raise an assertion.
     37 */
     38 function setupSynchronousObserver(t, target, subtree) {
     39  var observer = new MutationObserver(records => {
     40    assert_unreached("Any MutationRecords should not be observed in this " +
     41                     "callback");
     42  });
     43  t.add_cleanup(() => {
     44    observer.disconnect();
     45  });
     46  observer.observe(target, { animations: true, subtree });
     47  return observer;
     48 }
     49 
     50 function assert_record_list(actual, expected, desc, index, listName) {
     51  assert_equals(actual.length, expected.length,
     52                `${desc} - record[${index}].${listName} length`);
     53  if (actual.length != expected.length) {
     54    return;
     55  }
     56  for (var i = 0; i < actual.length; i++) {
     57    assert_not_equals(actual.indexOf(expected[i]), -1,
     58       `${desc} - record[${index}].${listName} contains expected Animation`);
     59  }
     60 }
     61 
     62 function assert_equals_records(actual, expected, desc) {
     63  assert_equals(actual.length, expected.length, `${desc} - number of records`);
     64  if (actual.length != expected.length) {
     65    return;
     66  }
     67  for (var i = 0; i < actual.length; i++) {
     68    assert_record_list(actual[i].addedAnimations,
     69                       expected[i].added, desc, i, "addedAnimations");
     70    assert_record_list(actual[i].changedAnimations,
     71                       expected[i].changed, desc, i, "changedAnimations");
     72    assert_record_list(actual[i].removedAnimations,
     73                       expected[i].removed, desc, i, "removedAnimations");
     74  }
     75 }
     76 
     77 function runTest() {
     78  [ { subtree: false },
     79    { subtree: true }
     80  ].forEach(aOptions => {
     81    test(t => {
     82      var div = addDiv(t);
     83      var observer =
     84        setupSynchronousObserver(t,
     85                                 aOptions.subtree ? div.parentNode : div,
     86                                 aOptions.subtree);
     87 
     88      var anim = div.animate({ opacity: [ 0, 1 ] }, 200 * MS_PER_SEC);
     89 
     90      assert_equals_records(observer.takeRecords(),
     91        [{ added: [anim], changed: [], removed: [] }],
     92        "records after animation is added");
     93 
     94      anim.effect.updateTiming({ duration: 100 * MS_PER_SEC });
     95      assert_equals_records(observer.takeRecords(),
     96        [{ added: [], changed: [anim], removed: [] }],
     97        "records after duration is changed");
     98 
     99      anim.effect.updateTiming({ duration: 100 * MS_PER_SEC });
    100      assert_equals_records(observer.takeRecords(),
    101        [], "records after assigning same value");
    102 
    103      anim.currentTime = anim.effect.getComputedTiming().duration * 2;
    104      anim.finish();
    105      assert_equals_records(observer.takeRecords(),
    106        [{ added: [], changed: [], removed: [anim] }],
    107        "records after animation end");
    108 
    109      anim.effect.updateTiming({
    110        duration: anim.effect.getComputedTiming().duration * 3
    111      });
    112      assert_equals_records(observer.takeRecords(),
    113        [{ added: [anim], changed: [], removed: [] }],
    114        "records after animation restarted");
    115 
    116      anim.effect.updateTiming({ duration: 'auto' });
    117      assert_equals_records(observer.takeRecords(),
    118        [{ added: [], changed: [], removed: [anim] }],
    119        "records after duration set \"auto\"");
    120 
    121      anim.effect.updateTiming({ duration: 'auto' });
    122      assert_equals_records(observer.takeRecords(),
    123        [], "records after assigning same value \"auto\"");
    124    }, "change_duration_and_currenttime");
    125 
    126    test(t => {
    127      var div = addDiv(t);
    128      var observer =
    129        setupSynchronousObserver(t,
    130                                 aOptions.subtree ? div.parentNode : div,
    131                                 aOptions.subtree);
    132 
    133      var anim = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
    134 
    135      assert_equals_records(observer.takeRecords(),
    136        [{ added: [anim], changed: [], removed: [] }],
    137        "records after animation is added");
    138 
    139      anim.effect.updateTiming({ endDelay: 10 * MS_PER_SEC });
    140      assert_equals_records(observer.takeRecords(),
    141        [{ added: [], changed: [anim], removed: [] }],
    142        "records after endDelay is changed");
    143 
    144      anim.effect.updateTiming({ endDelay: 10 * MS_PER_SEC });
    145      assert_equals_records(observer.takeRecords(),
    146        [], "records after assigning same value");
    147 
    148      anim.currentTime = 109 * MS_PER_SEC;
    149      assert_equals_records(observer.takeRecords(),
    150        [{ added: [], changed: [], removed: [anim] }],
    151        "records after currentTime during endDelay");
    152 
    153      anim.effect.updateTiming({ endDelay: -110 * MS_PER_SEC });
    154      assert_equals_records(observer.takeRecords(),
    155        [], "records after assigning negative value");
    156    }, "change_enddelay_and_currenttime");
    157 
    158    test(t => {
    159      var div = addDiv(t);
    160      var observer =
    161        setupSynchronousObserver(t,
    162                                 aOptions.subtree ? div.parentNode : div,
    163                                 aOptions.subtree);
    164 
    165      var anim = div.animate({ opacity: [ 0, 1 ] },
    166                             { duration: 100 * MS_PER_SEC,
    167                               endDelay: -100 * MS_PER_SEC });
    168      assert_equals_records(observer.takeRecords(),
    169        [], "records after animation is added");
    170    }, "zero_end_time");
    171 
    172    test(t => {
    173      var div = addDiv(t);
    174      var observer =
    175        setupSynchronousObserver(t,
    176                                 aOptions.subtree ? div.parentNode : div,
    177                                 aOptions.subtree);
    178 
    179      var anim = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
    180 
    181      assert_equals_records(observer.takeRecords(),
    182        [{ added: [anim], changed: [], removed: [] }],
    183        "records after animation is added");
    184 
    185      anim.effect.updateTiming({ iterations: 2 });
    186      assert_equals_records(observer.takeRecords(),
    187        [{ added: [], changed: [anim], removed: [] }],
    188        "records after iterations is changed");
    189 
    190      anim.effect.updateTiming({ iterations: 2 });
    191      assert_equals_records(observer.takeRecords(),
    192        [], "records after assigning same value");
    193 
    194      anim.effect.updateTiming({ iterations: 0 });
    195      assert_equals_records(observer.takeRecords(),
    196        [{ added: [], changed: [], removed: [anim] }],
    197        "records after animation end");
    198 
    199      anim.effect.updateTiming({ iterations: Infinity });
    200      assert_equals_records(observer.takeRecords(),
    201        [{ added: [anim], changed: [], removed: [] }],
    202        "records after animation restarted");
    203    }, "change_iterations");
    204 
    205    test(t => {
    206      var div = addDiv(t);
    207      var observer =
    208        setupSynchronousObserver(t,
    209                                 aOptions.subtree ? div.parentNode : div,
    210                                 aOptions.subtree);
    211 
    212      var anim = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
    213 
    214      assert_equals_records(observer.takeRecords(),
    215        [{ added: [anim], changed: [], removed: [] }],
    216        "records after animation is added");
    217 
    218      anim.effect.updateTiming({ delay: 100 });
    219      assert_equals_records(observer.takeRecords(),
    220        [{ added: [], changed: [anim], removed: [] }],
    221        "records after delay is changed");
    222 
    223      anim.effect.updateTiming({ delay: 100 });
    224      assert_equals_records(observer.takeRecords(),
    225        [], "records after assigning same value");
    226 
    227      anim.effect.updateTiming({ delay: -100 * MS_PER_SEC });
    228      assert_equals_records(observer.takeRecords(),
    229        [{ added: [], changed: [], removed: [anim] }],
    230        "records after animation end");
    231 
    232      anim.effect.updateTiming({ delay: 0 });
    233      assert_equals_records(observer.takeRecords(),
    234        [{ added: [anim], changed: [], removed: [] }],
    235        "records after animation restarted");
    236    }, "change_delay");
    237 
    238    test(t => {
    239      var div = addDiv(t);
    240      var observer =
    241        setupSynchronousObserver(t,
    242                                 aOptions.subtree ? div.parentNode : div,
    243                                 aOptions.subtree);
    244 
    245      var anim = div.animate({ opacity: [ 0, 1 ] },
    246                             { duration: 100 * MS_PER_SEC,
    247                               easing: "steps(2, start)" });
    248 
    249      assert_equals_records(observer.takeRecords(),
    250        [{ added: [anim], changed: [], removed: [] }],
    251        "records after animation is added");
    252 
    253      anim.effect.updateTiming({ easing: "steps(2, end)" });
    254      assert_equals_records(observer.takeRecords(),
    255        [{ added: [], changed: [anim], removed: [] }],
    256        "records after easing is changed");
    257 
    258      anim.effect.updateTiming({ easing: "steps(2, end)" });
    259      assert_equals_records(observer.takeRecords(),
    260        [], "records after assigning same value");
    261    }, "change_easing");
    262 
    263    test(t => {
    264      var div = addDiv(t);
    265      var observer =
    266        setupSynchronousObserver(t,
    267                                 aOptions.subtree ? div.parentNode : div,
    268                                 aOptions.subtree);
    269 
    270      var anim = div.animate({ opacity: [ 0, 1 ] },
    271                             { duration: 100, delay: -100 });
    272      assert_equals_records(observer.takeRecords(),
    273        [], "records after assigning negative value");
    274    }, "negative_delay_in_constructor");
    275 
    276    test(t => {
    277      var div = addDiv(t);
    278      var observer =
    279        setupSynchronousObserver(t,
    280                                 aOptions.subtree ? div.parentNode : div,
    281                                 aOptions.subtree);
    282 
    283      var effect = new KeyframeEffect(null,
    284                                      { opacity: [ 0, 1 ] },
    285                                      { duration: 100 * MS_PER_SEC });
    286      var anim = new Animation(effect, document.timeline);
    287      anim.play();
    288      assert_equals_records(observer.takeRecords(),
    289        [], "no records after animation is added");
    290    }, "create_animation_without_target");
    291 
    292    test(t => {
    293      var div = addDiv(t);
    294      var observer =
    295        setupSynchronousObserver(t,
    296                                 aOptions.subtree ? div.parentNode : div,
    297                                 aOptions.subtree);
    298 
    299      var anim = div.animate({ opacity: [ 0, 1 ] },
    300                             { duration: 100 * MS_PER_SEC });
    301      assert_equals_records(observer.takeRecords(),
    302        [{ added: [anim], changed: [], removed: [] }],
    303        "records after animation is added");
    304 
    305      anim.effect.target = div;
    306      assert_equals_records(observer.takeRecords(),
    307        [], "no records after setting the same target");
    308 
    309      anim.effect.target = null;
    310      assert_equals_records(observer.takeRecords(),
    311        [{ added: [], changed: [], removed: [anim] }],
    312        "records after setting null");
    313 
    314      anim.effect.target = null;
    315      assert_equals_records(observer.takeRecords(),
    316        [], "records after setting redundant null");
    317    }, "set_redundant_animation_target");
    318 
    319    test(t => {
    320      var div = addDiv(t);
    321      var observer =
    322        setupSynchronousObserver(t,
    323                                 aOptions.subtree ? div.parentNode : div,
    324                                 aOptions.subtree);
    325 
    326      var anim = div.animate({ opacity: [ 0, 1 ] },
    327                             { duration: 100 * MS_PER_SEC });
    328      assert_equals_records(observer.takeRecords(),
    329        [{ added: [anim], changed: [], removed: [] }],
    330        "records after animation is added");
    331 
    332      anim.effect = null;
    333      assert_equals_records(observer.takeRecords(),
    334        [{ added: [], changed: [], removed: [anim] }],
    335        "records after animation is removed");
    336    }, "set_null_animation_effect");
    337 
    338    test(t => {
    339      var div = addDiv(t);
    340      var observer =
    341        setupSynchronousObserver(t,
    342                                 aOptions.subtree ? div.parentNode : div,
    343                                 aOptions.subtree);
    344 
    345      var anim = new Animation();
    346      anim.play();
    347      anim.effect = new KeyframeEffect(div, { opacity: [ 0, 1 ] },
    348                                       100 * MS_PER_SEC);
    349      assert_equals_records(observer.takeRecords(),
    350        [{ added: [anim], changed: [], removed: [] }],
    351        "records after animation is added");
    352    }, "set_effect_on_null_effect_animation");
    353 
    354    test(t => {
    355      var div = addDiv(t);
    356      var observer =
    357        setupSynchronousObserver(t,
    358                                 aOptions.subtree ? div.parentNode : div,
    359                                 aOptions.subtree);
    360 
    361      var anim = div.animate({ marginLeft: [ "0px", "100px" ] },
    362                             100 * MS_PER_SEC);
    363      assert_equals_records(observer.takeRecords(),
    364        [{ added: [anim], changed: [], removed: [] }],
    365        "records after animation is added");
    366 
    367      anim.effect = new KeyframeEffect(div, { opacity: [ 0, 1 ] },
    368                                       100 * MS_PER_SEC);
    369      assert_equals_records(observer.takeRecords(),
    370        [{ added: [], changed: [anim], removed: [] }],
    371        "records after replace effects");
    372    }, "replace_effect_targeting_on_the_same_element");
    373 
    374    test(t => {
    375      var div = addDiv(t);
    376      var observer =
    377        setupSynchronousObserver(t,
    378                                 aOptions.subtree ? div.parentNode : div,
    379                                 aOptions.subtree);
    380 
    381      var anim = div.animate({ marginLeft: [ "0px", "100px" ] },
    382                             100 * MS_PER_SEC);
    383      assert_equals_records(observer.takeRecords(),
    384        [{ added: [anim], changed: [], removed: [] }],
    385        "records after animation is added");
    386 
    387      anim.currentTime = 60 * MS_PER_SEC;
    388      assert_equals_records(observer.takeRecords(),
    389        [{ added: [], changed: [anim], removed: [] }],
    390        "records after animation is changed");
    391 
    392      anim.effect = new KeyframeEffect(div, { opacity: [ 0, 1 ] },
    393                                       50 * MS_PER_SEC);
    394      assert_equals_records(observer.takeRecords(),
    395        [{ added: [], changed: [], removed: [anim] }],
    396        "records after replacing effects");
    397    }, "replace_effect_targeting_on_the_same_element_not_in_effect");
    398 
    399    test(t => {
    400      var div = addDiv(t);
    401      var observer =
    402        setupSynchronousObserver(t,
    403                                 aOptions.subtree ? div.parentNode : div,
    404                                 aOptions.subtree);
    405 
    406      var anim = div.animate({ }, 100 * MS_PER_SEC);
    407      assert_equals_records(observer.takeRecords(),
    408        [{ added: [anim], changed: [], removed: [] }],
    409        "records after animation is added");
    410 
    411      anim.effect.composite = "add";
    412      assert_equals_records(observer.takeRecords(),
    413        [{ added: [], changed: [anim], removed: [] }],
    414        "records after composite is changed");
    415 
    416      anim.effect.composite = "add";
    417      assert_equals_records(observer.takeRecords(),
    418        [], "no record after setting the same composite");
    419 
    420    }, "set_composite");
    421 
    422    // Test that starting a single animation that is cancelled by calling
    423    // cancel() dispatches an added notification and then a removed
    424    // notification.
    425    test(t => {
    426      var div = addDiv(t, { style: "animation: anim 100s forwards" });
    427      var observer =
    428        setupSynchronousObserver(t,
    429                                 aOptions.subtree ? div.parentNode : div,
    430                                 aOptions.subtree);
    431 
    432      var animations = div.getAnimations();
    433      assert_equals(animations.length, 1,
    434        "getAnimations().length after animation start");
    435 
    436      assert_equals_records(observer.takeRecords(),
    437        [{ added: animations, changed: [], removed: [] }],
    438        "records after animation start");
    439 
    440      animations[0].cancel();
    441 
    442      assert_equals_records(observer.takeRecords(),
    443        [{ added: [], changed: [], removed: animations }],
    444        "records after animation end");
    445 
    446      // Re-trigger the animation.
    447      animations[0].play();
    448 
    449      // Single MutationRecord for the Animation (re-)addition.
    450      assert_equals_records(observer.takeRecords(),
    451        [{ added: animations, changed: [], removed: [] }],
    452        "records after animation start");
    453    }, "single_animation_cancelled_api");
    454 
    455    // Test that updating a property on the Animation object dispatches a changed
    456    // notification.
    457    [
    458      { prop: "playbackRate", val: 0.5 },
    459      { prop: "startTime",    val: 50 * MS_PER_SEC },
    460      { prop: "currentTime",  val: 50 * MS_PER_SEC },
    461    ].forEach(aChangeTest => {
    462      test(t => {
    463        // We use a forwards fill mode so that even if the change we make causes
    464        // the animation to become finished, it will still be "relevant" so we
    465        // won't mark it as removed.
    466        var div = addDiv(t, { style: "animation: anim 100s forwards" });
    467        var observer =
    468          setupSynchronousObserver(t,
    469                                   aOptions.subtree ? div.parentNode : div,
    470                                   aOptions.subtree);
    471 
    472        var animations = div.getAnimations();
    473        assert_equals(animations.length, 1,
    474          "getAnimations().length after animation start");
    475 
    476        assert_equals_records(observer.takeRecords(),
    477          [{ added: animations, changed: [], removed: [] }],
    478          "records after animation start");
    479 
    480        // Update the property.
    481        animations[0][aChangeTest.prop] = aChangeTest.val;
    482 
    483        // Make a redundant change.
    484        // eslint-disable-next-line no-self-assign
    485        animations[0][aChangeTest.prop] = animations[0][aChangeTest.prop];
    486 
    487        assert_equals_records(observer.takeRecords(),
    488          [{ added: [], changed: animations, removed: [] }],
    489          "records after animation property change");
    490      }, `single_animation_api_change_${aChangeTest.prop}`);
    491    });
    492 
    493    // Test that making a redundant change to currentTime while an Animation
    494    // is pause-pending still generates a change MutationRecord since setting
    495    // the currentTime to any value in this state aborts the pending pause.
    496    test(t => {
    497      var div = addDiv(t, { style: "animation: anim 100s" });
    498      var observer =
    499        setupSynchronousObserver(t,
    500                                 aOptions.subtree ? div.parentNode : div,
    501                                 aOptions.subtree);
    502 
    503      var animations = div.getAnimations();
    504      assert_equals(animations.length, 1,
    505        "getAnimations().length after animation start");
    506 
    507      assert_equals_records(observer.takeRecords(),
    508        [{ added: animations, changed: [], removed: [] }],
    509        "records after animation start");
    510 
    511      animations[0].pause();
    512 
    513      // We are now pause-pending. Even if we make a redundant change to the
    514      // currentTime, we should still get a change record because setting the
    515      // currentTime while pause-pending has the effect of cancelling a pause.
    516      // eslint-disable-next-line no-self-assign
    517      animations[0].currentTime = animations[0].currentTime;
    518 
    519      // Two MutationRecords for the Animation changes: one for pausing, one
    520      // for aborting the pause.
    521      assert_equals_records(observer.takeRecords(),
    522        [{ added: [], changed: animations, removed: [] },
    523         { added: [], changed: animations, removed: [] }],
    524        "records after pausing then seeking");
    525    }, "change_currentTime_while_pause_pending");
    526 
    527    // Test that calling finish() on a forwards-filling Animation dispatches
    528    // a changed notification.
    529    test(t => {
    530      var div = addDiv(t, { style: "animation: anim 100s forwards" });
    531      var observer =
    532        setupSynchronousObserver(t,
    533                                 aOptions.subtree ? div.parentNode : div,
    534                                 aOptions.subtree);
    535 
    536      var animations = div.getAnimations();
    537      assert_equals(animations.length, 1,
    538        "getAnimations().length after animation start");
    539 
    540      assert_equals_records(observer.takeRecords(),
    541        [{ added: animations, changed: [], removed: [] }],
    542        "records after animation start");
    543 
    544      animations[0].finish();
    545 
    546      assert_equals_records(observer.takeRecords(),
    547        [{ added: [], changed: animations, removed: [] }],
    548        "records after finish()");
    549 
    550      // Redundant finish.
    551      animations[0].finish();
    552 
    553      // Ensure no change records.
    554      assert_equals_records(observer.takeRecords(),
    555        [], "records after redundant finish()");
    556    }, "finish_with_forwards_fill");
    557 
    558    // Test that calling finish() on an Animation that does not fill forwards,
    559    // dispatches a removal notification.
    560    test(t => {
    561      var div = addDiv(t, { style: "animation: anim 100s" });
    562      var observer =
    563        setupSynchronousObserver(t,
    564                                 aOptions.subtree ? div.parentNode : div,
    565                                 aOptions.subtree);
    566 
    567      var animations = div.getAnimations();
    568      assert_equals(animations.length, 1,
    569        "getAnimations().length after animation start");
    570 
    571      assert_equals_records(observer.takeRecords(),
    572        [{ added: animations, changed: [], removed: [] }],
    573        "records after animation start");
    574 
    575      animations[0].finish();
    576 
    577      // Single MutationRecord for the Animation removal.
    578      assert_equals_records(observer.takeRecords(),
    579        [{ added: [], changed: [], removed: animations }],
    580        "records after finishing");
    581    }, "finish_without_fill");
    582 
    583    // Test that calling finish() on a forwards-filling Animation dispatches
    584    test(t => {
    585      var div = addDiv(t, { style: "animation: anim 100s" });
    586      var observer =
    587        setupSynchronousObserver(t,
    588                                 aOptions.subtree ? div.parentNode : div,
    589                                 aOptions.subtree);
    590 
    591      var animation = div.getAnimations()[0];
    592      assert_equals_records(observer.takeRecords(),
    593        [{ added: [animation], changed: [], removed: []}],
    594        "records after creation");
    595      animation.id = "new id";
    596      assert_equals_records(observer.takeRecords(),
    597        [{ added: [], changed: [animation], removed: []}],
    598        "records after id is changed");
    599 
    600      animation.id = "new id";
    601      assert_equals_records(observer.takeRecords(),
    602        [], "records after assigning same value with id");
    603    }, "change_id");
    604 
    605    // Test that calling reverse() dispatches a changed notification.
    606    test(t => {
    607      var div = addDiv(t, { style: "animation: anim 100s both" });
    608      var observer =
    609        setupSynchronousObserver(t,
    610                                 aOptions.subtree ? div.parentNode : div,
    611                                 aOptions.subtree);
    612 
    613      var animations = div.getAnimations();
    614      assert_equals(animations.length, 1,
    615        "getAnimations().length after animation start");
    616 
    617      assert_equals_records(observer.takeRecords(),
    618        [{ added: animations, changed: [], removed: [] }],
    619        "records after animation start");
    620 
    621      animations[0].reverse();
    622 
    623      assert_equals_records(observer.takeRecords(),
    624        [{ added: [], changed: animations, removed: [] }],
    625        "records after calling reverse()");
    626    }, "reverse");
    627 
    628    // Test that calling reverse() does *not* dispatch a changed notification
    629    // when playbackRate == 0.
    630    test(t => {
    631      var div = addDiv(t, { style: "animation: anim 100s both" });
    632      var observer =
    633        setupSynchronousObserver(t,
    634                                 aOptions.subtree ? div.parentNode : div,
    635                                 aOptions.subtree);
    636 
    637      var animations = div.getAnimations();
    638      assert_equals(animations.length, 1,
    639        "getAnimations().length after animation start");
    640 
    641      assert_equals_records(observer.takeRecords(),
    642        [{ added: animations, changed: [], removed: [] }],
    643        "records after animation start");
    644 
    645      // Seek to the middle and set playbackRate to zero.
    646      animations[0].currentTime = 50 * MS_PER_SEC;
    647      animations[0].playbackRate = 0;
    648 
    649      // Two MutationRecords, one for each change.
    650      assert_equals_records(observer.takeRecords(),
    651        [{ added: [], changed: animations, removed: [] },
    652         { added: [], changed: animations, removed: [] }],
    653        "records after seeking and setting playbackRate");
    654 
    655      animations[0].reverse();
    656 
    657      // We should get no notifications.
    658      assert_equals_records(observer.takeRecords(),
    659        [], "records after calling reverse()");
    660    }, "reverse_with_zero_playbackRate");
    661 
    662    // Test that reverse() on an Animation does *not* dispatch a changed
    663    // notification when it throws an exception.
    664    test(t => {
    665      // Start an infinite animation
    666      var div = addDiv(t, { style: "animation: anim 10s infinite" });
    667      var observer =
    668        setupSynchronousObserver(t,
    669                                 aOptions.subtree ? div.parentNode : div,
    670                                 aOptions.subtree);
    671 
    672      var animations = div.getAnimations();
    673      assert_equals(animations.length, 1,
    674        "getAnimations().length after animation start");
    675 
    676      assert_equals_records(observer.takeRecords(),
    677        [{ added: animations, changed: [], removed: [] }],
    678        "records after animation start");
    679 
    680      // Shift the animation into the future such that when we call reverse
    681      // it will try to seek to the (infinite) end.
    682      animations[0].startTime = 100 * MS_PER_SEC;
    683 
    684      assert_equals_records(observer.takeRecords(),
    685        [{ added: [], changed: animations, removed: [] }],
    686        "records after adjusting startTime");
    687 
    688      // Reverse: should throw
    689      assert_throws('InvalidStateError', () => {
    690        animations[0].reverse();
    691      }, 'reverse() on future infinite animation throws an exception');
    692 
    693      // We should get no notifications.
    694      assert_equals_records(observer.takeRecords(),
    695        [], "records after calling reverse()");
    696    }, "reverse_with_exception");
    697 
    698    // Test that attempting to start an animation that should already be finished
    699    // does not send any notifications.
    700    test(t => {
    701      // Start an animation that should already be finished.
    702      var div = addDiv(t, { style: "animation: anim 1s -2s;" });
    703      var observer =
    704        setupSynchronousObserver(t,
    705                                 aOptions.subtree ? div.parentNode : div,
    706                                 aOptions.subtree);
    707 
    708      // The animation should cause no Animations to be created.
    709      var animations = div.getAnimations();
    710      assert_equals(animations.length, 0,
    711        "getAnimations().length after animation start");
    712 
    713      // And we should get no notifications.
    714      assert_equals_records(observer.takeRecords(),
    715        [], "records after attempted animation start");
    716    }, "already_finished");
    717 
    718    test(t => {
    719      var div = addDiv(t, { style: "animation: anim 100s, anotherAnim 100s" });
    720      var observer =
    721        setupSynchronousObserver(t,
    722                                 aOptions.subtree ? div.parentNode : div,
    723                                 aOptions.subtree);
    724 
    725      var animations = div.getAnimations();
    726 
    727      assert_equals_records(observer.takeRecords(),
    728        [{ added: animations, changed: [], removed: []}],
    729        "records after creation");
    730 
    731      div.style.animation = "anotherAnim 100s, anim 100s";
    732      animations = div.getAnimations();
    733 
    734      assert_equals_records(observer.takeRecords(),
    735        [{ added: [], changed: animations, removed: []}],
    736        "records after the order is changed");
    737 
    738      div.style.animation = "anotherAnim 100s, anim 100s";
    739 
    740      assert_equals_records(observer.takeRecords(),
    741        [], "no records after applying the same order");
    742    }, "animation_order_change");
    743 
    744    test(t => {
    745      var div = addDiv(t);
    746      var observer =
    747        setupSynchronousObserver(t,
    748                                 aOptions.subtree ? div.parentNode : div,
    749                                 aOptions.subtree);
    750 
    751      var anim = div.animate({ opacity: [ 0, 1 ] },
    752                             { duration: 100 * MS_PER_SEC,
    753                               iterationComposite: 'replace' });
    754 
    755      assert_equals_records(observer.takeRecords(),
    756        [{ added: [anim], changed: [], removed: [] }],
    757        "records after animation is added");
    758 
    759      anim.effect.iterationComposite = 'accumulate';
    760      assert_equals_records(observer.takeRecords(),
    761        [{ added: [], changed: [anim], removed: [] }],
    762        "records after iterationComposite is changed");
    763 
    764      anim.effect.iterationComposite = 'accumulate';
    765      assert_equals_records(observer.takeRecords(),
    766        [], "no record after setting the same iterationComposite");
    767 
    768    }, "set_iterationComposite");
    769 
    770    test(t => {
    771      var div = addDiv(t);
    772      var observer =
    773        setupSynchronousObserver(t,
    774                                 aOptions.subtree ? div.parentNode : div,
    775                                 aOptions.subtree);
    776 
    777      var anim = div.animate({ opacity: [ 0, 1 ] },
    778                             { duration: 100 * MS_PER_SEC });
    779 
    780      assert_equals_records(observer.takeRecords(),
    781        [{ added: [anim], changed: [], removed: [] }],
    782        "records after animation is added");
    783 
    784      anim.effect.setKeyframes({ opacity: 0.1 });
    785      assert_equals_records(observer.takeRecords(),
    786        [{ added: [], changed: [anim], removed: [] }],
    787        "records after keyframes are changed");
    788 
    789      anim.effect.setKeyframes({ opacity: 0.1 });
    790      assert_equals_records(observer.takeRecords(),
    791        [], "no record after setting the same keyframes");
    792 
    793      anim.effect.setKeyframes(null);
    794      assert_equals_records(observer.takeRecords(),
    795        [{ added: [], changed: [anim], removed: [] }],
    796        "records after keyframes are set to empty");
    797 
    798    }, "set_keyframes");
    799 
    800    // Test that starting a single transition that is cancelled by resetting
    801    // the transition-property property dispatches an added notification and
    802    // then a removed notification.
    803    test(t => {
    804      var div =
    805        addDiv(t, { style: "transition: background-color 100s; " +
    806                           "background-color: yellow;" });
    807      var observer =
    808        setupSynchronousObserver(t,
    809                                 aOptions.subtree ? div.parentNode : div,
    810                                 aOptions.subtree);
    811 
    812      getComputedStyle(div).transitionProperty;
    813      div.style.backgroundColor = "lime";
    814 
    815      // The transition should cause the creation of a single Animation.
    816      var animations = div.getAnimations();
    817      assert_equals(animations.length, 1,
    818        "getAnimations().length after transition start");
    819 
    820      assert_equals_records(observer.takeRecords(),
    821        [{ added: animations, changed: [], removed: [] }],
    822        "records after transition start");
    823 
    824      // Cancel the transition by setting transition-property.
    825      div.style.transitionProperty = "none";
    826      getComputedStyle(div).transitionProperty;
    827 
    828      assert_equals_records(observer.takeRecords(),
    829        [{ added: [], changed: [], removed: animations }],
    830        "records after transition end");
    831    }, "single_transition_cancelled_property");
    832 
    833    // Test that starting a single transition that is cancelled by setting
    834    // style to the currently animated value dispatches an added
    835    // notification and then a removed notification.
    836    test(t => {
    837      // A long transition with a predictable value.
    838      var div =
    839        addDiv(t, { style: "transition: z-index 100s -51s; " +
    840                           "z-index: 10;" });
    841      var observer =
    842        setupSynchronousObserver(t,
    843                                 aOptions.subtree ? div.parentNode : div,
    844                                 aOptions.subtree);
    845      getComputedStyle(div).transitionProperty;
    846      div.style.zIndex = "100";
    847 
    848      // The transition should cause the creation of a single Animation.
    849      var animations = div.getAnimations();
    850      assert_equals(animations.length, 1,
    851        "getAnimations().length after transition start");
    852 
    853      assert_equals_records(observer.takeRecords(),
    854        [{ added: animations, changed: [], removed: [] }],
    855        "records after transition start");
    856 
    857      // Cancel the transition by setting the current animation value.
    858      let value = "83";
    859      assert_equals(getComputedStyle(div).zIndex, value,
    860        "half-way transition value");
    861      div.style.zIndex = value;
    862      getComputedStyle(div).transitionProperty;
    863 
    864      assert_equals_records(observer.takeRecords(),
    865        [{ added: [], changed: [], removed: animations }],
    866        "records after transition end");
    867    }, "single_transition_cancelled_value");
    868 
    869    // Test that starting a single transition that is cancelled by setting
    870    // style to a non-interpolable value dispatches an added notification
    871    // and then a removed notification.
    872    test(t => {
    873      var div =
    874        addDiv(t, { style: "transition: line-height 100s; " +
    875                           "line-height: 16px;" });
    876      var observer =
    877        setupSynchronousObserver(t,
    878                                 aOptions.subtree ? div.parentNode : div,
    879                                 aOptions.subtree);
    880 
    881      getComputedStyle(div).transitionProperty;
    882      div.style.lineHeight = "100px";
    883 
    884      // The transition should cause the creation of a single Animation.
    885      var animations = div.getAnimations();
    886      assert_equals(animations.length, 1,
    887        "getAnimations().length after transition start");
    888 
    889      assert_equals_records(observer.takeRecords(),
    890        [{ added: animations, changed: [], removed: [] }],
    891        "records after transition start");
    892 
    893      // Cancel the transition by setting line-height to a non-interpolable value.
    894      div.style.lineHeight = "normal";
    895      getComputedStyle(div).transitionProperty;
    896 
    897      assert_equals_records(observer.takeRecords(),
    898        [{ added: [], changed: [], removed: animations }],
    899        "records after transition end");
    900    }, "single_transition_cancelled_noninterpolable");
    901 
    902    // Test that starting a single transition and then reversing it
    903    // dispatches an added notification, then a simultaneous removed and
    904    // added notification, then a removed notification once finished.
    905    test(t => {
    906      var div =
    907        addDiv(t, { style: "transition: background-color 100s step-start; " +
    908                           "background-color: yellow;" });
    909      var observer =
    910        setupSynchronousObserver(t,
    911                                 aOptions.subtree ? div.parentNode : div,
    912                                 aOptions.subtree);
    913 
    914      getComputedStyle(div).transitionProperty;
    915      div.style.backgroundColor = "lime";
    916 
    917      var animations = div.getAnimations();
    918 
    919      // The transition should cause the creation of a single Animation.
    920      assert_equals(animations.length, 1,
    921        "getAnimations().length after transition start");
    922 
    923      var firstAnimation = animations[0];
    924      assert_equals_records(observer.takeRecords(),
    925        [{ added: [firstAnimation], changed: [], removed: [] }],
    926        "records after transition start");
    927 
    928      firstAnimation.currentTime = 50 * MS_PER_SEC;
    929 
    930      // Reverse the transition by setting the background-color back to its
    931      // original value.
    932      div.style.backgroundColor = "yellow";
    933 
    934      // The reversal should cause the creation of a new Animation.
    935      animations = div.getAnimations();
    936      assert_equals(animations.length, 1,
    937        "getAnimations().length after transition reversal");
    938 
    939      var secondAnimation = animations[0];
    940 
    941      assert_true(firstAnimation != secondAnimation,
    942        "second Animation should be different from the first");
    943 
    944      assert_equals_records(observer.takeRecords(),
    945        [{ added: [], changed: [firstAnimation], removed: [] },
    946         { added: [secondAnimation], changed: [], removed: [firstAnimation] }],
    947        "records after transition reversal");
    948 
    949      // Cancel the transition.
    950      div.style.transitionProperty = "none";
    951      getComputedStyle(div).transitionProperty;
    952 
    953      assert_equals_records(observer.takeRecords(),
    954        [{ added: [], changed: [], removed: [secondAnimation] }],
    955        "records after transition end");
    956    }, "single_transition_reversed");
    957 
    958    // Test that multiple transitions starting and ending on an element
    959    // at the same time get batched up into a single MutationRecord.
    960    test(t => {
    961      var div =
    962        addDiv(t, { style: "transition-duration: 100s; " +
    963                           "transition-property: color, background-color, line-height" +
    964                           "background-color: yellow; line-height: 16px" });
    965      var observer =
    966        setupSynchronousObserver(t,
    967                                 aOptions.subtree ? div.parentNode : div,
    968                                 aOptions.subtree);
    969      getComputedStyle(div).transitionProperty;
    970 
    971      div.style.backgroundColor = "lime";
    972      div.style.color = "blue";
    973      div.style.lineHeight = "24px";
    974 
    975      // The transitions should cause the creation of three Animations.
    976      var animations = div.getAnimations();
    977      assert_equals(animations.length, 3,
    978        "getAnimations().length after transition starts");
    979 
    980      assert_equals_records(observer.takeRecords(),
    981        [{ added: animations, changed: [], removed: [] }],
    982        "records after transition starts");
    983 
    984      assert_equals(animations.filter(p => p.playState == "running").length, 3,
    985         "number of running Animations");
    986 
    987      // Seek well into each animation.
    988      animations.forEach(p => p.currentTime = 50 * MS_PER_SEC);
    989 
    990      // Prepare the set of expected change MutationRecords, one for each
    991      // animation that was seeked.
    992      var seekRecords = animations.map(
    993        p => ({ added: [], changed: [p], removed: [] })
    994      );
    995 
    996      // Cancel one of the transitions by setting transition-property.
    997      div.style.transitionProperty = "background-color, line-height";
    998 
    999      var colorAnimation  = animations.filter(p => p.playState != "running");
   1000      var otherAnimations = animations.filter(p => p.playState == "running");
   1001 
   1002      assert_equals(colorAnimation.length, 1,
   1003        "number of non-running Animations after cancelling one");
   1004      assert_equals(otherAnimations.length, 2,
   1005        "number of running Animations after cancelling one");
   1006 
   1007      assert_equals_records(observer.takeRecords(),
   1008        seekRecords.concat({ added: [], changed: [], removed: colorAnimation }),
   1009        "records after color transition end");
   1010 
   1011      // Cancel the remaining transitions.
   1012      div.style.transitionProperty = "none";
   1013      getComputedStyle(div).transitionProperty;
   1014 
   1015      assert_equals_records(observer.takeRecords(),
   1016        [{ added: [], changed: [], removed: otherAnimations }],
   1017        "records after other transition ends");
   1018    }, "multiple_transitions");
   1019 
   1020    // Test that starting a single animation that is cancelled by resetting
   1021    // the animation-name property dispatches an added notification and
   1022    // then a removed notification.
   1023    test(t => {
   1024      var div =
   1025        addDiv(t, { style: "animation: anim 100s" });
   1026      var observer =
   1027        setupSynchronousObserver(t,
   1028                                 aOptions.subtree ? div.parentNode : div,
   1029                                 aOptions.subtree);
   1030 
   1031      // The animation should cause the creation of a single Animation.
   1032      var animations = div.getAnimations();
   1033      assert_equals(animations.length, 1,
   1034        "getAnimations().length after animation start");
   1035 
   1036      assert_equals_records(observer.takeRecords(),
   1037        [{ added: animations, changed: [], removed: [] }],
   1038        "records after animation start");
   1039 
   1040      // Cancel the animation by setting animation-name.
   1041      div.style.animationName = "none";
   1042      getComputedStyle(div).animationName;
   1043 
   1044      assert_equals_records(observer.takeRecords(),
   1045        [{ added: [], changed: [], removed: animations }],
   1046        "records after animation end");
   1047    }, "single_animation_cancelled_name");
   1048 
   1049    // Test that starting a single animation that is cancelled by updating
   1050    // the animation-duration property dispatches an added notification and
   1051    // then a removed notification.
   1052    test(t => {
   1053      var div =
   1054        addDiv(t, { style: "animation: anim 100s" });
   1055      var observer =
   1056        setupSynchronousObserver(t,
   1057                                 aOptions.subtree ? div.parentNode : div,
   1058                                 aOptions.subtree);
   1059 
   1060      // The animation should cause the creation of a single Animation.
   1061      var animations = div.getAnimations();
   1062      assert_equals(animations.length, 1,
   1063        "getAnimations().length after animation start");
   1064 
   1065      assert_equals_records(observer.takeRecords(),
   1066        [{ added: animations, changed: [], removed: [] }],
   1067        "records after animation start");
   1068 
   1069      // Advance the animation by a second.
   1070      animations[0].currentTime += 1 * MS_PER_SEC;
   1071 
   1072      // Cancel the animation by setting animation-duration to a value less
   1073      // than a second.
   1074      div.style.animationDuration = "0.1s";
   1075      getComputedStyle(div).animationName;
   1076 
   1077      assert_equals_records(observer.takeRecords(),
   1078        [{ added: [], changed: animations, removed: [] },
   1079         { added: [], changed: [], removed: animations }],
   1080        "records after animation end");
   1081    }, "single_animation_cancelled_duration");
   1082 
   1083    // Test that starting a single animation that is cancelled by updating
   1084    // the animation-delay property dispatches an added notification and
   1085    // then a removed notification.
   1086    test(t => {
   1087      var div =
   1088        addDiv(t, { style: "animation: anim 100s" });
   1089      var observer =
   1090        setupSynchronousObserver(t,
   1091                                 aOptions.subtree ? div.parentNode : div,
   1092                                 aOptions.subtree);
   1093 
   1094      // The animation should cause the creation of a single Animation.
   1095      var animations = div.getAnimations();
   1096      assert_equals(animations.length, 1,
   1097        "getAnimations().length after animation start");
   1098 
   1099      assert_equals_records(observer.takeRecords(),
   1100        [{ added: animations, changed: [], removed: [] }],
   1101        "records after animation start");
   1102 
   1103      // Cancel the animation by setting animation-delay.
   1104      div.style.animationDelay = "-200s";
   1105      getComputedStyle(div).animationName;
   1106 
   1107      assert_equals_records(observer.takeRecords(),
   1108        [{ added: [], changed: [], removed: animations }],
   1109        "records after animation end");
   1110    }, "single_animation_cancelled_delay");
   1111 
   1112    // Test that starting a single animation that is cancelled by updating
   1113    // the animation-iteration-count property dispatches an added notification
   1114    // and then a removed notification.
   1115    test(t => {
   1116      // A short, repeated animation.
   1117      var div =
   1118        addDiv(t, { style: "animation: anim 0.5s infinite;" });
   1119      var observer =
   1120        setupSynchronousObserver(t,
   1121                                 aOptions.subtree ? div.parentNode : div,
   1122                                 aOptions.subtree);
   1123 
   1124      // The animation should cause the creation of a single Animation.
   1125      var animations = div.getAnimations();
   1126      assert_equals(animations.length, 1,
   1127        "getAnimations().length after animation start");
   1128 
   1129      assert_equals_records(observer.takeRecords(),
   1130        [{ added: animations, changed: [], removed: [] }],
   1131        "records after animation start");
   1132 
   1133      // Advance the animation until we are past the first iteration.
   1134      animations[0].currentTime += 1 * MS_PER_SEC;
   1135 
   1136      assert_equals_records(observer.takeRecords(),
   1137        [{ added: [], changed: animations, removed: [] }],
   1138        "records after seeking animations");
   1139 
   1140      // Cancel the animation by setting animation-iteration-count.
   1141      div.style.animationIterationCount = "1";
   1142      getComputedStyle(div).animationName;
   1143 
   1144      assert_equals_records(observer.takeRecords(),
   1145        [{ added: [], changed: [], removed: animations }],
   1146        "records after animation end");
   1147    }, "single_animation_cancelled_iteration_count");
   1148 
   1149    // Test that updating an animation property dispatches a changed notification.
   1150    [
   1151      { name: "duration",  prop: "animationDuration",       val: "200s"    },
   1152      { name: "timing",    prop: "animationTimingFunction", val: "linear"  },
   1153      { name: "iteration", prop: "animationIterationCount", val: "2"       },
   1154      { name: "direction", prop: "animationDirection",      val: "reverse" },
   1155      { name: "state",     prop: "animationPlayState",      val: "paused"  },
   1156      { name: "delay",     prop: "animationDelay",          val: "-1s"     },
   1157      { name: "fill",      prop: "animationFillMode",       val: "both"    },
   1158    ].forEach(aChangeTest => {
   1159      test(t => {
   1160        // Start a long animation.
   1161        var div = addDiv(t, { style: "animation: anim 100s;" });
   1162        var observer =
   1163          setupSynchronousObserver(t,
   1164                                   aOptions.subtree ? div.parentNode : div,
   1165                                   aOptions.subtree);
   1166 
   1167        // The animation should cause the creation of a single Animation.
   1168        var animations = div.getAnimations();
   1169        assert_equals(animations.length, 1,
   1170          "getAnimations().length after animation start");
   1171 
   1172        assert_equals_records(observer.takeRecords(),
   1173          [{ added: animations, changed: [], removed: [] }],
   1174          "records after animation start");
   1175 
   1176        // Change a property of the animation such that it keeps running.
   1177        div.style[aChangeTest.prop] = aChangeTest.val;
   1178        getComputedStyle(div).animationName;
   1179 
   1180        assert_equals_records(observer.takeRecords(),
   1181          [{ added: [], changed: animations, removed: [] }],
   1182          "records after animation change");
   1183 
   1184        // Cancel the animation.
   1185        div.style.animationName = "none";
   1186        getComputedStyle(div).animationName;
   1187 
   1188        assert_equals_records(observer.takeRecords(),
   1189          [{ added: [], changed: [], removed: animations }],
   1190          "records after animation end");
   1191      }, `single_animation_change_${aChangeTest.name}`);
   1192    });
   1193 
   1194    // Test that calling finish() on a pause-pending (but otherwise finished)
   1195    // animation dispatches a changed notification.
   1196    test(t => {
   1197      var div =
   1198        addDiv(t, { style: "animation: anim 100s forwards" });
   1199      var observer =
   1200        setupSynchronousObserver(t,
   1201                                 aOptions.subtree ? div.parentNode : div,
   1202                                 aOptions.subtree);
   1203 
   1204      // The animation should cause the creation of a single Animation.
   1205      var animations = div.getAnimations();
   1206      assert_equals(animations.length, 1,
   1207        "getAnimations().length after animation start");
   1208 
   1209      assert_equals_records(observer.takeRecords(),
   1210        [{ added: animations, changed: [], removed: [] }],
   1211        "records after animation start");
   1212 
   1213      // Finish and pause.
   1214      animations[0].finish();
   1215      animations[0].pause();
   1216      assert_true(animations[0].pending && animations[0].playState === "paused",
   1217        "playState after finishing and calling pause()");
   1218 
   1219      // Call finish() again to abort the pause
   1220      animations[0].finish();
   1221      assert_equals(animations[0].playState, "finished",
   1222        "playState after finishing again");
   1223 
   1224      // Wait for three MutationRecords for the Animation changes to
   1225      // be delivered: one for each finish(), pause(), finish() operation.
   1226      assert_equals_records(observer.takeRecords(),
   1227        [{ added: [], changed: animations, removed: [] },
   1228         { added: [], changed: animations, removed: [] },
   1229         { added: [], changed: animations, removed: [] }],
   1230        "records after finish(), pause(), finish()");
   1231 
   1232      // Cancel the animation.
   1233      div.style = "";
   1234      getComputedStyle(div).animationName;
   1235 
   1236      assert_equals_records(observer.takeRecords(),
   1237        [{ added: [], changed: [], removed: animations }],
   1238        "records after animation end");
   1239    }, "finish_from_pause_pending");
   1240 
   1241    // Test that calling play() on a finished Animation that fills forwards
   1242    // dispatches a changed notification.
   1243    test(t => {
   1244      // Animation with a forwards fill
   1245      var div =
   1246        addDiv(t, { style: "animation: anim 100s forwards" });
   1247      var observer =
   1248        setupSynchronousObserver(t,
   1249                                 aOptions.subtree ? div.parentNode : div,
   1250                                 aOptions.subtree);
   1251 
   1252      // The animation should cause the creation of a single Animation.
   1253      var animations = div.getAnimations();
   1254      assert_equals(animations.length, 1,
   1255        "getAnimations().length after animation start");
   1256 
   1257      assert_equals_records(observer.takeRecords(),
   1258        [{ added: animations, changed: [], removed: [] }],
   1259        "records after animation start");
   1260 
   1261      // Seek to the end
   1262      animations[0].finish();
   1263 
   1264      assert_equals_records(observer.takeRecords(),
   1265        [{ added: [], changed: animations, removed: [] }],
   1266        "records after finish()");
   1267 
   1268      // Since we are filling forwards, calling play() should produce a
   1269      // change record since the animation remains relevant.
   1270      animations[0].play();
   1271 
   1272      assert_equals_records(observer.takeRecords(),
   1273        [{ added: [], changed: animations, removed: [] }],
   1274        "records after play()");
   1275 
   1276      // Cancel the animation.
   1277      div.style = "";
   1278      getComputedStyle(div).animationName;
   1279 
   1280      assert_equals_records(observer.takeRecords(),
   1281        [{ added: [], changed: [], removed: animations }],
   1282        "records after animation end");
   1283    }, "play_filling_forwards");
   1284 
   1285    // Test that calling pause() on an Animation dispatches a changed
   1286    // notification.
   1287    test(t => {
   1288      var div =
   1289        addDiv(t, { style: "animation: anim 100s" });
   1290      var observer =
   1291        setupSynchronousObserver(t,
   1292                                 aOptions.subtree ? div.parentNode : div,
   1293                                 aOptions.subtree);
   1294 
   1295      // The animation should cause the creation of a single Animation.
   1296      var animations = div.getAnimations();
   1297      assert_equals(animations.length, 1,
   1298        "getAnimations().length after animation start");
   1299 
   1300      assert_equals_records(observer.takeRecords(),
   1301        [{ added: animations, changed: [], removed: [] }],
   1302        "records after animation start");
   1303 
   1304      // Pause
   1305      animations[0].pause();
   1306 
   1307      assert_equals_records(observer.takeRecords(),
   1308        [{ added: [], changed: animations, removed: [] }],
   1309        "records after pause()");
   1310 
   1311      // Redundant pause
   1312      animations[0].pause();
   1313 
   1314      assert_equals_records(observer.takeRecords(),
   1315        [], "records after redundant pause()");
   1316 
   1317      // Cancel the animation.
   1318      div.style = "";
   1319      getComputedStyle(div).animationName;
   1320 
   1321      assert_equals_records(observer.takeRecords(),
   1322        [{ added: [], changed: [], removed: animations }],
   1323        "records after animation end");
   1324    }, "pause");
   1325 
   1326    // Test that calling pause() on an Animation that is pause-pending
   1327    // does not dispatch an additional changed notification.
   1328    test(t => {
   1329      var div =
   1330        addDiv(t, { style: "animation: anim 100s" });
   1331      var observer =
   1332        setupSynchronousObserver(t,
   1333                                 aOptions.subtree ? div.parentNode : div,
   1334                                 aOptions.subtree);
   1335 
   1336      // The animation should cause the creation of a single Animation.
   1337      var animations = div.getAnimations();
   1338      assert_equals(animations.length, 1,
   1339        "getAnimations().length after animation start");
   1340 
   1341      assert_equals_records(observer.takeRecords(),
   1342        [{ added: animations, changed: [], removed: [] }],
   1343        "records after animation start");
   1344 
   1345      // Pause
   1346      animations[0].pause();
   1347 
   1348      // We are now pause-pending, but pause again
   1349      animations[0].pause();
   1350 
   1351      assert_equals_records(observer.takeRecords(),
   1352        [{ added: [], changed: animations, removed: [] }],
   1353        "records after pause()");
   1354 
   1355      // Cancel the animation.
   1356      div.style = "";
   1357      getComputedStyle(div).animationName;
   1358 
   1359      assert_equals_records(observer.takeRecords(),
   1360        [{ added: [], changed: [], removed: animations }],
   1361        "records after animation end");
   1362    }, "pause_while_pause_pending");
   1363 
   1364    // Test that calling play() on an Animation that is pause-pending
   1365    // dispatches a changed notification.
   1366    test(t => {
   1367      var div =
   1368        addDiv(t, { style: "animation: anim 100s" });
   1369      var observer =
   1370        setupSynchronousObserver(t,
   1371                                 aOptions.subtree ? div.parentNode : div,
   1372                                 aOptions.subtree);
   1373 
   1374      // The animation should cause the creation of a single Animation.
   1375      var animations = div.getAnimations();
   1376      assert_equals(animations.length, 1,
   1377        "getAnimations().length after animation start");
   1378 
   1379      assert_equals_records(observer.takeRecords(),
   1380        [{ added: animations, changed: [], removed: [] }],
   1381        "records after animation start");
   1382 
   1383      // Pause
   1384      animations[0].pause();
   1385 
   1386      // We are now pause-pending. If we play() now, we will abort the pause
   1387      animations[0].play();
   1388 
   1389      assert_equals_records(observer.takeRecords(),
   1390        [{ added: [], changed: animations, removed: [] },
   1391        { added: [], changed: animations, removed: [] }],
   1392                      "records after aborting a pause()");
   1393 
   1394      // Cancel the animation.
   1395      div.style = "";
   1396      getComputedStyle(div).animationName;
   1397 
   1398      assert_equals_records(observer.takeRecords(),
   1399        [{ added: [], changed: [], removed: animations }],
   1400        "records after animation end");
   1401    }, "aborted_pause");
   1402 
   1403    // Test that calling play() on a finished Animation that does *not* fill
   1404    // forwards dispatches an addition notification.
   1405    test(t => {
   1406      var div =
   1407        addDiv(t, { style: "animation: anim 100s" });
   1408      var observer =
   1409        setupSynchronousObserver(t,
   1410                                 aOptions.subtree ? div.parentNode : div,
   1411                                 aOptions.subtree);
   1412 
   1413      // The animation should cause the creation of a single Animation.
   1414      var animations = div.getAnimations();
   1415      assert_equals(animations.length, 1,
   1416        "getAnimations().length after animation start");
   1417 
   1418      assert_equals_records(observer.takeRecords(),
   1419        [{ added: animations, changed: [], removed: [] }],
   1420        "records after animation start");
   1421 
   1422      // Seek to the end
   1423      animations[0].finish();
   1424 
   1425      assert_equals_records(observer.takeRecords(),
   1426        [{ added: [], changed: [], removed: animations }],
   1427        "records after finish()");
   1428 
   1429      // Since we are *not* filling forwards, calling play() is equivalent
   1430      // to creating a new animation since it becomes relevant again.
   1431      animations[0].play();
   1432 
   1433      assert_equals_records(observer.takeRecords(),
   1434        [{ added: animations, changed: [], removed: [] }],
   1435        "records after play()");
   1436 
   1437      // Cancel the animation.
   1438      div.style = "";
   1439      getComputedStyle(div).animationName;
   1440 
   1441      assert_equals_records(observer.takeRecords(),
   1442        [{ added: [], changed: [], removed: animations }],
   1443        "records after animation end");
   1444    }, "play_after_finish");
   1445 
   1446  });
   1447 
   1448  test(t => {
   1449    var div = addDiv(t);
   1450    var observer = setupSynchronousObserver(t, div, true);
   1451 
   1452    var child = document.createElement("div");
   1453    div.appendChild(child);
   1454 
   1455    var anim1 = div.animate({ marginLeft: [ "0px", "50px" ] },
   1456                            100 * MS_PER_SEC);
   1457    var anim2 = child.animate({ marginLeft: [ "0px", "100px" ] },
   1458                              50 * MS_PER_SEC);
   1459    assert_equals_records(observer.takeRecords(),
   1460      [{ added: [anim1], changed: [], removed: [] },
   1461       { added: [anim2], changed: [], removed: [] }],
   1462      "records after animation is added");
   1463 
   1464    // After setting a new effect, we remove the current animation, anim1,
   1465    // because it is no longer attached to |div|, and then remove the previous
   1466    // animation, anim2. Finally, add back the anim1 which is in effect on
   1467    // |child| now. In addition, we sort them by tree order and they are
   1468    // batched.
   1469    anim1.effect = anim2.effect;
   1470    assert_equals_records(observer.takeRecords(),
   1471      [{ added: [], changed: [], removed: [anim1] },       // div
   1472       { added: [anim1], changed: [], removed: [anim2] }], // child
   1473      "records after animation effects are changed");
   1474  }, "set_effect_with_previous_animation");
   1475 
   1476  test(t => {
   1477    var div = addDiv(t);
   1478    var observer = setupSynchronousObserver(t, document, true);
   1479 
   1480    var anim = div.animate({ opacity: [ 0, 1 ] },
   1481                           { duration: 100 * MS_PER_SEC });
   1482 
   1483    var newTarget = document.createElement("div");
   1484 
   1485    assert_equals_records(observer.takeRecords(),
   1486      [{ added: [anim], changed: [], removed: [] }],
   1487      "records after animation is added");
   1488 
   1489    anim.effect.target = null;
   1490    assert_equals_records(observer.takeRecords(),
   1491      [{ added: [], changed: [], removed: [anim] }],
   1492      "records after setting null");
   1493 
   1494    anim.effect.target = div;
   1495    assert_equals_records(observer.takeRecords(),
   1496      [{ added: [anim], changed: [], removed: [] }],
   1497      "records after setting a target");
   1498 
   1499    anim.effect.target = addDiv(t);
   1500    assert_equals_records(observer.takeRecords(),
   1501      [{ added: [], changed: [], removed: [anim] },
   1502       { added: [anim], changed: [], removed: [] }],
   1503      "records after setting a different target");
   1504  }, "set_animation_target");
   1505 
   1506  test(t => {
   1507    var div = addDiv(t);
   1508    var observer = setupSynchronousObserver(t, div, true);
   1509 
   1510    var anim = div.animate({ opacity: [ 0, 1 ] },
   1511                           { duration: 200 * MS_PER_SEC,
   1512                             pseudoElement: '::before' });
   1513 
   1514    assert_equals_records(observer.takeRecords(),
   1515      [{ added: [anim], changed: [], removed: [] }],
   1516      "records after animation is added");
   1517 
   1518    anim.effect.updateTiming({ duration: 100 * MS_PER_SEC });
   1519    assert_equals_records(observer.takeRecords(),
   1520      [{ added: [], changed: [anim], removed: [] }],
   1521      "records after duration is changed");
   1522 
   1523    anim.effect.updateTiming({ duration: 100 * MS_PER_SEC });
   1524    assert_equals_records(observer.takeRecords(),
   1525      [], "records after assigning same value");
   1526 
   1527    anim.currentTime = anim.effect.getComputedTiming().duration * 2;
   1528    anim.finish();
   1529    assert_equals_records(observer.takeRecords(),
   1530      [{ added: [], changed: [], removed: [anim] }],
   1531      "records after animation end");
   1532 
   1533    anim.effect.updateTiming({
   1534      duration: anim.effect.getComputedTiming().duration * 3
   1535    });
   1536    assert_equals_records(observer.takeRecords(),
   1537      [{ added: [anim], changed: [], removed: [] }],
   1538      "records after animation restarted");
   1539 
   1540    anim.effect.updateTiming({ duration: "auto" });
   1541    assert_equals_records(observer.takeRecords(),
   1542      [{ added: [], changed: [], removed: [anim] }],
   1543      "records after duration set \"auto\"");
   1544 
   1545    anim.effect.updateTiming({ duration: "auto" });
   1546    assert_equals_records(observer.takeRecords(),
   1547      [], "records after assigning same value \"auto\"");
   1548  }, "change_duration_and_currenttime_on_pseudo_elements");
   1549 
   1550  test(t => {
   1551    var div = addDiv(t);
   1552    var observer = setupSynchronousObserver(t, div, false);
   1553 
   1554    var anim = div.animate({ opacity: [ 0, 1 ] },
   1555                           { duration: 100 * MS_PER_SEC });
   1556    var pAnim = div.animate({ opacity: [ 0, 1 ] },
   1557                            { duration: 100 * MS_PER_SEC,
   1558                              pseudoElement: "::before" });
   1559 
   1560    assert_equals_records(observer.takeRecords(),
   1561      [{ added: [anim], changed: [], removed: [] }],
   1562      "records after animation is added");
   1563 
   1564    anim.finish();
   1565    pAnim.finish();
   1566 
   1567    assert_equals_records(observer.takeRecords(),
   1568      [{ added: [], changed: [], removed: [anim] }],
   1569      "records after animation is finished");
   1570  }, "exclude_animations_targeting_pseudo_elements");
   1571 }
   1572 
   1573 W3CTest.runner.expectAssertions(0, 12); // bug 1189015
   1574 runTest();
   1575 
   1576 </script>