tor-browser

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

style-change-events.html (11238B)


      1 <!doctype html>
      2 <meta charset=utf-8>
      3 <title>Animation interface: style change events</title>
      4 <link rel="help"
      5      href="https://drafts.csswg.org/web-animations-1/#model-liveness">
      6 <script src="/resources/testharness.js"></script>
      7 <script src="/resources/testharnessreport.js"></script>
      8 <script src="../../testcommon.js"></script>
      9 <body>
     10 <div id="log"></div>
     11 <script>
     12 'use strict';
     13 
     14 // Test that each property defined in the Animation interface behaves as
     15 // expected with regards to whether or not it produces style change events.
     16 //
     17 // There are two types of tests:
     18 //
     19 //   PlayAnimationTest
     20 //
     21 //     For properties that are able to cause the Animation to start affecting
     22 //     the target CSS property.
     23 //
     24 //     This function takes either:
     25 //
     26 //     (a) A function that simply "plays" that passed-in Animation (i.e. makes
     27 //         it start affecting the target CSS property.
     28 //
     29 //     (b) An object with the following format:
     30 //
     31 //         {
     32 //            setup: elem => { /* return Animation */ },
     33 //            test: animation => { /* play |animation| */ },
     34 //            shouldFlush: boolean /* optional, defaults to false */
     35 //         }
     36 //
     37 //     If the latter form is used, the setup function should return an Animation
     38 //     that does NOT (yet) have an in-effect AnimationEffect that affects the
     39 //     'opacity' property. Otherwise, the transition we use to detect if a style
     40 //     change event has occurred will never have a chance to be triggered (since
     41 //     the animated style will clobber both before-change and after-change
     42 //     style).
     43 //
     44 //     Examples of valid animations:
     45 //
     46 //       - An animation that is idle, or finished but without a fill mode.
     47 //       - An animation with an effect that that does not affect opacity.
     48 //
     49 //  UsePropertyTest
     50 //
     51 //    For properties that cannot cause the Animation to start affecting the
     52 //    target CSS property.
     53 //
     54 //    The shape of the parameter to the UsePropertyTest is identical to the
     55 //    PlayAnimationTest. The only difference is that the function (or 'test'
     56 //    function of the object format is used) does not need to play the
     57 //    animation, but simply needs to get/set the property under test.
     58 
     59 const PlayAnimationTest = testFuncOrObj => {
     60  let test, setup, shouldFlush;
     61 
     62  if (typeof testFuncOrObj === 'function') {
     63    test = testFuncOrObj;
     64    shouldFlush = false;
     65  } else {
     66    test = testFuncOrObj.test;
     67    if (typeof testFuncOrObj.setup === 'function') {
     68      setup = testFuncOrObj.setup;
     69    }
     70    shouldFlush = !!testFuncOrObj.shouldFlush;
     71  }
     72 
     73  if (!setup) {
     74    setup = elem =>
     75      new Animation(
     76        new KeyframeEffect(elem, { opacity: [0, 1] }, 100 * MS_PER_SEC)
     77      );
     78  }
     79 
     80  return { test, setup, shouldFlush };
     81 };
     82 
     83 const UsePropertyTest = testFuncOrObj => {
     84  const { setup, test, shouldFlush } = PlayAnimationTest(testFuncOrObj);
     85 
     86  let coveringAnimation;
     87  return {
     88    setup: elem => {
     89      coveringAnimation = new Animation(
     90        new KeyframeEffect(elem, { opacity: [0, 1] }, 100 * MS_PER_SEC)
     91      );
     92 
     93      return setup(elem);
     94    },
     95    test: animation => {
     96      test(animation);
     97      coveringAnimation.play();
     98    },
     99    shouldFlush,
    100  };
    101 };
    102 
    103 const tests = {
    104  id: UsePropertyTest(animation => (animation.id = 'yer')),
    105  get effect() {
    106    let effect;
    107    return PlayAnimationTest({
    108      setup: elem => {
    109        // Create a new effect and animation but don't associate them yet
    110        effect = new KeyframeEffect(
    111          elem,
    112          { opacity: [0.5, 1] },
    113          100 * MS_PER_SEC
    114        );
    115        return elem.animate(null, 100 * MS_PER_SEC);
    116      },
    117      test: animation => {
    118        // Read the effect
    119        animation.effect;
    120 
    121        // Assign the effect
    122        animation.effect = effect;
    123      },
    124    });
    125  },
    126  timeline: PlayAnimationTest({
    127    setup: elem => {
    128      // Create a new animation with no timeline
    129      const animation = new Animation(
    130        new KeyframeEffect(elem, { opacity: [0.5, 1] }, 100 * MS_PER_SEC),
    131        null
    132      );
    133      // Set the hold time so that once we assign a timeline it will begin to
    134      // play.
    135      animation.currentTime = 0;
    136 
    137      return animation;
    138    },
    139    test: animation => {
    140      // Get the timeline
    141      animation.timeline;
    142 
    143      // Play the animation by setting the timeline
    144      animation.timeline = document.timeline;
    145    },
    146  }),
    147  startTime: PlayAnimationTest(animation => {
    148    // Get the startTime
    149    animation.startTime;
    150 
    151    // Play the animation by setting the startTime
    152    animation.startTime = document.timeline.currentTime;
    153  }),
    154  currentTime: PlayAnimationTest(animation => {
    155    // Get the currentTime
    156    animation.currentTime;
    157 
    158    // Play the animation by setting the currentTime
    159    animation.currentTime = 0;
    160  }),
    161  playbackRate: UsePropertyTest(animation => {
    162    // Get and set the playbackRate
    163    animation.playbackRate = animation.playbackRate * 1.1;
    164  }),
    165  playState: UsePropertyTest(animation => animation.playState),
    166  pending: UsePropertyTest(animation => animation.pending),
    167  // Strictly speaking, rangeStart and rangeEnd can change whether the effect
    168  // is active, but only if the animation has a view timeline. Otherwise, it has
    169  // no effect.
    170  rangeStart: UsePropertyTest(animation => animation.rangeStart),
    171  rangeEnd:  UsePropertyTest(animation => animation.rangeEnd),
    172  overallProgress: UsePropertyTest(animation => animation.overallProgress),
    173  replaceState: UsePropertyTest(animation => animation.replaceState),
    174  ready: UsePropertyTest(animation => animation.ready),
    175  finished: UsePropertyTest(animation => {
    176    // Get the finished Promise
    177    animation.finished;
    178  }),
    179  onfinish: UsePropertyTest(animation => {
    180    // Get the onfinish member
    181    animation.onfinish;
    182 
    183    // Set the onfinish menber
    184    animation.onfinish = () => {};
    185  }),
    186  onremove: UsePropertyTest(animation => {
    187    // Get the onremove member
    188    animation.onremove;
    189 
    190    // Set the onremove menber
    191    animation.onremove = () => {};
    192  }),
    193  oncancel: UsePropertyTest(animation => {
    194    // Get the oncancel member
    195    animation.oncancel;
    196 
    197    // Set the oncancel menber
    198    animation.oncancel = () => {};
    199  }),
    200  cancel: UsePropertyTest({
    201    // Animate _something_ just to make the test more interesting
    202    setup: elem => elem.animate({ color: ['green', 'blue'] }, 100 * MS_PER_SEC),
    203    test: animation => {
    204      animation.cancel();
    205    },
    206  }),
    207  finish: PlayAnimationTest({
    208    setup: elem =>
    209      new Animation(
    210        new KeyframeEffect(
    211          elem,
    212          { opacity: [0.5, 1] },
    213          {
    214            duration: 100 * MS_PER_SEC,
    215            fill: 'both',
    216          }
    217        )
    218      ),
    219    test: animation => {
    220      animation.finish();
    221    },
    222  }),
    223  play: PlayAnimationTest(animation => animation.play()),
    224  pause: PlayAnimationTest(animation => {
    225    // Pause animation -- this will cause the animation to transition from the
    226    // 'idle' state to the 'paused' (but pending) state with hold time zero.
    227    animation.pause();
    228  }),
    229  updatePlaybackRate: UsePropertyTest(animation => {
    230    animation.updatePlaybackRate(1.1);
    231  }),
    232  // We would like to use a PlayAnimationTest here but reverse() is async and
    233  // doesn't start applying its result until the animation is ready.
    234  reverse: UsePropertyTest({
    235    setup: elem => {
    236      // Create a new animation and seek it to the end so that it no longer
    237      // affects style (since it has no fill mode).
    238      const animation = elem.animate({ opacity: [0.5, 1] }, 100 * MS_PER_SEC);
    239      animation.finish();
    240      return animation;
    241    },
    242    test: animation => {
    243      animation.reverse();
    244    },
    245  }),
    246  persist: PlayAnimationTest({
    247    setup: async elem => {
    248      // Create an animation whose replaceState is 'removed'.
    249      const animA = elem.animate(
    250        { opacity: 1 },
    251        { duration: 1, fill: 'forwards' }
    252      );
    253      const animB = elem.animate(
    254        { opacity: 1 },
    255        { duration: 1, fill: 'forwards' }
    256      );
    257      await animA.finished;
    258      animB.cancel();
    259 
    260      return animA;
    261    },
    262    test: animation => {
    263      animation.persist();
    264    },
    265  }),
    266  commitStyles: PlayAnimationTest({
    267    setup: async elem => {
    268      // Create an animation whose replaceState is 'removed'.
    269      const animA = elem.animate(
    270        // It's important to use opacity of '1' here otherwise we'll create a
    271        // transition due to updating the specified style whereas the transition
    272        // we want to detect is the one from flushing due to calling
    273        // commitStyles.
    274        { opacity: 1 },
    275        { duration: 1, fill: 'forwards' }
    276      );
    277      const animB = elem.animate(
    278        { opacity: 1 },
    279        { duration: 1, fill: 'forwards' }
    280      );
    281      await animA.finished;
    282      animB.cancel();
    283 
    284      return animA;
    285    },
    286    test: animation => {
    287      animation.commitStyles();
    288    },
    289    shouldFlush: true,
    290  }),
    291  get ['Animation constructor']() {
    292    let originalElem;
    293    return UsePropertyTest({
    294      setup: elem => {
    295        originalElem = elem;
    296        // Return a dummy animation so the caller has something to wait on
    297        return elem.animate(null);
    298      },
    299      test: () =>
    300        new Animation(
    301          new KeyframeEffect(
    302            originalElem,
    303            { opacity: [0.5, 1] },
    304            100 * MS_PER_SEC
    305          )
    306        ),
    307    });
    308  },
    309 };
    310 
    311 // Check that each enumerable property and the constructor follow the
    312 // expected behavior with regards to triggering style change events.
    313 const properties = [
    314  ...Object.keys(Animation.prototype),
    315  'Animation constructor',
    316 ];
    317 
    318 test(() => {
    319  for (const property of Object.keys(tests)) {
    320    assert_in_array(
    321      property,
    322      properties,
    323      `Test property '${property}' should be one of the properties on ` +
    324        ' Animation'
    325    );
    326  }
    327 }, 'All property keys are recognized');
    328 
    329 for (const key of properties) {
    330  promise_test(async t => {
    331    assert_own_property(tests, key, `Should have a test for '${key}' property`);
    332    const { setup, test, shouldFlush } = tests[key];
    333 
    334    // Setup target element
    335    const div = createDiv(t);
    336    let gotTransition = false;
    337    div.addEventListener('transitionrun', () => {
    338      gotTransition = true;
    339    });
    340 
    341    // Setup animation
    342    const animation = await setup(div);
    343 
    344    // Setup transition start point
    345    div.style.transition = 'opacity 100s';
    346    getComputedStyle(div).opacity;
    347 
    348    // Update specified style but don't flush
    349    div.style.opacity = '0.5';
    350 
    351    // Trigger the property
    352    test(animation);
    353 
    354    // If the test function produced a style change event it will have triggered
    355    // a transition.
    356 
    357    // Wait for the animation to start and then for at least two animation
    358    // frames to give the transitionrun event a chance to be dispatched.
    359    assert_true(
    360      typeof animation.ready !== 'undefined',
    361      'Should have a valid animation to wait on'
    362    );
    363    await animation.ready;
    364    await waitForAnimationFrames(2);
    365 
    366    if (shouldFlush) {
    367      assert_true(gotTransition, 'A transition should have been triggered');
    368    } else {
    369      assert_false(
    370        gotTransition,
    371        'A transition should NOT have been triggered'
    372      );
    373    }
    374  }, `Animation.${key} produces expected style change events`);
    375 }
    376 </script>
    377 </body>