Files
tubestation/dom/animation/test/css-animations/file_animation-currenttime.html
Brian Birtles ec2b66d21a Bug 1161320 - Fix conflict between finishing and aborting a pause; r=jwatt
Animation::ResumeAt contains an assertion that, when we exit the play-pending
state, checks we either have a resolved start time or a resolved hold time.

That's normally true but if we are aborting a pause on animation that is
finished we can end up with a resolved start time (since we don't clear the
start time when we're aborting a pause) and a resolved hold time (either
because the regular finishing behavior set one, or, because play() applied
auto-rewinding behavior and set it).

In that case we should actually respect the hold time and update the start time
when resuming the animation. However, ResumeAt won't update the start time if it
is already set.

This patch fixes that by clearing the start time in DoPlay in the case where we
are aborting a pause and have a hold time.
2015-05-08 16:17:13 +09:00

572 lines
21 KiB
HTML

<!doctype html>
<html>
<head>
<meta charset=utf-8>
<title>Tests for the effect of setting a CSS animation's
Animation.currentTime</title>
<style>
.animated-div {
margin-left: 10px;
/* Make it easier to calculate expected values: */
animation-timing-function: linear ! important;
}
@keyframes anim {
from { margin-left: 100px; }
to { margin-left: 200px; }
}
</style>
<script src="../testcommon.js"></script>
</head>
<body>
<script type="text/javascript">
'use strict';
// TODO: add equivalent tests without an animation-delay, but first we need to
// change the timing of animationstart dispatch. (Right now the animationstart
// event will fire before the ready Promise is resolved if there is no
// animation-delay.)
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1134163
// TODO: Once the computedTiming property is implemented, add checks to the
// checker helpers to ensure that computedTiming's properties are updated as
// expected.
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1108055
const CSS_ANIM_EVENTS =
['animationstart', 'animationiteration', 'animationend'];
const ANIM_DELAY_MS = 1000000; // 1000s
const ANIM_DUR_MS = 1000000; // 1000s
const ANIM_PROPERTY_VAL = 'anim ' + ANIM_DUR_MS + 'ms ' + ANIM_DELAY_MS + 'ms';
/**
* These helpers get the value that the currentTime needs to be set to, to put
* an animation that uses the above ANIM_DELAY_MS and ANIM_DUR_MS values into
* the middle of various phases or points through the active duration.
*/
function currentTimeForBeforePhase(timeline) {
return ANIM_DELAY_MS / 2;
}
function currentTimeForActivePhase(timeline) {
return ANIM_DELAY_MS + ANIM_DUR_MS / 2;
}
function currentTimeForAfterPhase(timeline) {
return ANIM_DELAY_MS + ANIM_DUR_MS + ANIM_DELAY_MS / 2;
}
function currentTimeForStartOfActiveInterval(timeline) {
return ANIM_DELAY_MS;
}
function currentTimeForFiftyPercentThroughActiveInterval(timeline) {
return ANIM_DELAY_MS + ANIM_DUR_MS * 0.5;
}
function currentTimeForEndOfActiveInterval(timeline) {
return ANIM_DELAY_MS + ANIM_DUR_MS;
}
// Expected computed 'margin-left' values at points during the active interval:
// When we assert_between_inclusive using these values we could in theory cause
// intermittent failure due to very long delays between paints, but since the
// active duration is 1000s long, a delay would need to be around 100s to cause
// that. If that's happening then there are likely other issues that should be
// fixed, so a failure to make us look into that seems like a good thing.
const UNANIMATED_POSITION = 10;
const INITIAL_POSITION = 100;
const TEN_PCT_POSITION = 110;
const FIFTY_PCT_POSITION = 150;
const END_POSITION = 200;
// The terms used for the naming of the following helper functions refer to
// terms used in the Web Animations specification for specific phases of an
// animation. The terms can be found here:
//
// https://w3c.github.io/web-animations/#animation-effect-phases-and-states
//
// Note the distinction between the "animation start time" which occurs before
// the start delay and the start of the active interval which occurs after it.
// Called when currentTime is set to zero (the beginning of the start delay).
function checkStateOnSettingCurrentTimeToZero(animation)
{
// We don't test animation.currentTime since our caller just set it.
assert_equals(animation.playState, 'running',
'Animation.playState should be "running" at the start of ' +
'the start delay');
assert_equals(animation.effect.target.style.animationPlayState, 'running',
'Animation.effect.target.style.animationPlayState should be ' +
'"running" at the start of the start delay');
var div = animation.effect.target;
var marginLeft = parseFloat(getComputedStyle(div).marginLeft);
assert_equals(marginLeft, UNANIMATED_POSITION,
'the computed value of margin-left should be unaffected ' +
'at the beginning of the start delay');
}
// Called when the ready Promise's callbacks should happen
function checkStateOnReadyPromiseResolved(animation)
{
// the 0.0001 here is for rounding error
assert_less_than_equal(animation.currentTime,
animation.timeline.currentTime - animation.startTime + 0.0001,
'Animation.currentTime should be less than the local time ' +
'equivalent of the timeline\'s currentTime on the first paint tick ' +
'after animation creation');
assert_equals(animation.playState, 'running',
'Animation.playState should be "running" on the first paint ' +
'tick after animation creation');
assert_equals(animation.effect.target.style.animationPlayState, 'running',
'Animation.effect.target.style.animationPlayState should be ' +
'"running" on the first paint tick after animation creation');
var div = animation.effect.target;
var marginLeft = parseFloat(getComputedStyle(div).marginLeft);
assert_equals(marginLeft, UNANIMATED_POSITION,
'the computed value of margin-left should be unaffected ' +
'by an animation with a delay on ready Promise resolve');
}
// Called when currentTime is set to the time the active interval starts.
function checkStateAtActiveIntervalStartTime(animation)
{
// We don't test animation.currentTime since our caller just set it.
assert_equals(animation.playState, 'running',
'Animation.playState should be "running" at the start of ' +
'the active interval');
assert_equals(animation.effect.target.style.animationPlayState, 'running',
'Animation.effect.target.style.animationPlayState should be ' +
'"running" at the start of the active interval');
var div = animation.effect.target;
var marginLeft = parseFloat(getComputedStyle(div).marginLeft);
assert_between_inclusive(marginLeft, INITIAL_POSITION, TEN_PCT_POSITION,
'the computed value of margin-left should be close to the value at the ' +
'beginning of the animation');
}
function checkStateAtFiftyPctOfActiveInterval(animation)
{
// We don't test animation.currentTime since our caller just set it.
var div = animation.effect.target;
var marginLeft = parseFloat(getComputedStyle(div).marginLeft);
assert_equals(marginLeft, FIFTY_PCT_POSITION,
'the computed value of margin-left should be half way through the ' +
'animation at the midpoint of the active interval');
}
// Called when currentTime is set to the time the active interval ends.
function checkStateAtActiveIntervalEndTime(animation)
{
// We don't test animation.currentTime since our caller just set it.
assert_equals(animation.playState, 'finished',
'Animation.playState should be "finished" at the end of ' +
'the active interval');
assert_equals(animation.effect.target.style.animationPlayState, "running",
'Animation.effect.target.style.animationPlayState should be ' +
'"finished" at the end of the active interval');
var div = animation.effect.target;
var marginLeft = parseFloat(getComputedStyle(div).marginLeft);
assert_equals(marginLeft, UNANIMATED_POSITION,
'the computed value of margin-left should be unaffected ' +
'by the animation at the end of the active duration when the ' +
'animation-fill-mode is none');
}
test(function(t)
{
var div = addDiv(t, {'class': 'animated-div'});
div.style.animation = ANIM_PROPERTY_VAL;
var animation = div.getAnimations()[0];
// Animations shouldn't start until the next paint tick, so:
assert_equals(animation.currentTime, 0,
'Animation.currentTime should be zero when an animation ' +
'is initially created');
assert_equals(animation.playState, "pending",
'Animation.playState should be "pending" when an animation ' +
'is initially created');
assert_equals(animation.effect.target.style.animationPlayState, 'running',
'Animation.effect.target.style.animationPlayState should be ' +
'"running" when an animation is initially created');
// XXX Ideally we would have a test to check the ready Promise is initially
// unresolved, but currently there is no Web API to do that. Waiting for the
// ready Promise with a timeout doesn't work because the resolved callback
// will be called (async) regardless of whether the Promise was resolved in
// the past or is resolved in the future.
// So that animation is running instead of paused when we set currentTime:
animation.startTime = animation.timeline.currentTime;
assert_approx_equals(animation.currentTime, 0, 0.0001, // rounding error
'Check setting of currentTime actually works');
checkStateOnSettingCurrentTimeToZero(animation);
}, 'Sanity test to check round-tripping assigning to new animation\'s ' +
'currentTime');
async_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
div.style.animation = ANIM_PROPERTY_VAL;
var animation = div.getAnimations()[0];
animation.ready.then(t.step_func(function() {
checkStateOnReadyPromiseResolved(animation);
animation.currentTime =
currentTimeForStartOfActiveInterval(animation.timeline);
return eventWatcher.wait_for('animationstart');
})).then(t.step_func(function() {
checkStateAtActiveIntervalStartTime(animation);
animation.currentTime =
currentTimeForFiftyPercentThroughActiveInterval(animation.timeline);
checkStateAtFiftyPctOfActiveInterval(animation);
animation.currentTime =
currentTimeForEndOfActiveInterval(animation.timeline);
return eventWatcher.wait_for('animationend');
})).then(t.step_func(function() {
checkStateAtActiveIntervalEndTime(animation);
})).catch(t.step_func(function(reason) {
assert_unreached(reason);
})).then(function() {
t.done();
});
}, 'Skipping forward through animation');
async_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
div.style.animation = ANIM_PROPERTY_VAL;
var animation = div.getAnimations()[0];
// So that animation is running instead of paused when we set currentTime:
animation.startTime = animation.timeline.currentTime;
animation.currentTime = currentTimeForEndOfActiveInterval(animation.timeline);
var previousTimelineTime = animation.timeline.currentTime;
// Skipping over the active interval will dispatch an 'animationstart' then
// an 'animationend' event. We need to wait for these events before we start
// testing going backwards since EventWatcher will fail the test if it gets
// an event that we haven't told it about.
eventWatcher.wait_for(['animationstart',
'animationend']).then(t.step_func(function() {
assert_true(document.timeline.currentTime - previousTimelineTime <
ANIM_DUR_MS,
'Sanity check that seeking worked rather than the events ' +
'firing after normal playback through the very long ' +
'animation duration');
// Now we can start the tests for skipping backwards, but first we check
// that after the events we're still in the same end time state:
checkStateAtActiveIntervalEndTime(animation);
animation.currentTime =
currentTimeForFiftyPercentThroughActiveInterval(animation.timeline);
// Despite going backwards from after the end of the animation (to being
// in the active interval), we now expect an 'animationstart' event
// because the animation should go from being inactive to active.
//
// Calling checkStateAtFiftyPctOfActiveInterval will check computed style,
// causing computed style to be updated and the 'animationstart' event to
// be dispatched synchronously. We need to call wait_for first
// otherwise eventWatcher will assert that the event was unexpected.
var promise = eventWatcher.wait_for('animationstart');
checkStateAtFiftyPctOfActiveInterval(animation);
return promise;
})).then(t.step_func(function() {
animation.currentTime =
currentTimeForStartOfActiveInterval(animation.timeline);
checkStateAtActiveIntervalStartTime(animation);
animation.currentTime = 0;
// Despite going backwards from just after the active interval starts to
// the animation start time, we now expect an animationend event
// because we went from inside to outside the active interval.
return eventWatcher.wait_for('animationend');
})).then(t.step_func(function() {
checkStateOnReadyPromiseResolved(animation);
})).catch(t.step_func(function(reason) {
assert_unreached(reason);
})).then(function() {
t.done();
});
// This must come after we've set up the Promise chain, since requesting
// computed style will force events to be dispatched.
// XXX For some reason this fails occasionally (either the animation.playState
// check or the marginLeft check).
//checkStateAtActiveIntervalEndTime(animation);
}, 'Skipping backwards through animation');
// Next we have multiple tests to check that redundant currentTime changes do
// NOT dispatch events. It's impossible to distinguish between events not being
// dispatched and events just taking an incredibly long time to dispatch
// without waiting an infinitely long time. Obviously we don't want to do that
// (block this test from finishing forever), so instead we just listen for
// events until two animation frames (i.e. requestAnimationFrame callbacks)
// have happened, then assume that no events will ever be dispatched for the
// redundant changes if no events were detected in that time.
async_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
div.style.animation = ANIM_PROPERTY_VAL;
var animation = div.getAnimations()[0];
animation.currentTime = currentTimeForActivePhase(animation.timeline);
animation.currentTime = currentTimeForBeforePhase(animation.timeline);
waitForAnimationFrames(2).then(function() {
t.done();
});
}, 'Redundant change, before -> active, then back');
async_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
div.style.animation = ANIM_PROPERTY_VAL;
var animation = div.getAnimations()[0];
animation.currentTime = currentTimeForAfterPhase(animation.timeline);
animation.currentTime = currentTimeForBeforePhase(animation.timeline);
waitForAnimationFrames(2).then(function() {
t.done();
});
}, 'Redundant change, before -> after, then back');
async_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
div.style.animation = ANIM_PROPERTY_VAL;
var animation = div.getAnimations()[0];
eventWatcher.wait_for('animationstart').then(function() {
animation.currentTime = currentTimeForBeforePhase(animation.timeline);
animation.currentTime = currentTimeForActivePhase(animation.timeline);
waitForAnimationFrames(2).then(function() {
t.done();
});
});
// get us into the initial state:
animation.currentTime = currentTimeForActivePhase(animation.timeline);
}, 'Redundant change, active -> before, then back');
async_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
div.style.animation = ANIM_PROPERTY_VAL;
var animation = div.getAnimations()[0];
eventWatcher.wait_for('animationstart').then(function() {
animation.currentTime = currentTimeForAfterPhase(animation.timeline);
animation.currentTime = currentTimeForActivePhase(animation.timeline);
waitForAnimationFrames(2).then(function() {
t.done();
});
});
// get us into the initial state:
animation.currentTime = currentTimeForActivePhase(animation.timeline);
}, 'Redundant change, active -> after, then back');
async_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
div.style.animation = ANIM_PROPERTY_VAL;
var animation = div.getAnimations()[0];
eventWatcher.wait_for(['animationstart',
'animationend']).then(function() {
animation.currentTime = currentTimeForBeforePhase(animation.timeline);
animation.currentTime = currentTimeForAfterPhase(animation.timeline);
waitForAnimationFrames(2).then(function() {
t.done();
});
});
// get us into the initial state:
animation.currentTime = currentTimeForAfterPhase(animation.timeline);
}, 'Redundant change, after -> before, then back');
async_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
div.style.animation = ANIM_PROPERTY_VAL;
var animation = div.getAnimations()[0];
eventWatcher.wait_for(['animationstart',
'animationend']).then(function() {
animation.currentTime = currentTimeForActivePhase(animation.timeline);
animation.currentTime = currentTimeForAfterPhase(animation.timeline);
waitForAnimationFrames(2).then(function() {
t.done();
});
});
// get us into the initial state:
animation.currentTime = currentTimeForAfterPhase(animation.timeline);
}, 'Redundant change, after -> active, then back');
async_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
div.style.animation = ANIM_PROPERTY_VAL;
var animation = div.getAnimations()[0];
animation.ready.then(t.step_func(function() {
var exception;
try {
animation.currentTime = null;
} catch (e) {
exception = e;
}
assert_equals(exception.name, 'TypeError',
'Expect TypeError exception on trying to set ' +
'Animation.currentTime to null');
})).catch(t.step_func(function(reason) {
assert_unreached(reason);
})).then(function() {
t.done();
});
}, 'Setting currentTime to null');
async_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
div.style.animation = 'anim 100s';
var animation = div.getAnimations()[0];
var pauseTime;
animation.ready.then(t.step_func(function() {
assert_not_equals(animation.currentTime, null,
'Animation.currentTime not null on ready Promise resolve');
animation.pause();
return animation.ready;
})).then(t.step_func(function() {
pauseTime = animation.currentTime;
return waitForFrame();
})).then(t.step_func(function() {
assert_equals(animation.currentTime, pauseTime,
'Animation.currentTime is unchanged after pausing');
})).catch(t.step_func(function(reason) {
assert_unreached(reason);
})).then(function() {
t.done();
});
}, 'Animation.currentTime after pausing');
async_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
div.style.animation = ANIM_PROPERTY_VAL;
var animation = div.getAnimations()[0];
animation.ready.then(function() {
// just before animation ends:
animation.currentTime = ANIM_DELAY_MS + ANIM_DUR_MS - 1;
return waitForAnimationFrames(2);
}).then(t.step_func(function() {
assert_equals(animation.currentTime, ANIM_DELAY_MS + ANIM_DUR_MS,
'Animation.currentTime should not continue to increase after the ' +
'animation has finished');
t.done();
}));
}, 'Animation.currentTime clamping');
async_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
div.style.animation = ANIM_PROPERTY_VAL;
var animation = div.getAnimations()[0];
animation.ready.then(function() {
// play backwards:
animation.playbackRate = -1;
// just before animation ends (at the "start"):
animation.currentTime = 1;
return waitForAnimationFrames(2);
}).then(t.step_func(function() {
assert_equals(animation.currentTime, 0,
'Animation.currentTime should not continue to decrease after an ' +
'animation running in reverse has finished and currentTime is zero');
t.done();
}));
}, 'Animation.currentTime clamping for reversed animation');
test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
div.style.animation = 'anim 100s';
var animation = div.getAnimations()[0];
animation.cancel();
assert_equals(animation.currentTime, null,
'The currentTime of a cancelled animation should be null');
}, 'Animation.currentTime after cancelling');
async_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
div.style.animation = 'anim 100s';
var animation = div.getAnimations()[0];
animation.ready.then(t.step_func(function() {
animation.finish();
// Initiate a pause then abort it
animation.pause();
animation.play();
// Wait to return to running state
return animation.ready;
})).then(t.step_func(function() {
assert_true(animation.currentTime < 100 * 1000,
'After aborting a pause when finished, the currentTime should'
+ ' jump back towards the start of the animation');
t.done();
}));
}, 'After aborting a pause when finished, the call to play() should rewind'
+ ' the current time');
done();
</script>
</body>
</html>