Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

<!doctype html>
<meta charset=utf-8>
<title>Animation interface: style change events</title>
<link rel="help"
<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),
overallProgress: UsePropertyTest(animation => animation.overallProgress),
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>