dom/animation/test/css-animations/file_animation-starttime.html
author emi suzuki <suzuming@gmail.com>
Sat, 26 Mar 2016 17:00:51 +0900
changeset 290569 feb85260ffb750c7e0a3096e1ffd7134249d4e67
parent 241985 1103c98d1a93d2c0fcdfd3e2b5b8fcd8c4370565
permissions -rw-r--r--
Bug 1259883 - Enable a test for player finishing behavior in file_animation-starttime.html r=hiro MozReview-Commit-ID: 1GPW9UsEK5E

<!doctype html>
<html>
  <head>
    <meta charset=utf-8>
    <title>Tests for the effect of setting a CSS animation's
           Animation.startTime</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 startTime 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 startTimeForBeforePhase(timeline) {
  return timeline.currentTime - ANIM_DELAY_MS / 2;
}
function startTimeForActivePhase(timeline) {
  return timeline.currentTime - ANIM_DELAY_MS - ANIM_DUR_MS / 2;
}
function startTimeForAfterPhase(timeline) {
  return timeline.currentTime - ANIM_DELAY_MS - ANIM_DUR_MS - ANIM_DELAY_MS / 2;
}
function startTimeForStartOfActiveInterval(timeline) {
  return timeline.currentTime - ANIM_DELAY_MS;
}
function startTimeForFiftyPercentThroughActiveInterval(timeline) {
  return timeline.currentTime - ANIM_DELAY_MS - ANIM_DUR_MS * 0.5;
}
function startTimeForEndOfActiveInterval(timeline) {
  return timeline.currentTime - 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 the ready Promise's callbacks should happen
function checkStateOnReadyPromiseResolved(animation)
{
  assert_less_than_equal(animation.startTime, animation.timeline.currentTime,
    'Animation.startTime should be less than 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 startTime is set to the time the active interval starts.
function checkStateAtActiveIntervalStartTime(animation)
{
  // We don't test animation.startTime 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.startTime 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 startTime is set to the time the active interval ends.
function checkStateAtActiveIntervalEndTime(animation)
{
  // We don't test animation.startTime 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, { 'style': 'animation: anim 100s' });
  var animation = div.getAnimations()[0];
  assert_equals(animation.startTime, null, 'startTime is unresolved');
}, 'startTime of a newly created (play-pending) animation is unresolved');

test(function(t)
{
  var div = addDiv(t, { 'style': 'animation: anim 100s paused' });
  var animation = div.getAnimations()[0];
  assert_equals(animation.startTime, null, 'startTime is unresolved');
}, 'startTime of a newly created (pause-pending) animation is unresolved');

async_test(function(t)
{
  var div = addDiv(t, { 'style': 'animation: anim 100s' });
  var animation = div.getAnimations()[0];
  animation.ready.then(t.step_func(function() {
    assert_true(animation.startTime > 0,
                'startTime is resolved when running');
    t.done();
  }));
}, 'startTime is resolved when running');

async_test(function(t)
{
  var div = addDiv(t, { 'style': 'animation: anim 100s paused' });
  var animation = div.getAnimations()[0];
  animation.ready.then(t.step_func(function() {
    assert_equals(animation.startTime, null,
                  'startTime is unresolved when paused');
    t.done();
  }));
}, 'startTime is unresolved when paused');

async_test(function(t)
{
  var div = addDiv(t, { 'style': 'animation: anim 100s' });
  var animation = div.getAnimations()[0];
  animation.ready.then(t.step_func(function() {
    div.style.animationPlayState = 'paused';
    getComputedStyle(div).animationPlayState;
    assert_not_equals(animation.startTime, null,
                      'startTime is resolved when pause-pending');

    div.style.animationPlayState = 'running';
    getComputedStyle(div).animationPlayState;
    assert_not_equals(animation.startTime, null,
                      'startTime is preserved when a pause is aborted');
    t.done();
  }));
}, 'startTime while pause-pending and play-pending');

async_test(function(t) {
  var div = addDiv(t, { 'style': 'animation: anim 100s' });
  var animation = div.getAnimations()[0];
  // Seek to end to put us in the finished state
  animation.currentTime = 100 * 1000;
  animation.ready.then(t.step_func(function() {
    // Call play() which puts us back in the running state
    animation.play();
    assert_equals(animation.startTime, null, 'startTime is unresolved');
    t.done();
  }));
}, 'startTime while play-pending from finished state');

test(function(t) {
  var div = addDiv(t, { 'style': 'animation: anim 100s' });
  var animation = div.getAnimations()[0];
  animation.finish();
  // Call play() which puts us back in the running state
  animation.play();
  assert_equals(animation.startTime, null, 'startTime is unresolved');
}, 'startTime while play-pending from finished state using finish()');

async_test(function(t) {
  var div = addDiv(t, { style: 'animation: anim 1000s' });
  var animation = div.getAnimations()[0];

  assert_equals(animation.startTime, null, 'The initial startTime is null');
  var initialTimelineTime = document.timeline.currentTime;

  animation.ready.then(t.step_func(function() {
    assert_true(animation.startTime > initialTimelineTime,
                'After the animation has started, startTime is greater than ' +
                'the time when it was started');
    var startTimeBeforePausing = animation.startTime;

    div.style.animationPlayState = 'paused';
    // Flush styles just in case querying animation.startTime doesn't flush
    // styles (which would be a bug in of itself and could mask a further bug
    // by causing startTime to appear to not change).
    getComputedStyle(div).animationPlayState;

    assert_equals(animation.startTime, startTimeBeforePausing,
                  'The startTime does not change when pausing-pending');
    return animation.ready;
  })).then(t.step_func(function() {
    assert_equals(animation.startTime, null,
                  'After actually pausing, the startTime of an animation ' +
                  'is null');
    t.done();
  }));
}, 'Pausing should make the startTime become null');

test(function(t)
{
  var div = addDiv(t, {'class': 'animated-div'});
  div.style.animation = ANIM_PROPERTY_VAL;

  var animation = div.getAnimations()[0];
  var currentTime = animation.timeline.currentTime;
  animation.startTime = currentTime;
  assert_approx_equals(animation.startTime, currentTime, 0.0001, // rounding error
    'Check setting of startTime actually works');
}, 'Sanity test to check round-tripping assigning to a new animation\'s ' +
   'startTime');


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.startTime = startTimeForStartOfActiveInterval(animation.timeline);
    return eventWatcher.wait_for('animationstart');
  })).then(t.step_func(function() {
    checkStateAtActiveIntervalStartTime(animation);

    animation.startTime =
      startTimeForFiftyPercentThroughActiveInterval(animation.timeline);
    checkStateAtFiftyPctOfActiveInterval(animation);

    animation.startTime = startTimeForEndOfActiveInterval(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];

  animation.startTime = startTimeForEndOfActiveInterval(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.startTime =
      startTimeForFiftyPercentThroughActiveInterval(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.startTime = startTimeForStartOfActiveInterval(animation.timeline);
    checkStateAtActiveIntervalStartTime(animation);

    animation.startTime = animation.timeline.currentTime;
    // 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 startTime 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.startTime = startTimeForActivePhase(animation.timeline);
  animation.startTime = startTimeForBeforePhase(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.startTime = startTimeForAfterPhase(animation.timeline);
  animation.startTime = startTimeForBeforePhase(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.startTime = startTimeForBeforePhase(animation.timeline);
    animation.startTime = startTimeForActivePhase(animation.timeline);

    waitForAnimationFrames(2).then(function() {
      t.done();
    });
  });
  // get us into the initial state:
  animation.startTime = startTimeForActivePhase(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.startTime = startTimeForAfterPhase(animation.timeline);
    animation.startTime = startTimeForActivePhase(animation.timeline);

    waitForAnimationFrames(2).then(function() {
      t.done();
    });
  });
  // get us into the initial state:
  animation.startTime = startTimeForActivePhase(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.startTime = startTimeForBeforePhase(animation.timeline);
    animation.startTime = startTimeForAfterPhase(animation.timeline);

    waitForAnimationFrames(2).then(function() {
      t.done();
    });
  });
  // get us into the initial state:
  animation.startTime = startTimeForAfterPhase(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.startTime = startTimeForActivePhase(animation.timeline);
    animation.startTime = startTimeForAfterPhase(animation.timeline);

    waitForAnimationFrames(2).then(function() {
      t.done();
    });
  });
  // get us into the initial state:
  animation.startTime = startTimeForAfterPhase(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];

  var storedCurrentTime;

  animation.ready.then(t.step_func(function() {
    storedCurrentTime = animation.currentTime;
    animation.startTime = null;
    return animation.ready;
  })).catch(t.step_func(function(reason) {
    assert_unreached(reason);
  })).then(t.step_func(function() {
    assert_equals(animation.currentTime, storedCurrentTime,
      'Test that hold time is correct');
    t.done();
  }));
}, 'Setting startTime to null');


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() {
    var savedStartTime = animation.startTime;

    assert_not_equals(animation.startTime, null,
      'Animation.startTime not null on ready Promise resolve');

    animation.pause();
    return animation.ready;
  })).then(t.step_func(function() {
    assert_equals(animation.startTime, null,
      'Animation.startTime is null after paused');
    assert_equals(animation.playState, 'paused',
      'Animation.playState is "paused" after pause() call');
  })).catch(t.step_func(function(reason) {
    assert_unreached(reason);
  })).then(function() {
    t.done();
  });
}, 'Animation.startTime after pausing');

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.cancel();
    assert_equals(animation.startTime, null,
                  'The startTime of a cancelled animation should be null');
    t.done();
  }));
}, 'Animation.startTime after cancelling');

done();
    </script>
  </body>
</html>