| <!doctype html> |
| <meta charset=utf-8> |
| <title>Animation interface: style change events</title> |
| <link rel="help" |
| href="https://drafts.csswg.org/web-animations-1/#model-liveness"> |
| <script src="/resources/testharness.js"></script> |
| <script src="/resources/testharnessreport.js"></script> |
| <script src="../../testcommon.js"></script> |
| <body> |
| <div id="log"></div> |
| <script> |
| 'use strict'; |
| |
| // Test that each property defined in the Animation interface behaves as |
| // expected with regards to whether or not it produces style change events. |
| // |
| // There are two types of tests: |
| // |
| // PlayAnimationTest |
| // |
| // For properties that are able to cause the Animation to start affecting |
| // the target CSS property. |
| // |
| // This function takes either: |
| // |
| // (a) A function that simply "plays" that passed-in Animation (i.e. makes |
| // it start affecting the target CSS property. |
| // |
| // (b) An object with the following format: |
| // |
| // { |
| // setup: elem => { /* return Animation */ }, |
| // test: animation => { /* play |animation| */ }, |
| // shouldFlush: boolean /* optional, defaults to false */ |
| // } |
| // |
| // If the latter form is used, the setup function should return an Animation |
| // that does NOT (yet) have an in-effect AnimationEffect that affects the |
| // 'opacity' property. Otherwise, the transition we use to detect if a style |
| // change event has occurred will never have a chance to be triggered (since |
| // the animated style will clobber both before-change and after-change |
| // style). |
| // |
| // Examples of valid animations: |
| // |
| // - An animation that is idle, or finished but without a fill mode. |
| // - An animation with an effect that that does not affect opacity. |
| // |
| // UsePropertyTest |
| // |
| // For properties that cannot cause the Animation to start affecting the |
| // target CSS property. |
| // |
| // The shape of the parameter to the UsePropertyTest is identical to the |
| // PlayAnimationTest. The only difference is that the function (or 'test' |
| // function of the object format is used) does not need to play the |
| // animation, but simply needs to get/set the property under test. |
| |
| const PlayAnimationTest = testFuncOrObj => { |
| let test, setup, shouldFlush; |
| |
| if (typeof testFuncOrObj === 'function') { |
| test = testFuncOrObj; |
| shouldFlush = false; |
| } else { |
| test = testFuncOrObj.test; |
| if (typeof testFuncOrObj.setup === 'function') { |
| setup = testFuncOrObj.setup; |
| } |
| shouldFlush = !!testFuncOrObj.shouldFlush; |
| } |
| |
| if (!setup) { |
| setup = elem => |
| new Animation( |
| new KeyframeEffect(elem, { opacity: [0, 1] }, 100 * MS_PER_SEC) |
| ); |
| } |
| |
| return { test, setup, shouldFlush }; |
| }; |
| |
| const UsePropertyTest = testFuncOrObj => { |
| const { setup, test, shouldFlush } = PlayAnimationTest(testFuncOrObj); |
| |
| let coveringAnimation; |
| return { |
| setup: elem => { |
| coveringAnimation = new Animation( |
| new KeyframeEffect(elem, { opacity: [0, 1] }, 100 * MS_PER_SEC) |
| ); |
| |
| return setup(elem); |
| }, |
| test: animation => { |
| test(animation); |
| coveringAnimation.play(); |
| }, |
| shouldFlush, |
| }; |
| }; |
| |
| const tests = { |
| id: UsePropertyTest(animation => (animation.id = 'yer')), |
| get effect() { |
| let effect; |
| return PlayAnimationTest({ |
| setup: elem => { |
| // Create a new effect and animation but don't associate them yet |
| effect = new KeyframeEffect( |
| elem, |
| { opacity: [0.5, 1] }, |
| 100 * MS_PER_SEC |
| ); |
| return elem.animate(null, 100 * MS_PER_SEC); |
| }, |
| test: animation => { |
| // Read the effect |
| animation.effect; |
| |
| // Assign the effect |
| animation.effect = effect; |
| }, |
| }); |
| }, |
| timeline: PlayAnimationTest({ |
| setup: elem => { |
| // Create a new animation with no timeline |
| const animation = new Animation( |
| new KeyframeEffect(elem, { opacity: [0.5, 1] }, 100 * MS_PER_SEC), |
| null |
| ); |
| // Set the hold time so that once we assign a timeline it will begin to |
| // play. |
| animation.currentTime = 0; |
| |
| return animation; |
| }, |
| test: animation => { |
| // Get the timeline |
| animation.timeline; |
| |
| // Play the animation by setting the timeline |
| animation.timeline = document.timeline; |
| }, |
| }), |
| startTime: PlayAnimationTest(animation => { |
| // Get the startTime |
| animation.startTime; |
| |
| // Play the animation by setting the startTime |
| animation.startTime = document.timeline.currentTime; |
| }), |
| currentTime: PlayAnimationTest(animation => { |
| // Get the currentTime |
| animation.currentTime; |
| |
| // Play the animation by setting the currentTime |
| animation.currentTime = 0; |
| }), |
| playbackRate: UsePropertyTest(animation => { |
| // Get and set the playbackRate |
| animation.playbackRate = animation.playbackRate * 1.1; |
| }), |
| playState: UsePropertyTest(animation => animation.playState), |
| pending: UsePropertyTest(animation => animation.pending), |
| // Strictly speaking, rangeStart and rangeEnd can change whether the effect |
| // is active, but only if the animation has a view timeline. Otherwise, it has |
| // no effect. |
| rangeStart: UsePropertyTest(animation => animation.rangeStart), |
| rangeEnd: UsePropertyTest(animation => animation.rangeEnd), |
| progress: UsePropertyTest(animation => animation.progress), |
| replaceState: UsePropertyTest(animation => animation.replaceState), |
| ready: UsePropertyTest(animation => animation.ready), |
| finished: UsePropertyTest(animation => { |
| // Get the finished Promise |
| animation.finished; |
| }), |
| onfinish: UsePropertyTest(animation => { |
| // Get the onfinish member |
| animation.onfinish; |
| |
| // Set the onfinish menber |
| animation.onfinish = () => {}; |
| }), |
| onremove: UsePropertyTest(animation => { |
| // Get the onremove member |
| animation.onremove; |
| |
| // Set the onremove menber |
| animation.onremove = () => {}; |
| }), |
| oncancel: UsePropertyTest(animation => { |
| // Get the oncancel member |
| animation.oncancel; |
| |
| // Set the oncancel menber |
| animation.oncancel = () => {}; |
| }), |
| cancel: UsePropertyTest({ |
| // Animate _something_ just to make the test more interesting |
| setup: elem => elem.animate({ color: ['green', 'blue'] }, 100 * MS_PER_SEC), |
| test: animation => { |
| animation.cancel(); |
| }, |
| }), |
| finish: PlayAnimationTest({ |
| setup: elem => |
| new Animation( |
| new KeyframeEffect( |
| elem, |
| { opacity: [0.5, 1] }, |
| { |
| duration: 100 * MS_PER_SEC, |
| fill: 'both', |
| } |
| ) |
| ), |
| test: animation => { |
| animation.finish(); |
| }, |
| }), |
| play: PlayAnimationTest(animation => animation.play()), |
| pause: PlayAnimationTest(animation => { |
| // Pause animation -- this will cause the animation to transition from the |
| // 'idle' state to the 'paused' (but pending) state with hold time zero. |
| animation.pause(); |
| }), |
| updatePlaybackRate: UsePropertyTest(animation => { |
| animation.updatePlaybackRate(1.1); |
| }), |
| // We would like to use a PlayAnimationTest here but reverse() is async and |
| // doesn't start applying its result until the animation is ready. |
| reverse: UsePropertyTest({ |
| setup: elem => { |
| // Create a new animation and seek it to the end so that it no longer |
| // affects style (since it has no fill mode). |
| const animation = elem.animate({ opacity: [0.5, 1] }, 100 * MS_PER_SEC); |
| animation.finish(); |
| return animation; |
| }, |
| test: animation => { |
| animation.reverse(); |
| }, |
| }), |
| persist: PlayAnimationTest({ |
| setup: async elem => { |
| // Create an animation whose replaceState is 'removed'. |
| const animA = elem.animate( |
| { opacity: 1 }, |
| { duration: 1, fill: 'forwards' } |
| ); |
| const animB = elem.animate( |
| { opacity: 1 }, |
| { duration: 1, fill: 'forwards' } |
| ); |
| await animA.finished; |
| animB.cancel(); |
| |
| return animA; |
| }, |
| test: animation => { |
| animation.persist(); |
| }, |
| }), |
| commitStyles: PlayAnimationTest({ |
| setup: async elem => { |
| // Create an animation whose replaceState is 'removed'. |
| const animA = elem.animate( |
| // It's important to use opacity of '1' here otherwise we'll create a |
| // transition due to updating the specified style whereas the transition |
| // we want to detect is the one from flushing due to calling |
| // commitStyles. |
| { opacity: 1 }, |
| { duration: 1, fill: 'forwards' } |
| ); |
| const animB = elem.animate( |
| { opacity: 1 }, |
| { duration: 1, fill: 'forwards' } |
| ); |
| await animA.finished; |
| animB.cancel(); |
| |
| return animA; |
| }, |
| test: animation => { |
| animation.commitStyles(); |
| }, |
| shouldFlush: true, |
| }), |
| get ['Animation constructor']() { |
| let originalElem; |
| return UsePropertyTest({ |
| setup: elem => { |
| originalElem = elem; |
| // Return a dummy animation so the caller has something to wait on |
| return elem.animate(null); |
| }, |
| test: () => |
| new Animation( |
| new KeyframeEffect( |
| originalElem, |
| { opacity: [0.5, 1] }, |
| 100 * MS_PER_SEC |
| ) |
| ), |
| }); |
| }, |
| }; |
| |
| // Check that each enumerable property and the constructor follow the |
| // expected behavior with regards to triggering style change events. |
| const properties = [ |
| ...Object.keys(Animation.prototype), |
| 'Animation constructor', |
| ]; |
| |
| test(() => { |
| for (const property of Object.keys(tests)) { |
| assert_in_array( |
| property, |
| properties, |
| `Test property '${property}' should be one of the properties on ` + |
| ' Animation' |
| ); |
| } |
| }, 'All property keys are recognized'); |
| |
| for (const key of properties) { |
| promise_test(async t => { |
| assert_own_property(tests, key, `Should have a test for '${key}' property`); |
| const { setup, test, shouldFlush } = tests[key]; |
| |
| // Setup target element |
| const div = createDiv(t); |
| let gotTransition = false; |
| div.addEventListener('transitionrun', () => { |
| gotTransition = true; |
| }); |
| |
| // Setup animation |
| const animation = await setup(div); |
| |
| // Setup transition start point |
| div.style.transition = 'opacity 100s'; |
| getComputedStyle(div).opacity; |
| |
| // Update specified style but don't flush |
| div.style.opacity = '0.5'; |
| |
| // Trigger the property |
| test(animation); |
| |
| // If the test function produced a style change event it will have triggered |
| // a transition. |
| |
| // Wait for the animation to start and then for at least two animation |
| // frames to give the transitionrun event a chance to be dispatched. |
| assert_true( |
| typeof animation.ready !== 'undefined', |
| 'Should have a valid animation to wait on' |
| ); |
| await animation.ready; |
| await waitForAnimationFrames(2); |
| |
| if (shouldFlush) { |
| assert_true(gotTransition, 'A transition should have been triggered'); |
| } else { |
| assert_false( |
| gotTransition, |
| 'A transition should NOT have been triggered' |
| ); |
| } |
| }, `Animation.${key} produces expected style change events`); |
| } |
| </script> |
| </body> |