<!DOCTYPE html>
<meta charset=utf-8>
<title>Verify timeline time, animation time, effect time, and effect progress for all timeline states: before start, at start, in range, at end, after end while using various effect delay values</title>
<meta name="timeout" content="long">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/web-animations/testcommon.js"></script>
<script src="testcommon.js"></script>
<style>
.scroller {
overflow: hidden;
height: 200px;
width: 200px;
}
.contents {
/* Make scroll range 1000 to simplify the math and avoid rounding errors */
height: 1200px;
width: 100%;
}
</style>
<div id="log"></div>
<script>
'use strict';
// Note: effects are scaled to fill the timeline.
// Each entry is [[test input], [test expectations]]
// test input = ["description", delay, end_delay, scroll percent]
// test expectations = [timeline time, animation current time,
// effect local time, effect progress, effect phase,
// opacity]
/* All interesting transitions:
at timeline start
before effect delay
at effect start
in active range
at effect end
after effect end
at timeline end
*/
const test_cases = [
// Case 1: No delays.
// Boundary at end of active phase is inclusive.
[
["at start", 0, 0, 0],
[0, 0, 0, 0, "active", 0.3]
],
[
["in active range", 0, 0, 0.50],
[50, 50, 50, 0.5, "active", 0.5]
],
[
["at effect end time", 0, 0, 1.0],
[100, 100, 100, 1.0, "active", 0.7]
],
// Case 2: Positive start delay and no end delay.
// Boundary at end of active phase is inclusive.
[
["at timeline start", 500, 0, 0],
[0, 0, 0, null, "before", 1]
],
[
["before start delay", 500, 0, 0.25],
[25, 25, 25, null, "before", 1]
],
[
["at start delay", 500, 0, 0.5],
[50, 50, 50, 0, "active", 0.3]
],
[
["in active range", 500, 0, 0.75],
[75, 75, 75, 0.5, "active", 0.5]
],
[
["at effect end time", 500, 0, 1.0],
[100, 100, 100, 1.0, "active", 0.7]
],
// case 3: No start delay, Positive end delay.
// Boundary at end of active phase is exclusive.
[
["at timeline start", 0, 500, 0],
[0, 0, 0, 0, "active", 0.3]
],
[
["in active range", 0, 500, 0.25],
[25, 25, 25, 0.5, "active", 0.5]
],
[
["at effect end time", 0, 500, 0.5],
[50, 50, 50, null, "after", 1.0]
],
[
["after effect end time", 0, 500, 0.75],
[75, 75, 75, null, "after", 1.0]
],
[
["at timeline boundary", 0, 500, 1.0],
[100, 100, 100, null, "after", 1.0]
],
// case 4: Positive start and end delays.
// Boundary at end of active phase is exclusive.
[
["at timeline start", 250, 250, 0],
[0, 0, 0, null, "before", 1]
],
[
["before start delay", 250, 250, 0.1],
[10, 10, 10, null, "before", 1]
],
[
["at start delay", 250, 250, 0.25],
[25, 25, 25, 0, "active", 0.3]
],
[
["in active range", 250, 250, 0.5],
[50, 50, 50, 0.5, "active", 0.5]
],
[
["at effect end time", 250, 250, 0.75],
[75, 75, 75, null, "after", 1.0]
],
[
["after effect end time", 250, 250, 0.9],
[90, 90, 90, null, "after", 1.0]
],
[
["at timeline boundary", 250, 250, 1.0],
[100, 100, 100, null, "after", 1.0]
],
// Case 5: Negative start and end delays.
// Effect boundaries are not reachable.
[
["at timeline start", -125, -125, 0],
[0, 0, 0, 0.25, "active", 0.4]
],
[
["in active range", -125, -125, 0.5],
[50, 50, 50, 0.5, "active", 0.5]
],
[
["at timeline end", -125, -125, 1.0],
[100, 100, 100, 0.75, "active", 0.6]
]
];
for (const test_case of test_cases) {
const [inputs, expected] = test_case;
const [test_name, delay, end_delay, scroll_percentage] = inputs;
const description = `Current times and effect phase ${test_name} when` +
` delay = ${delay} and endDelay = ${end_delay} |`;
promise_test(
create_scroll_timeline_delay_test(
delay, end_delay, scroll_percentage, expected),
description);
}
function create_scroll_timeline_delay_test(
delay, end_delay, scroll_percentage, expected){
return async t => {
const target = createDiv(t);
const timeline = createScrollTimeline(t);
const effect = new KeyframeEffect(
target,
{
opacity: [0.3, 0.7]
},
{
duration: 500,
delay: delay,
endDelay: end_delay
}
);
const animation = new Animation(effect, timeline);
t.add_cleanup(() => {
animation.cancel();
});
const scroller = timeline.source;
const maxScroll = scroller.scrollHeight - scroller.clientHeight;
animation.play();
await animation.ready;
scroller.scrollTop = scroll_percentage * maxScroll;
// Wait for new animation frame which allows the timeline to compute
// new current time.
await waitForNextFrame();
const [expected_timeline_current_time,
expected_animation_current_time,
expected_effect_local_time,
expected_effect_progress,
expected_effect_phase,
expected_opacity] = expected;
assert_percents_equal(
animation.timeline.currentTime,
expected_timeline_current_time,
"timeline current time");
assert_percents_equal(
animation.currentTime,
expected_animation_current_time,
"animation current time");
assert_percents_equal(
animation.effect.getComputedTiming().localTime,
expected_effect_local_time,
"animation effect local time");
assert_approx_equals_or_null(
animation.effect.getComputedTiming().progress,
expected_effect_progress,
0.001,
"animation effect progress");
assert_phase(
animation, expected_effect_phase);
assert_approx_equals(
parseFloat(getComputedStyle(target).opacity), expected_opacity,
0.001,
'target opacity');
}
}
function createKeyframeEffectOpacity(test){
return new KeyframeEffect(
createDiv(test),
{
opacity: [0.3, 0.7]
},
{
duration: 1000
}
);
}
function verifyEffectBeforePhase(animation) {
// If currentTime is null, we are either idle, or running with an
// inactive timeline. Either way, the animation is not in effect and cannot
// be in the before phase.
assert_true(animation.currentTime != null,
'Animation is not in effect');
const fillMode = animation.effect.getTiming().fill;
animation.effect.updateTiming({ fill: 'none' });
// progress == null AND opacity == 1 implies we are in the effect before
// or after phase.
assert_equals(animation.effect.getComputedTiming().progress, null);
assert_equals(
window.getComputedStyle(animation.effect.target)
.getPropertyValue("opacity"),
"1");
// If the progress is no longer null after adding fill: backwards, then we
// are in the before phase.
animation.effect.updateTiming({ fill: 'backwards' });
assert_true(animation.effect.getComputedTiming().progress != null);
assert_equals(
window.getComputedStyle(animation.effect.target)
.getPropertyValue("opacity"),
"0.3");
// Reset fill mode to avoid side-effects.
animation.effect.updateTiming({ fill: fillMode });
}
function createScrollLinkedOpacityAnimationWithDelays(t) {
const animation = new Animation(
createKeyframeEffectOpacity(t),
createScrollTimeline(t)
);
t.add_cleanup(() => {
animation.cancel();
});
animation.effect.updateTiming({
duration: 1000,
delay: 500,
endDelay: 500
});
return animation;
}
promise_test(async t => {
const animation = createScrollLinkedOpacityAnimationWithDelays(t);
const scroller = animation.timeline.source;
const maxScroll = scroller.scrollHeight - scroller.clientHeight;
// scroll pos
// current time
// start time
// |
// |---- 25% before ----|---- 50% active ----|---- 25% after ----|
animation.play();
await animation.ready;
assert_percents_equal(animation.startTime, 0);
assert_phase(animation, 'before');
// start time scroll pos
// | current time
// | |
// |---- 25% before ----|---- 50% active ----|---- 25% after ----|
scroller.scrollTop = 0.5 * maxScroll;
await waitForNextFrame();
assert_phase(animation, 'active');
// start time scroll pos current time
// | | |
// |---- 25% before ----|---- 50% active ----|---- 25% after ----|
animation.playbackRate = 2;
assert_phase(animation, 'after');
// start time scroll pos current time
// | | |
// |---- 33.3% before ----|---- 66.7% active ---------------------|
animation.effect.updateTiming({ endDelay: 0 });
assert_phase(animation, 'active');
// scroll pos start time
// current time |
// | |
// |---- 33.3% before ----|---- 66.7% active ----------------------|
animation.playbackRate = -1;
assert_percents_equal(animation.startTime, 100);
assert_phase(animation, 'active');
// start time
// scroll pos current time
// | | |
// |---- 33.3% before ----|---- 66.7% active -----------------------|
animation.playbackRate = -2;
assert_phase(animation, 'active');
// current time start time
// | scroll pos
// | |
// |---- 33.3% before ----|---- 66.7% active -----------------------|
scroller.scrollTop = maxScroll;
await waitForNextFrame();
assert_phase(animation, 'before');
// current time start time
// | scroll pos
// | |
// |--------------------- 100% active -------------------------------|
animation.effect.updateTiming({ delay: 0 });
assert_phase(animation, 'active');
// Finally, switch to a document timeline. The before-active boundary
// becomes exclusive.
animation.timeline = document.timeline;
animation.currentTime = 0;
await waitForNextFrame();
assert_phase(animation, 'before');
}, 'Playback rate affects whether active phase boundary is inclusive.');
promise_test(async t => {
const animation = createScrollLinkedOpacityAnimationWithDelays(t);
const scroller = animation.timeline.source;
const maxScroll = scroller.scrollHeight - scroller.clientHeight;
animation.play();
await animation.ready;
verifyEffectBeforePhase(animation);
animation.pause();
await waitForNextFrame();
verifyEffectBeforePhase(animation);
animation.play();
await waitForNextFrame();
verifyEffectBeforePhase(animation);
}, 'Verify that (play -> pause -> play) doesn\'t change phase/progress.');
promise_test(async t => {
const animation = createScrollLinkedOpacityAnimationWithDelays(t);
const scroller = animation.timeline.source;
const maxScroll = scroller.scrollHeight - scroller.clientHeight;
animation.play();
await animation.ready;
verifyEffectBeforePhase(animation);
animation.pause();
await animation.ready;
verifyEffectBeforePhase(animation);
// Scrolling should not cause the animation effect to change.
scroller.scrollTop = 0.5 * maxScroll;
await waitForNextFrame();
// Check timeline phase
assert_percents_equal(animation.timeline.currentTime, 50);
assert_percents_equal(animation.currentTime, 0);
assert_percents_equal(animation.effect.getComputedTiming().localTime, 0,
"effect local time");
// Make sure the effect is still in the before phase even though the
// timeline is not.
verifyEffectBeforePhase(animation);
}, 'Pause in before phase, scroll timeline into active phase, animation ' +
'should remain in the before phase');
promise_test(async t => {
const animation = createScrollLinkedOpacityAnimationWithDelays(t);
const scroller = animation.timeline.source;
const maxScroll = scroller.scrollHeight - scroller.clientHeight;
animation.play();
await animation.ready;
verifyEffectBeforePhase(animation);
animation.pause();
await waitForNextFrame();
verifyEffectBeforePhase(animation);
// Setting the current time should force the animation into effect.
const expected_time = 50;
animation.currentTime = CSS.percent(expected_time);
await waitForNextFrame();
assert_percents_equal(animation.timeline.currentTime, 0);
assert_percents_equal(animation.currentTime, expected_time,
'Current time matches set value');
assert_percents_equal(
animation.effect.getComputedTiming().localTime,
expected_time, "Effect local time after setting animation.currentTime");
assert_equals(animation.effect.getComputedTiming().progress, 0.5,
"Progress after setting animation.currentTime");
assert_equals(
window.getComputedStyle(animation.effect.target)
.getPropertyValue("opacity"),
"0.5", "Opacity after setting animation.currentTime");
// Scrolling should not cause the animation effect to change since
// paused.
scroller.scrollTop = 0.75 * maxScroll; // scroll so that timeline is 75%
await waitForNextFrame();
assert_percents_equal(animation.timeline.currentTime, 75);
// animation and effect timings are unchanged.
assert_percents_equal(animation.currentTime, expected_time,
"Current time after scrolling while paused");
assert_percents_equal(
animation.effect.getComputedTiming().localTime,
expected_time,
"Effect local time after scrolling while paused");
assert_equals(animation.effect.getComputedTiming().progress, 0.5,
"Progress after scrolling while paused");
assert_equals(
window.getComputedStyle(animation.effect.target)
.getPropertyValue("opacity"),
"0.5", "Opacity after scrolling while paused");
}, 'Pause in before phase, set animation current time to be in active ' +
'range, animation should become active. Scrolling should have no effect.');
promise_test(async t => {
const animation = createScrollLinkedOpacityAnimationWithDelays(t);
const scroller = animation.timeline.source;
const maxScroll = scroller.scrollHeight - scroller.clientHeight;
animation.play();
await animation.ready;
// Causes the timeline to be inactive
scroller.style.overflow = "visible";
await waitForNextFrame();
await waitForNextFrame();
// Verify that he timeline is inactive
assert_equals(animation.timeline.currentTime, null,
"Timeline is inactive");
assert_equals(
animation.currentTime, null,
"Current time for running animation with an inactive timeline");
assert_equals(animation.effect.getComputedTiming().localTime, null,
"effect local time with inactive timeline");
// Setting the current time while timeline is inactive should pause the
// animation at the specified time.
animation.currentTime = CSS.percent(50);
await waitForNextFrame();
await waitForNextFrame();
// Verify that animation currentTime is properly set despite the inactive
// timeline.
assert_equals(animation.timeline.currentTime, null);
assert_percents_equal(animation.currentTime, 50);
assert_percents_equal(animation.effect.getComputedTiming().localTime, 50,
"effect local time after setting animation current time");
// Check effect phase
// progress == 0.5 AND opacity == 0.5 shows we are in the effect active
// phase.
assert_equals(animation.effect.getComputedTiming().progress, 0.5,
"effect progress");
assert_equals(
window.getComputedStyle(animation.effect.target)
.getPropertyValue("opacity"),
"0.5",
"effect opacity after setting animation current time");
}, 'Make scroller inactive, then set current time to an in range time');
promise_test(async t => {
const animation = createScrollLinkedOpacityAnimationWithDelays(t);
const scroller = animation.timeline.source;
const maxScroll = scroller.scrollHeight - scroller.clientHeight;
scroller.scrollTop = 0.5 * maxScroll;
// Update timeline.currentTime.
await waitForNextFrame();
animation.pause();
await animation.ready;
// verify effect is applied.
const expected_progress = 0.5;
assert_equals(
animation.effect.getComputedTiming().progress,
expected_progress,
"Verify effect progress after pausing.");
// cause the timeline to become inactive
scroller.style.overflow = 'visible';
await waitForAnimationFrames(2);
assert_equals(animation.timeline.currentTime, null,
'Sanity check the timeline is inactive.');
assert_equals(
animation.effect.getComputedTiming().progress,
expected_progress,
"Verify effect progress after the timeline goes inactive.");
}, 'Animation effect is still applied after pausing and making timeline ' +
'inactive.');
promise_test(async t => {
const animation = createScrollLinkedOpacityAnimationWithDelays(t);
const scroller = animation.timeline.source;
const maxScroll = scroller.scrollHeight - scroller.clientHeight;
animation.play();
await animation.ready;
// cause the timeline to become inactive
scroller.style.overflow = 'visible';
scroller.scrollTop;
animation.pause();
}, 'Make timeline inactive, force style update then pause the animation. ' +
'No crashing indicates test success.');
</script>