diff options
Diffstat (limited to 'testing/web-platform/tests/web-animations/timing-model')
40 files changed, 6975 insertions, 0 deletions
diff --git a/testing/web-platform/tests/web-animations/timing-model/animation-effects/active-time.html b/testing/web-platform/tests/web-animations/timing-model/animation-effects/active-time.html new file mode 100644 index 0000000000..a2feb2323c --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animation-effects/active-time.html @@ -0,0 +1,141 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Active time</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#calculating-the-active-time"> +<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(t => { + const tests = [ { fill: 'none', progress: null }, + { fill: 'backwards', progress: 0 }, + { fill: 'forwards', progress: null }, + { fill: 'both', progress: 0 } ]; + for (const test of tests) { + const anim = createDiv(t).animate(null, { delay: 1, fill: test.fill }); + assert_equals(anim.effect.getComputedTiming().progress, test.progress, + `Progress in before phase when using '${test.fill}' fill`); + } +}, 'Active time in before phase'); + +test(t => { + const anim = createDiv(t).animate(null, 1000); + anim.currentTime = 500; + assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.5); +}, 'Active time in active phase and no start delay is the local time'); + +test(t => { + const anim = createDiv(t).animate(null, { duration: 1000, delay: 500 }); + anim.currentTime = 1000; + assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.5); +}, 'Active time in active phase and positive start delay is the local time' + + ' minus the start delay'); + +test(t => { + const anim = createDiv(t).animate(null, { duration: 1000, delay: -500 }); + assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.5); +}, 'Active time in active phase and negative start delay is the local time' + + ' minus the start delay'); + +test(t => { + const anim = createDiv(t).animate(null); + assert_equals(anim.effect.getComputedTiming().progress, null); +}, 'Active time in after phase with no fill is unresolved'); + +test(t => { + const anim = createDiv(t).animate(null, { fill: 'backwards' }); + assert_equals(anim.effect.getComputedTiming().progress, null); +}, 'Active time in after phase with backwards-only fill is unresolved'); + +test(t => { + const anim = createDiv(t).animate(null, { duration: 1000, + iterations: 2.3, + delay: 500, // Should have no effect + fill: 'forwards' }); + anim.finish(); + assert_equals(anim.effect.getComputedTiming().currentIteration, 2); + assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.3); +}, 'Active time in after phase with forwards fill is the active duration'); + +test(t => { + const anim = createDiv(t).animate(null, { duration: 0, + iterations: Infinity, + fill: 'forwards' }); + anim.finish(); + assert_equals(anim.effect.getComputedTiming().currentIteration, Infinity); + assert_equals(anim.effect.getComputedTiming().progress, 1); +}, 'Active time in after phase with forwards fill, zero-duration, and ' + + ' infinite iteration count is the active duration'); + +test(t => { + const anim = createDiv(t).animate(null, { duration: 1000, + iterations: 2.3, + delay: 500, + endDelay: 4000, + fill: 'forwards' }); + anim.finish(); + assert_equals(anim.effect.getComputedTiming().currentIteration, 2); + assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.3); +}, 'Active time in after phase with forwards fill and positive end delay' + + ' is the active duration'); + +test(t => { + const anim = createDiv(t).animate(null, { duration: 1000, + iterations: 2.3, + delay: 500, + endDelay: -800, + fill: 'forwards' }); + anim.finish(); + assert_equals(anim.effect.getComputedTiming().currentIteration, 1); + assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.5); +}, 'Active time in after phase with forwards fill and negative end delay' + + ' is the active duration + end delay'); + +test(t => { + const anim = createDiv(t).animate(null, { duration: 1000, + iterations: 2.3, + delay: 500, + endDelay: -2500, + fill: 'forwards' }); + anim.finish(); + assert_equals(anim.effect.getComputedTiming().currentIteration, 0); + assert_equals(anim.effect.getComputedTiming().progress, 0); +}, 'Active time in after phase with forwards fill and negative end delay' + + ' greater in magnitude than the active duration is zero'); + +test(t => { + const anim = createDiv(t).animate(null, { duration: 1000, + iterations: 2.3, + delay: 500, + endDelay: -4000, + fill: 'forwards' }); + anim.finish(); + assert_equals(anim.effect.getComputedTiming().currentIteration, 0); + assert_equals(anim.effect.getComputedTiming().progress, 0); +}, 'Active time in after phase with forwards fill and negative end delay' + + ' greater in magnitude than the sum of the active duration and start delay' + + ' is zero'); + +test(t => { + const anim = createDiv(t).animate(null, { duration: 1000, + iterations: 2.3, + delay: 500, + fill: 'both' }); + anim.finish(); + assert_equals(anim.effect.getComputedTiming().currentIteration, 2); + assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.3); +}, 'Active time in after phase with \'both\' fill is the active duration'); + +test(t => { + // Create an effect with a non-zero duration so we ensure we're not just + // testing the after-phase behavior. + const effect = new KeyframeEffect(null, null, 1); + assert_equals(effect.getComputedTiming().progress, null); +}, 'Active time when the local time is unresolved, is unresolved'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/timing-model/animation-effects/current-iteration.html b/testing/web-platform/tests/web-animations/timing-model/animation-effects/current-iteration.html new file mode 100644 index 0000000000..24464ce05f --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animation-effects/current-iteration.html @@ -0,0 +1,620 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Current iteration</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#current-iteration"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<script src="../../resources/effect-tests.js"></script> +<body> +<div id="log"></div> +<script> +'use strict'; + +function runTests(tests, description) { + for (const currentTest of tests) { + let testParams = Object.entries(currentTest.input) + .map(([attr, value]) => `${attr}:${value}`) + .join(' '); + if (currentTest.playbackRate !== undefined) { + testParams += ` playbackRate:${currentTest.playbackRate}`; + } + + test(t => { + const div = createDiv(t); + const anim = div.animate({}, currentTest.input); + if (currentTest.playbackRate !== undefined) { + anim.playbackRate = currentTest.playbackRate; + } + + assert_computed_timing_for_each_phase( + anim, + 'currentIteration', + { before: currentTest.before, + activeBoundary: currentTest.active, + after: currentTest.after }, + ); + }, `${description}: ${testParams}`); + } +} + +async_test(t => { + const div = createDiv(t); + const anim = div.animate({ opacity: [ 0, 1 ] }, { delay: 1 }); + assert_equals(anim.effect.getComputedTiming().currentIteration, null); + anim.finished.then(t.step_func(() => { + assert_equals(anim.effect.getComputedTiming().currentIteration, null); + t.done(); + })); +}, 'Test currentIteration during before and after phase when fill is none'); + + +// -------------------------------------------------------------------- +// +// Zero iteration duration tests +// +// -------------------------------------------------------------------- + +runTests([ + { + input: { iterations: 0, + iterationStart: 0, + duration: 0, + delay: 1, + fill: 'both' }, + before: 0, + after: 0 + }, + + { + input: { iterations: 0, + iterationStart: 0, + duration: 100, + delay: 1, + fill: 'both' }, + before: 0, + after: 0 + }, + + { + input: { iterations: 0, + iterationStart: 0, + duration: Infinity, + delay: 1, + fill: 'both' }, + before: 0, + after: 0 + }, + + { + input: { iterations: 0, + iterationStart: 2.5, + duration: 0, + delay: 1, + fill: 'both' }, + before: 2, + after: 2 + }, + + { + input: { iterations: 0, + iterationStart: 2.5, + duration: 100, + delay: 1, + fill: 'both' }, + before: 2, + after: 2 + }, + + { + input: { iterations: 0, + iterationStart: 2.5, + duration: Infinity, + delay: 1, + fill: 'both' }, + before: 2, + after: 2 + }, + + { + input: { iterations: 0, + iterationStart: 3, + duration: 0, + delay: 1, + fill: 'both' }, + before: 3, + after: 3 + }, + + { + input: { iterations: 0, + iterationStart: 3, + duration: 100, + delay: 1, + fill: 'both' }, + before: 3, + after: 3 + }, + + { + input: { iterations: 0, + iterationStart: 3, + duration: Infinity, + delay: 1, + fill: 'both' }, + before: 3, + after: 3 + } +], 'Test zero iterations'); + + +// -------------------------------------------------------------------- +// +// Tests where the iteration count is an integer +// +// -------------------------------------------------------------------- + +runTests([ + { + input: { iterations: 3, + iterationStart: 0, + duration: 0, + delay: 1, + fill: 'both' }, + before: 0, + after: 2 + }, + + { + input: { iterations: 3, + iterationStart: 0, + duration: 100, + delay: 1, + fill: 'both' }, + before: 0, + active: 0, + after: 2 + }, + + { + input: { iterations: 3, + iterationStart: 0, + duration: Infinity, + delay: 1, + fill: 'both' }, + before: 0, + active: 0 + }, + + { + input: { iterations: 3, + iterationStart: 2.5, + duration: 0, + delay: 1, + fill: 'both' }, + before: 2, + after: 5 + }, + + { + input: { iterations: 3, + iterationStart: 2.5, + duration: 100, + delay: 1, + fill: 'both' }, + before: 2, + active: 2, + after: 5 + }, + + { + input: { iterations: 3, + iterationStart: 2.5, + duration: Infinity, + delay: 1, + fill: 'both' }, + before: 2, + active: 2 + }, + + { + input: { iterations: 3, + iterationStart: 3, + duration: 0, + delay: 1, + fill: 'both' }, + before: 3, + after: 5 + }, + + { + input: { iterations: 3, + iterationStart: 3, + duration: 100, + delay: 1, + fill: 'both' }, + before: 3, + active: 3, + after: 5 + }, + + { + input: { iterations: 3, + iterationStart: 3, + duration: Infinity, + delay: 1, + fill: 'both' }, + before: 3, + active: 3 + } +], 'Test integer iterations'); + + +// -------------------------------------------------------------------- +// +// Tests where the iteration count is a fraction +// +// -------------------------------------------------------------------- + +runTests([ + { + input: { iterations: 3.5, + iterationStart: 0, + duration: 0, + delay: 1, + fill: 'both' }, + before: 0, + after: 3 + }, + + { + input: { iterations: 3.5, + iterationStart: 0, + duration: 100, + delay: 1, + fill: 'both' }, + before: 0, + active: 0, + after: 3 + }, + + { + input: { iterations: 3.5, + iterationStart: 0, + duration: Infinity, + delay: 1, + fill: 'both' }, + before: 0, + active: 0 + }, + + { + input: { iterations: 3.5, + iterationStart: 2.5, + duration: 0, + delay: 1, + fill: 'both' }, + before: 2, + after: 5 + }, + + { + input: { iterations: 3.5, + iterationStart: 2.5, + duration: 100, + delay: 1, + fill: 'both' }, + before: 2, + active: 2, + after: 5 + }, + + { + input: { iterations: 3.5, + iterationStart: 2.5, + duration: Infinity, + delay: 1, + fill: 'both' }, + before: 2, + active: 2 + }, + + { + input: { iterations: 3.5, + iterationStart: 3, + duration: 0, + delay: 1, + fill: 'both' }, + before: 3, + after: 6 + }, + + { + input: { iterations: 3.5, + iterationStart: 3, + duration: 100, + delay: 1, + fill: 'both' }, + before: 3, + active: 3, + after: 6 + }, + + { + input: { iterations: 3.5, + iterationStart: 3, + duration: Infinity, + delay: 1, + fill: 'both' }, + before: 3, + active: 3 + } +], 'Test fractional iterations'); + + +// -------------------------------------------------------------------- +// +// Tests where the iteration count is Infinity +// +// -------------------------------------------------------------------- + +runTests([ + { + input: { iterations: Infinity, + iterationStart: 0, + duration: 0, + delay: 1, + fill: 'both' }, + before: 0, + after: Infinity + }, + + { + input: { iterations: Infinity, + iterationStart: 0, + duration: 100, + delay: 1, + fill: 'both' }, + before: 0, + active: 0 + }, + + { + input: { iterations: Infinity, + iterationStart: 0, + duration: Infinity, + delay: 1, + fill: 'both' }, + before: 0, + active: 0 + }, + + { + input: { iterations: Infinity, + iterationStart: 2.5, + duration: 0, + delay: 1, + fill: 'both' }, + before: 2, + after: Infinity + }, + + { + input: { iterations: Infinity, + iterationStart: 2.5, + duration: 100, + delay: 1, + fill: 'both' }, + before: 2, + active: 2 + }, + + { + input: { iterations: Infinity, + iterationStart: 2.5, + duration: Infinity, + delay: 1, + fill: 'both' }, + before: 2, + active: 2 + }, + + { + input: { iterations: Infinity, + iterationStart: 3, + duration: 0, + delay: 1, + fill: 'both' }, + before: 3, + after: Infinity + }, + + { + input: { iterations: Infinity, + iterationStart: 3, + duration: 100, + delay: 1, + fill: 'both' }, + before: 3, + active: 3 + }, + + { + input: { iterations: Infinity, + iterationStart: 3, + duration: Infinity, + delay: 1, + fill: 'both' }, + before: 3, + active: 3 + } +], 'Test infinity iterations'); + + +// -------------------------------------------------------------------- +// +// End delay tests +// +// -------------------------------------------------------------------- + +runTests([ + { + input: { duration: 100, + delay: 1, + fill: 'both', + endDelay: 50 }, + before: 0, + active: 0, + after: 0 + }, + + { + input: { duration: 100, + delay: 1, + fill: 'both', + endDelay: -50 }, + before: 0, + active: 0, + after: 0 + }, + + { + input: { duration: 100, + delay: 1, + fill: 'both', + endDelay: -100 }, + before: 0, + active: 0, + after: 0 + }, + + { + input: { duration: 100, + delay: 1, + fill: 'both', + endDelay: -200 }, + before: 0, + active: 0, + after: 0 + }, + + { + input: { iterationStart: 0.5, + duration: 100, + delay: 1, + fill: 'both', + endDelay: 50 }, + before: 0, + active: 0, + after: 1 + }, + + { + input: { iterationStart: 0.5, + duration: 100, + delay: 1, + fill: 'both', + endDelay: -50 }, + before: 0, + active: 0, + after: 1 + }, + + { + input: { iterationStart: 0.5, + duration: 100, + delay: 1, + fill: 'both', + endDelay: -100 }, + before: 0, + active: 0, + after: 0 + }, + + { + input: { iterations: 2, + duration: 100, + delay: 1, + fill: 'both', + endDelay: -100 }, + before: 0, + active: 0, + after: 1 + }, + + { + input: { iterations: 1, + iterationStart: 2, + duration: 100, + delay: 1, + fill: 'both', + endDelay: -50 }, + before: 2, + active: 2, + after: 2 + }, + + { + input: { iterations: 1, + iterationStart: 2, + duration: 100, + delay: 1, + fill: 'both', + endDelay: -100 }, + before: 2, + active: 2, + after: 2 + }, +], 'Test end delay'); + + +// -------------------------------------------------------------------- +// +// Negative playback rate tests +// +// -------------------------------------------------------------------- + +runTests([ + { + input: { duration: 1, + delay: 1, + fill: 'both' }, + playbackRate: -1, + before: 0, + active: 0, + after: 0 + }, + + { + input: { duration: 1, + delay: 1, + iterations: 2, + fill: 'both' }, + playbackRate: -1, + before: 0, + active: 1, + after: 1 + }, + + { + input: { duration: 0, + delay: 1, + fill: 'both' }, + playbackRate: -1, + before: 0, + after: 0 + }, + + { + input: { duration: 0, + iterations: 0, + delay: 1, + fill: 'both' }, + playbackRate: -1, + before: 0, + after: 0 + }, +], 'Test negative playback rate'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/timing-model/animation-effects/local-time.html b/testing/web-platform/tests/web-animations/timing-model/animation-effects/local-time.html new file mode 100644 index 0000000000..79437d9f54 --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animation-effects/local-time.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Local time</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#local-time"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<body> +<script> +'use strict'; + +test(t => { + const anim = createDiv(t).animate(null, 10 * MS_PER_SEC); + for (const seconds of [-1, 0, 5, 10, 20]) { + anim.currentTime = seconds * MS_PER_SEC; + assert_equals( + anim.effect.getComputedTiming().localTime, + seconds * MS_PER_SEC + ); + } +}, 'Local time is current time for animation effects associated with an animation'); + +test(t => { + const effect = new KeyframeEffect(createDiv(t), null, 10 * MS_PER_SEC); + assert_equals(effect.getComputedTiming().localTime, null); +}, 'Local time is unresolved for animation effects not associated with an animation'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/timing-model/animation-effects/phases-and-states.html b/testing/web-platform/tests/web-animations/timing-model/animation-effects/phases-and-states.html new file mode 100644 index 0000000000..a33dbf517e --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animation-effects/phases-and-states.html @@ -0,0 +1,149 @@ +<!doctype html> +<meta charset=utf-8> +<title>Phases and states</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#animation-effect-phases-and-states"> +<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'; + +// -------------------------------------------------------------------- +// +// Phases +// +// -------------------------------------------------------------------- + +test(t => { + const animation = createDiv(t).animate(null, 1); + + for (const test of [{ currentTime: -1, phase: 'before' }, + { currentTime: 0, phase: 'active' }, + { currentTime: 1, phase: 'after' }]) { + assert_phase_at_time(animation, test.phase, test.currentTime); + } +}, 'Phase calculation for a simple animation effect'); + +test(t => { + const animation = createDiv(t).animate(null, { duration: 1, delay: 1 }); + + for (const test of [{ currentTime: 0, phase: 'before' }, + { currentTime: 1, phase: 'active' }, + { currentTime: 2, phase: 'after' }]) { + assert_phase_at_time(animation, test.phase, test.currentTime); + } +}, 'Phase calculation for an animation effect with a positive start delay'); + +test(t => { + const animation = createDiv(t).animate(null, { duration: 1, delay: -1 }); + + for (const test of [{ currentTime: -2, phase: 'before' }, + { currentTime: -1, phase: 'before' }, + { currentTime: 0, phase: 'after' }]) { + assert_phase_at_time(animation, test.phase, test.currentTime); + } +}, 'Phase calculation for an animation effect with a negative start delay'); + +test(t => { + const animation = createDiv(t).animate(null, { duration: 1, endDelay: 1 }); + + for (const test of [{ currentTime: -1, phase: 'before' }, + { currentTime: 0, phase: 'active' }, + { currentTime: 1, phase: 'after' }, + { currentTime: 2, phase: 'after' }]) { + assert_phase_at_time(animation, test.phase, test.currentTime); + } +}, 'Phase calculation for an animation effect with a positive end delay'); + +test(t => { + const animation = createDiv(t).animate(null, { duration: 2, endDelay: -1 }); + + for (const test of [{ currentTime: -1, phase: 'before' }, + { currentTime: 0, phase: 'active' }, + { currentTime: 0.9, phase: 'active' }, + { currentTime: 1, phase: 'after' }]) { + assert_phase_at_time(animation, test.phase, test.currentTime); + } +}, 'Phase calculation for an animation effect with a negative end delay lesser' + + ' in magnitude than the active duration'); + +test(t => { + const animation = createDiv(t).animate(null, { duration: 1, endDelay: -1 }); + + for (const test of [{ currentTime: -1, phase: 'before' }, + { currentTime: 0, phase: 'after' }, + { currentTime: 1, phase: 'after' }]) { + assert_phase_at_time(animation, test.phase, test.currentTime); + } +}, 'Phase calculation for an animation effect with a negative end delay equal' + + ' in magnitude to the active duration'); + +test(t => { + const animation = createDiv(t).animate(null, { duration: 1, endDelay: -2 }); + + for (const test of [{ currentTime: -2, phase: 'before' }, + { currentTime: -1, phase: 'before' }, + { currentTime: 0, phase: 'after' }]) { + assert_phase_at_time(animation, test.phase, test.currentTime); + } +}, 'Phase calculation for an animation effect with a negative end delay' + + ' greater in magnitude than the active duration'); + +test(t => { + const animation = createDiv(t).animate(null, { duration: 2, + delay: 1, + endDelay: -1 }); + + for (const test of [{ currentTime: 0, phase: 'before' }, + { currentTime: 1, phase: 'active' }, + { currentTime: 2, phase: 'after' }]) { + assert_phase_at_time(animation, test.phase, test.currentTime); + } +}, 'Phase calculation for an animation effect with a positive start delay' + + ' and a negative end delay lesser in magnitude than the active duration'); + +test(t => { + const animation = createDiv(t).animate(null, { duration: 1, + delay: -1, + endDelay: -1 }); + + for (const test of [{ currentTime: -2, phase: 'before' }, + { currentTime: -1, phase: 'before' }, + { currentTime: 0, phase: 'after' }]) { + assert_phase_at_time(animation, test.phase, test.currentTime); + } +}, 'Phase calculation for an animation effect with a negative start delay' + + ' and a negative end delay equal in magnitude to the active duration'); + +test(t => { + const animation = createDiv(t).animate(null, { duration: 1, + delay: -1, + endDelay: -2 }); + + for (const test of [{ currentTime: -3, phase: 'before' }, + { currentTime: -2, phase: 'before' }, + { currentTime: -1, phase: 'before' }, + { currentTime: 0, phase: 'after' }]) { + assert_phase_at_time(animation, test.phase, test.currentTime); + } +}, 'Phase calculation for an animation effect with a negative start delay' + + ' and a negative end delay equal greater in magnitude than the active' + + ' duration'); + +test(t => { + const animation = createDiv(t).animate(null, 1); + animation.playbackRate = -1; + + for (const test of [{ currentTime: -1, phase: 'before' }, + { currentTime: 0, phase: 'before' }, + { currentTime: 1, phase: 'active' }, + { currentTime: 2, phase: 'after' }]) { + assert_phase_at_time(animation, test.phase, test.currentTime); + } +}, 'Phase calculation for a simple animation effect with negative playback' + + ' rate'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/timing-model/animation-effects/simple-iteration-progress.html b/testing/web-platform/tests/web-animations/timing-model/animation-effects/simple-iteration-progress.html new file mode 100644 index 0000000000..3c42f79a71 --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animation-effects/simple-iteration-progress.html @@ -0,0 +1,600 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Simple iteration progress</title> +<link rel="help" + href="https://drafts.csswg.org/web-animations/#simple-iteration-progress"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<script src="../../resources/effect-tests.js"></script> +<body> +<div id="log"></div> +<script> +'use strict'; + +function runTests(tests, description) { + for (const currentTest of tests) { + let testParams = Object.entries(currentTest.input) + .map(([attr, value]) => `${attr}:${value}`) + .join(' '); + if (currentTest.playbackRate !== undefined) { + testParams += ` playbackRate:${currentTest.playbackRate}`; + } + + test(t => { + const div = createDiv(t); + const anim = div.animate({}, currentTest.input); + if (currentTest.playbackRate !== undefined) { + anim.playbackRate = currentTest.playbackRate; + } + + assert_computed_timing_for_each_phase( + anim, + 'progress', + { before: currentTest.before, + activeBoundary: currentTest.active, + after: currentTest.after }, + ); + }, `${description}: ${testParams}`); + } +} + + +// -------------------------------------------------------------------- +// +// Zero iteration duration tests +// +// -------------------------------------------------------------------- + +runTests([ + { + input: { iterations: 0, + iterationStart: 0, + duration: 0, + delay: 1, + fill: 'both' }, + before: 0, + after: 0 + }, + + { + input: { iterations: 0, + iterationStart: 0, + duration: 100, + delay: 1, + fill: 'both' }, + before: 0, + after: 0 + }, + + { + input: { iterations: 0, + iterationStart: 0, + duration: Infinity, + delay: 1, + fill: 'both' }, + before: 0, + after: 0 + }, + + { + input: { iterations: 0, + iterationStart: 2.5, + duration: 0, + delay: 1, + fill: 'both' }, + before: 0.5, + after: 0.5 + }, + + { + input: { iterations: 0, + iterationStart: 2.5, + duration: 100, + delay: 1, + fill: 'both' }, + before: 0.5, + after: 0.5 + }, + + { + input: { iterations: 0, + iterationStart: 2.5, + duration: Infinity, + delay: 1, + fill: 'both' }, + before: 0.5, + after: 0.5 + }, + + { + input: { iterations: 0, + iterationStart: 3, + duration: 0, + delay: 1, + fill: 'both' }, + before: 0, + after: 0 + }, + + { + input: { iterations: 0, + iterationStart: 3, + duration: 100, + delay: 1, + fill: 'both' }, + before: 0, + after: 0 + }, + + { + input: { iterations: 0, + iterationStart: 3, + duration: Infinity, + delay: 1, + fill: 'both' }, + before: 0, + after: 0 + } +], 'Test zero iterations'); + + +// -------------------------------------------------------------------- +// +// Tests where the iteration count is an integer +// +// -------------------------------------------------------------------- + +runTests([ + { + input: { iterations: 3, + iterationStart: 0, + duration: 0, + delay: 1, + fill: 'both' }, + before: 0, + after: 1 + }, + + { + input: { iterations: 3, + iterationStart: 0, + duration: 100, + delay: 1, + fill: 'both' }, + before: 0, + active: 0, + after: 1 + }, + + { + input: { iterations: 3, + iterationStart: 0, + duration: Infinity, + delay: 1, + fill: 'both' }, + before: 0, + active: 0 + }, + + { + input: { iterations: 3, + iterationStart: 2.5, + duration: 0, + delay: 1, + fill: 'both' }, + before: 0.5, + after: 0.5 + }, + + { + input: { iterations: 3, + iterationStart: 2.5, + duration: 100, + delay: 1, + fill: 'both' }, + before: 0.5, + active: 0.5, + after: 0.5 + }, + + { + input: { iterations: 3, + iterationStart: 2.5, + duration: Infinity, + delay: 1, + fill: 'both' }, + before: 0.5, + active: 0.5 + }, + + { + input: { iterations: 3, + iterationStart: 3, + duration: 0, + delay: 1, + fill: 'both' }, + before: 0, + after: 1 + }, + + { + input: { iterations: 3, + iterationStart: 3, + duration: 100, + delay: 1, + fill: 'both' }, + before: 0, + active: 0, + after: 1 + }, + + { + input: { iterations: 3, + iterationStart: 3, + duration: Infinity, + delay: 1, + fill: 'both' }, + before: 0, + active: 0 + } +], 'Test integer iterations'); + + +// -------------------------------------------------------------------- +// +// Tests where the iteration count is a fraction +// +// -------------------------------------------------------------------- + +runTests([ + { + input: { iterations: 3.5, + iterationStart: 0, + duration: 0, + delay: 1, + fill: 'both' }, + before: 0, + after: 0.5 + }, + + { + input: { iterations: 3.5, + iterationStart: 0, + duration: 100, + delay: 1, + fill: 'both' }, + before: 0, + active: 0, + after: 0.5 + }, + + { + input: { iterations: 3.5, + iterationStart: 0, + duration: Infinity, + delay: 1, + fill: 'both' }, + before: 0, + active: 0 + }, + + { + input: { iterations: 3.5, + iterationStart: 2.5, + duration: 0, + delay: 1, + fill: 'both' }, + before: 0.5, + after: 1 + }, + + { + input: { iterations: 3.5, + iterationStart: 2.5, + duration: 100, + delay: 1, + fill: 'both' }, + before: 0.5, + active: 0.5, + after: 1 + }, + + { + input: { iterations: 3.5, + iterationStart: 2.5, + duration: Infinity, + delay: 1, + fill: 'both' }, + before: 0.5, + active: 0.5 + }, + + { + input: { iterations: 3.5, + iterationStart: 3, + duration: 0, + delay: 1, + fill: 'both' }, + before: 0, + after: 0.5 + }, + + { + input: { iterations: 3.5, + iterationStart: 3, + duration: 100, + delay: 1, + fill: 'both' }, + before: 0, + active: 0, + after: 0.5 + }, + + { + input: { iterations: 3.5, + iterationStart: 3, + duration: Infinity, + delay: 1, + fill: 'both' }, + before: 0, + active: 0 + } +], 'Test fractional iterations'); + + +// -------------------------------------------------------------------- +// +// Tests where the iteration count is Infinity +// +// -------------------------------------------------------------------- + +runTests([ + { + input: { iterations: Infinity, + iterationStart: 0, + duration: 0, + delay: 1, + fill: 'both' }, + before: 0, + after: 1 + }, + + { + input: { iterations: Infinity, + iterationStart: 0, + duration: 100, + delay: 1, + fill: 'both' }, + before: 0, + active: 0 + }, + + { + input: { iterations: Infinity, + iterationStart: 0, + duration: Infinity, + delay: 1, + fill: 'both' }, + before: 0, + active: 0 + }, + + { + input: { iterations: Infinity, + iterationStart: 2.5, + duration: 0, + delay: 1, + fill: 'both' }, + before: 0.5, + after: 0.5 + }, + + { + input: { iterations: Infinity, + iterationStart: 2.5, + duration: 100, + delay: 1, + fill: 'both' }, + before: 0.5, + active: 0.5 + }, + + { + input: { iterations: Infinity, + iterationStart: 2.5, + duration: Infinity, + delay: 1, + fill: 'both' }, + before: 0.5, + active: 0.5 + }, + + { + input: { iterations: Infinity, + iterationStart: 3, + duration: 0, + delay: 1, + fill: 'both' }, + before: 0, + after: 1 + }, + + { + input: { iterations: Infinity, + iterationStart: 3, + duration: 100, + delay: 1, + fill: 'both' }, + before: 0, + active: 0 + }, + + { + input: { iterations: Infinity, + iterationStart: 3, + duration: Infinity, + delay: 1, + fill: 'both' }, + before: 0, + active: 0 + } +], 'Test infinity iterations'); + + +// -------------------------------------------------------------------- +// +// End delay tests +// +// -------------------------------------------------------------------- + +runTests([ + { + input: { duration: 100, + delay: 1, + fill: 'both', + endDelay: 50 }, + before: 0, + active: 0, + after: 1 + }, + + { + input: { duration: 100, + delay: 1, + fill: 'both', + endDelay: -50 }, + before: 0, + active: 0, + after: 0.5 + }, + + { + input: { duration: 100, + delay: 1, + fill: 'both', + endDelay: -100 }, + before: 0, + active: 0, + after: 0 + }, + + { + input: { duration: 100, + delay: 1, + fill: 'both', + endDelay: -200 }, + before: 0, + active: 0, + after: 0 + }, + + { + input: { iterationStart: 0.5, + duration: 100, + delay: 1, + fill: 'both', + endDelay: 50 }, + before: 0.5, + active: 0.5, + after: 0.5 + }, + + { + input: { iterationStart: 0.5, + duration: 100, + delay: 1, + fill: 'both', + endDelay: -50 }, + before: 0.5, + active: 0.5, + after: 0 + }, + + { + input: { iterationStart: 0.5, + duration: 100, + delay: 1, + fill: 'both', + endDelay: -100 }, + before: 0.5, + active: 0.5, + after: 0.5 + }, + + { + input: { iterations: 2, + duration: 100, + delay: 1, + fill: 'both', + endDelay: -100 }, + before: 0, + active: 0, + after: 0 + }, + + { + input: { iterations: 1, + iterationStart: 2, + duration: 100, + delay: 1, + fill: 'both', + endDelay: -50 }, + before: 0, + active: 0, + after: 0.5 + }, + + { + input: { iterations: 1, + iterationStart: 2, + duration: 100, + delay: 1, + fill: 'both', + endDelay: -100 }, + before: 0, + active: 0, + after: 0 + }, +], 'Test end delay'); + + +// -------------------------------------------------------------------- +// +// Negative playback rate tests +// +// -------------------------------------------------------------------- + +runTests([ + { + input: { duration: 1, + delay: 1, + fill: 'both' }, + playbackRate: -1, + before: 0, + active: 1, + after: 1 + }, + + { + input: { duration: 0, + delay: 1, + fill: 'both' }, + playbackRate: -1, + before: 0, + after: 1 + }, + + { + input: { duration: 0, + iterations: 0, + delay: 1, + fill: 'both' }, + playbackRate: -1, + before: 0, + after: 0 + }, +], 'Test negative playback rate'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/canceling-an-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/canceling-an-animation.html new file mode 100644 index 0000000000..f296ac4da7 --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/canceling-an-animation.html @@ -0,0 +1,127 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Canceling an animation</title> +<link rel="help" + href="https://drafts.csswg.org/web-animations/#canceling-an-animation-section"> +<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(t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.cancel(); + + assert_equals(animation.startTime, null, + 'The start time of a canceled animation should be unresolved'); + assert_equals(animation.currentTime, null, + 'The hold time of a canceled animation should be unresolved'); +}, 'Canceling an animation should cause its start time and hold time to be' + + ' unresolved'); + +promise_test(t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + const retPromise = animation.ready.then(() => { + assert_unreached('ready promise was fulfilled'); + }).catch(err => { + assert_equals(err.name, 'AbortError', + 'ready promise is rejected with AbortError'); + }); + + animation.cancel(); + + return retPromise; +}, 'A play-pending ready promise should be rejected when the animation is' + + ' canceled'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + await animation.ready; + + // Make it pause-pending + animation.pause(); + + // We need to store the original ready promise since cancel() will + // replace it + const originalPromise = animation.ready; + animation.cancel(); + + await promise_rejects_dom(t, 'AbortError', originalPromise, + 'Cancel should abort ready promise'); +}, 'A pause-pending ready promise should be rejected when the animation is' + + ' canceled'); + +promise_test(async t => { + const animation = createDiv(t).animate(null); + animation.cancel(); + const promiseResult = await animation.ready; + assert_equals(promiseResult, animation); +}, 'When an animation is canceled, it should create a resolved Promise'); + +test(t => { + const animation = createDiv(t).animate(null); + const promise = animation.ready; + animation.cancel(); + assert_not_equals(animation.ready, promise); + promise_rejects_dom(t, 'AbortError', promise, 'Cancel should abort ready promise'); +}, 'The ready promise should be replaced when the animation is canceled'); + +promise_test(t => { + const animation = new Animation( + new KeyframeEffect(null, null, { duration: 1000 }), + null + ); + assert_equals(animation.playState, 'idle', + 'The animation should be initially idle'); + + animation.finished.then(t.step_func(() => { + assert_unreached('Finished promise should not resolve'); + }), t.step_func(() => { + assert_unreached('Finished promise should not reject'); + })); + + animation.cancel(); + + return waitForAnimationFrames(3); +}, 'The finished promise should NOT be rejected if the animation is already' + + ' idle'); + +promise_test(t => { + const animation = new Animation( + new KeyframeEffect(null, null, { duration: 1000 }), + null + ); + assert_equals(animation.playState, 'idle', + 'The animation should be initially idle'); + + animation.oncancel = t.step_func(() => { + assert_unreached('Cancel event should not be fired'); + }); + + animation.cancel(); + + return waitForAnimationFrames(3); +}, 'The cancel event should NOT be fired if the animation is already' + + ' idle'); + +promise_test(async t => { + const div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + div.remove(); + + const eventWatcher = new EventWatcher(t, animation, 'cancel'); + + await animation.ready; + animation.cancel(); + + await eventWatcher.wait_for('cancel'); + + assert_equals(animation.effect.target.parentNode, null, + 'cancel event should be fired for the animation on an orphaned element'); +}, 'Canceling an animation should fire cancel event on orphaned element'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/document-timeline-animation-ref.html b/testing/web-platform/tests/web-animations/timing-model/animations/document-timeline-animation-ref.html new file mode 100644 index 0000000000..d1ee52a553 --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/document-timeline-animation-ref.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<title>Reference for document timeline animation</title> +<style> + #notes { + position: absolute; + left: 0px; + top: 100px; + } + body { + background: white; + } +</style> +<body> + <div id="box"></div> + <p id="notes"> + This test creates a document timeline animation. If any blue pixels appear + in the screenshot, the test fails. + </p> +</body>
\ No newline at end of file diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/document-timeline-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/document-timeline-animation.html new file mode 100644 index 0000000000..7d4dc76849 --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/document-timeline-animation.html @@ -0,0 +1,63 @@ + +<!DOCTYPE html> +<html class="reftest-wait"> +<meta charset="UTF-8"> +<title>document timeline animation</title> +<link rel="match" href="document-timeline-animation-ref.html"> +<script src="/common/reftest-wait.js"></script> +<script src="../../testcommon.js"></script> +<style> + #box-1, #box-2 { + position: absolute; + top: 0px; + width: 40px; + height: 40px; + } + #box-1 { + background: blue; + z-index: 1; + left: 0px; + } + #box-2 { + background: white; + z-index: 2; + left: 100px; + } + #notes { + position: absolute; + left: 0px; + top: 100px; + } + body { + background: white; + } +</style> + +<body> + <div id="box-1"></div> + <div id="box-2"></div> + <p id="notes"> + This test creates a document timeline animation. If any blue pixels appear + in the screenshot, the test fails. + </p> +</body> +<script> + onload = async function() { + const elem = document.getElementById('box-1'); + const keyframes = [ + { transform: 'none' }, + { transform: 'translateX(100px)' } + ]; + const effect = + new KeyframeEffect(elem, keyframes, + {iterations: 1, duration: 10000, fill: 'forwards'}); + const timeline = new DocumentTimeline(); + const animation = new Animation(effect, timeline); + animation.play(); + await animation.ready; + animation.finish(); + await animation.finished; + await waitForAnimationFrames(2); + takeScreenshot(); + }; +</script> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/finishing-an-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/finishing-an-animation.html new file mode 100644 index 0000000000..fbf6558f78 --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/finishing-an-animation.html @@ -0,0 +1,330 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Finishing an animation</title> +<link rel="help" + href="https://drafts.csswg.org/web-animations/#finishing-an-animation-section"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<script src="../../resources/timing-override.js"></script> +<body> +<div id="log"></div> +<script> +'use strict'; + +test(t => { + const div = createDiv(t); + const animation = div.animate(null, 100 * MS_PER_SEC); + animation.playbackRate = 0; + + assert_throws_dom('InvalidStateError', () => { + animation.finish(); + }); +}, 'Finishing an animation with a zero playback rate throws'); + +test(t => { + const div = createDiv(t); + const animation = div.animate(null, + { duration : 100 * MS_PER_SEC, + iterations : Infinity }); + + assert_throws_dom('InvalidStateError', () => { + animation.finish(); + }); +}, 'Finishing an infinite animation throws'); + +test(t => { + const div = createDiv(t); + const animation = div.animate(null, 100 * MS_PER_SEC); + animation.finish(); + + assert_time_equals_literal(animation.currentTime, 100 * MS_PER_SEC, + 'After finishing, the currentTime should be set to the end of the' + + ' active duration'); +}, 'Finishing an animation seeks to the end time'); + +test(t => { + const div = createDiv(t); + const animation = div.animate(null, 100 * MS_PER_SEC); + // 1s past effect end + animation.currentTime = + animation.effect.getComputedTiming().endTime + 1 * MS_PER_SEC; + animation.finish(); + + assert_time_equals_literal(animation.currentTime, 100 * MS_PER_SEC, + 'After finishing, the currentTime should be set back to the end of the' + + ' active duration'); +}, 'Finishing an animation with a current time past the effect end jumps' + + ' back to the end'); + +promise_test(async t => { + const div = createDiv(t); + const animation = div.animate(null, 100 * MS_PER_SEC); + animation.currentTime = 100 * MS_PER_SEC; + await animation.finished; + + animation.playbackRate = -1; + animation.finish(); + + assert_equals(animation.currentTime, 0, + 'After finishing a reversed animation the currentTime ' + + 'should be set to zero'); +}, 'Finishing a reversed animation jumps to zero time'); + +promise_test(async t => { + const div = createDiv(t); + const animation = div.animate(null, 100 * MS_PER_SEC); + animation.currentTime = 100 * MS_PER_SEC; + await animation.finished; + + animation.playbackRate = -1; + animation.currentTime = -1000; + animation.finish(); + + assert_equals(animation.currentTime, 0, + 'After finishing a reversed animation the currentTime ' + + 'should be set back to zero'); +}, 'Finishing a reversed animation with a current time less than zero' + + ' makes it jump back to zero'); + +promise_test(async t => { + const div = createDiv(t); + const animation = div.animate(null, 100 * MS_PER_SEC); + animation.pause(); + await animation.ready; + + animation.finish(); + + assert_equals(animation.playState, 'finished', + 'The play state of a paused animation should become ' + + '"finished"'); + assert_times_equal(animation.startTime, + animation.timeline.currentTime - 100 * MS_PER_SEC, + 'The start time of a paused animation should be set'); +}, 'Finishing a paused animation resolves the start time'); + +test(t => { + const div = createDiv(t); + const animation = div.animate(null, 100 * MS_PER_SEC); + // Update playbackRate so we can test that the calculated startTime + // respects it + animation.playbackRate = 2; + animation.pause(); + // While animation is still pause-pending call finish() + animation.finish(); + + assert_false(animation.pending); + assert_equals(animation.playState, 'finished', + 'The play state of a pause-pending animation should become ' + + '"finished"'); + assert_times_equal(animation.startTime, + animation.timeline.currentTime - 100 * MS_PER_SEC / 2, + 'The start time of a pause-pending animation should ' + + 'be set'); +}, 'Finishing a pause-pending animation resolves the pending task' + + ' immediately and update the start time'); + +test(t => { + const div = createDiv(t); + const animation = div.animate(null, 100 * MS_PER_SEC); + animation.playbackRate = -2; + animation.pause(); + animation.finish(); + + assert_false(animation.pending); + assert_equals(animation.playState, 'finished', + 'The play state of a pause-pending animation should become ' + + '"finished"'); + assert_times_equal(animation.startTime, animation.timeline.currentTime, + 'The start time of a pause-pending animation should be ' + + 'set'); +}, 'Finishing a pause-pending animation with negative playback rate' + + ' resolves the pending task immediately'); + +test(t => { + const div = createDiv(t); + const animation = div.animate(null, 100 * MS_PER_SEC); + animation.playbackRate = 0.5; + animation.finish(); + + assert_false(animation.pending); + assert_equals(animation.playState, 'finished', + 'The play state of a play-pending animation should become ' + + '"finished"'); + assert_times_equal(animation.startTime, + animation.timeline.currentTime - 100 * MS_PER_SEC / 0.5, + 'The start time of a play-pending animation should ' + + 'be set'); +}, 'Finishing an animation while play-pending resolves the pending' + + ' task immediately'); + +// FIXME: Add a test for when we are play-pending without an active timeline. +// - In that case even after calling finish() we should still be pending but +// the current time should be updated + +promise_test(async t => { + const div = createDiv(t); + const animation = div.animate(null, 100 * MS_PER_SEC); + await animation.ready; + + animation.pause(); + animation.play(); + // We are now in the unusual situation of being play-pending whilst having + // a resolved start time. Check that finish() still triggers a transition + // to the finished state immediately. + animation.finish(); + + assert_equals(animation.playState, 'finished', + 'After aborting a pause then finishing an animation its play ' + + 'state should become "finished" immediately'); +}, 'Finishing an animation during an aborted pause makes it finished' + + ' immediately'); + +promise_test(async t => { + const div = createDiv(t); + const animation = div.animate(null, 100 * MS_PER_SEC); + let resolvedFinished = false; + animation.finished.then(() => { + resolvedFinished = true; + }); + + await animation.ready; + + animation.finish(); + await Promise.resolve(); + + assert_true(resolvedFinished, 'finished promise should be resolved'); +}, 'Finishing an animation resolves the finished promise synchronously'); + +promise_test(async t => { + const effect = new KeyframeEffect(null, null, 100 * MS_PER_SEC); + const animation = new Animation(effect, document.timeline); + let resolvedFinished = false; + animation.finished.then(() => { + resolvedFinished = true; + }); + + await animation.ready; + + animation.finish(); + await Promise.resolve(); + + assert_true(resolvedFinished, 'finished promise should be resolved'); +}, 'Finishing an animation without a target resolves the finished promise' + + ' synchronously'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + const promise = animation.ready; + let readyResolved = false; + + animation.finish(); + animation.ready.then(() => { readyResolved = true; }); + + const promiseResult = await animation.finished; + + assert_equals(promiseResult, animation); + assert_equals(animation.ready, promise); + assert_true(readyResolved); +}, 'A pending ready promise is resolved and not replaced when the animation' + + ' is finished'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + await animation.ready; + + animation.updatePlaybackRate(2); + assert_true(animation.pending); + + animation.finish(); + assert_false(animation.pending); + assert_equals(animation.playbackRate, 2); + assert_time_equals_literal(animation.currentTime, 100 * MS_PER_SEC); +}, 'A pending playback rate should be applied immediately when an animation' + + ' is finished'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + await animation.ready; + + animation.updatePlaybackRate(0); + + assert_throws_dom('InvalidStateError', () => { + animation.finish(); + }); +}, 'An exception should be thrown if the effective playback rate is zero'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, { + duration: 100 * MS_PER_SEC, + iterations: Infinity + }); + animation.currentTime = 50 * MS_PER_SEC; + animation.playbackRate = -1; + await animation.ready; + + animation.updatePlaybackRate(1); + + assert_throws_dom('InvalidStateError', () => { + animation.finish(); + }); +}, 'An exception should be thrown when finishing if the effective playback rate' + + ' is positive and the target effect end is infinity'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, { + duration: 100 * MS_PER_SEC, + iterations: Infinity + }); + await animation.ready; + + animation.updatePlaybackRate(-1); + + animation.finish(); + // Should not have thrown +}, 'An exception is NOT thrown when finishing if the effective playback rate' + + ' is negative and the target effect end is infinity'); + +promise_test(async t => { + const div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + div.remove(); + + const eventWatcher = new EventWatcher(t, animation, 'finish'); + + await animation.ready; + animation.finish(); + + await eventWatcher.wait_for('finish'); + assert_equals(animation.effect.target.parentNode, null, + 'finish event should be fired for the animation on an orphaned element'); +}, 'Finishing an animation fires finish event on orphaned element'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + await animation.ready; + + const originalFinishPromise = animation.finished; + + animation.cancel(); + assert_equals(animation.startTime, null); + assert_equals(animation.currentTime, null); + + const resolvedFinishPromise = animation.finished; + assert_not_equals(originalFinishPromise, resolvedFinishPromise, + 'Canceling an animation should create a new finished promise'); + + animation.finish(); + assert_equals(animation.playState, 'finished', + 'The play state of a canceled animation should become ' + + '"finished"'); + assert_times_equal(animation.startTime, + animation.timeline.currentTime - 100 * MS_PER_SEC, + 'The start time of a finished animation should be set'); + assert_times_equal(animation.currentTime, 100000, + 'Hold time should be set to end boundary of the animation'); + +}, 'Finishing a canceled animation sets the current and start times'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/infinite-duration-animation-ref.html b/testing/web-platform/tests/web-animations/timing-model/animations/infinite-duration-animation-ref.html new file mode 100644 index 0000000000..6b358bd4e7 --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/infinite-duration-animation-ref.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<title>Reference for infinite duration animation</title> +<style> + #notes { + position: absolute; + left: 0px; + top: 100px; + } + body { + background: white; + } +</style> +<body> + <p id="notes"> + This test creates an infinite duration animations, which should be stuck at + a progress of 0. If any blue pixels appear in the screenshot, the test + fails. + </p> +</body> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/infinite-duration-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/infinite-duration-animation.html new file mode 100644 index 0000000000..c641e5afa2 --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/infinite-duration-animation.html @@ -0,0 +1,64 @@ + +<!DOCTYPE html> +<html class="reftest-wait"> +<meta charset="UTF-8"> +<title>Infinite duration animation</title> +<link rel="match" href="infinite-duration-animation-ref.html"> +<script src="/common/reftest-wait.js"></script> +<script src="../../testcommon.js"></script> +<style> + #box-1, #box-2 { + border: 1px solid white; + height: 40px; + position: absolute; + top: 40px; + width: 40px; + } + #box-1 { + background: blue; + z-index: 1; + } + #box-2 { + background: white; + z-index: 2; + } + #notes { + position: absolute; + left: 0px; + top: 100px; + } + body { + background: white; + } +</style> + +<body> + <div id="box-1"></div> + <div id="box-2"></div> + <p id="notes"> + This test creates an infinite duration animations, which should be stuck at + a progress of 0. If any blue pixels appear in the screenshot, the test + fails. + </p> +</body> +<script> + onload = async function() { + // Double rAF to ensure that we are not bogged down during initialization + // and the compositor is ready. + waitForAnimationFrames(2).then(() => { + const elem = document.getElementById('box-1'); + const keyframes = [ + { transform: 'translateX(0px)' }, + { transform: 'translateX(100px)' } + ]; + const effect = + new KeyframeEffect(elem, keyframes, + {iterations: 3, duration: Infinity}); + const animation = new Animation(effect); + animation.play(); + animation.ready.then(() => { + takeScreenshotDelayed(100); + }); + }); + }; +</script> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/invalidating-animation-before-start-time-synced.html b/testing/web-platform/tests/web-animations/timing-model/animations/invalidating-animation-before-start-time-synced.html new file mode 100644 index 0000000000..653e46906a --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/invalidating-animation-before-start-time-synced.html @@ -0,0 +1,71 @@ +<!DOCTYPE html> +<link rel="author" href="mailto:bokan@chromium.org"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<style> +.target { + width: 100px; + height: 100px; + background-color: dodgerblue; +} +</style> +<script> +function jank() { + let start = performance.now(); + let x = 0; + while(performance.now() - start < 100) { + x++; + } +} + +function target1() { return document.querySelector('#target1'); } +function target2() { return document.querySelector('#target2'); } +function spinner() { return document.querySelector('#spinner'); } + +function firstFrame() { + target1().animate([{transform: 'translateX(400px)'}], {id: "target1", duration: 10000}); + target2().animate([{transform: 'translateX(400px)'}], {id: "target2", duration: 10000}); + requestAnimationFrame(secondFrame); + + // Simulate some jank so that, if the above animations are started + // asynchronously, the next rendering opportunity is likely to start + // immediately after this one and without the animations having started yet. + jank(); +} + +function secondFrame() { + // Modify the style to invalidate the starting keyframe on the first target + // only. + target1().style.transform = `translateY(-1px)`; + + // The spinner is used to avoid a specific Chrome behavior (bug?). It ensures + // a new animation is pushed to the compositor in this frame and prevents the + // #target1 animation being started from the main thread in + // PendingAnimations::Update when `started_synchronized_on_compositor` is + // false. + spinner().animate([{transform: 'rotateZ(90deg)'}], {id: 'spinner', duration: 1000}); + + requestAnimationFrame(finishTestCb); +} + +let finishTestCb = null; +const finishTest = new Promise(resolve => { finishTestCb = resolve; }); + +promise_test(async (t) => { + onload = () => requestAnimationFrame(firstFrame); + await finishTest; + + anim1 = target1().getAnimations()[0]; + anim2 = target2().getAnimations()[0]; + + await Promise.all([anim1.ready, anim2.ready]); + assert_not_equals(anim1.startTime, 0); + assert_equals(anim1.startTime, anim2.startTime); +}, "Animation invalidated before startTime is set doesn't affect startTime"); +</script> +<!-- This text is necessary in Chrome in order to trigger a first contentful +paint which unblocks compositor commits. --> +The blue boxes below should stay aligned in the x-axis. +<div id="target1" class="target"></div> +<div id="target2" class="target"></div> +<div id="spinner" class="target" style="background:limegreen"></div> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/pausing-an-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/pausing-an-animation.html new file mode 100644 index 0000000000..dd9522cb35 --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/pausing-an-animation.html @@ -0,0 +1,119 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Pausing an animation</title> +<link rel="help" + href="https://drafts.csswg.org/web-animations/#pausing-an-animation-section"> +<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'; + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + await animation.ready; + + const startTimeBeforePausing = animation.startTime; + + animation.pause(); + assert_equals(animation.startTime, startTimeBeforePausing, + 'The start time does not change when pausing-pending'); + + await animation.ready; + + assert_equals(animation.startTime, null, + 'The start time is unresolved when paused'); +}, 'Pausing clears the start time'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + await animation.ready; + + animation.pause(); + assert_not_equals(animation.startTime, null, + 'The start time is resolved when pause-pending'); + + animation.play(); + assert_not_equals(animation.startTime, null, + 'The start time is preserved when a pause is aborted'); +}, 'Aborting a pause preserves the start time'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + const promise = animation.ready; + animation.pause(); + + const promiseResult = await promise; + + assert_equals(promiseResult, animation); + assert_equals(animation.ready, promise); + assert_false(animation.pending, 'No longer pause-pending'); +}, 'A pending ready promise should be resolved and not replaced when the' + + ' animation is paused'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + // Let animation start roughly half-way through + animation.currentTime = 50 * MS_PER_SEC; + await animation.ready; + + // Go pause-pending and also set a pending playback rate + animation.pause(); + animation.updatePlaybackRate(0.5); + + await animation.ready; + // If the current time was updated using the new playback rate it will jump + // back to 25s but if we correctly used the old playback rate the current time + // will be >= 50s. + assert_greater_than_equal(animation.currentTime, 50 * MS_PER_SEC); +}, 'A pause-pending animation maintains the current time when applying a' + + ' pending playback rate'); + +promise_test(async t => { + // This test does not cover a specific step in the algorithm but serves as a + // high-level sanity check that pausing does, in fact, freeze the current + // time. + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + await animation.ready; + + animation.pause(); + await animation.ready; + + const currentTimeAfterPausing = animation.currentTime; + + await waitForNextFrame(); + + assert_equals(animation.currentTime, currentTimeAfterPausing, + 'Animation.currentTime is unchanged after pausing'); +}, 'The animation\'s current time remains fixed after pausing'); + + +promise_test(async t => { + + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + + const originalReadyPromise = animation.ready; + animation.cancel(); + assert_equals(animation.startTime, null); + assert_equals(animation.currentTime, null); + + const readyPromise = animation.ready; + assert_not_equals(originalReadyPromise, readyPromise, + 'Canceling an animation should create a new ready promise'); + + animation.pause(); + assert_equals(animation.playState, 'paused', + 'Pausing a canceled animation should update the play state'); + assert_true(animation.pending, 'animation should be pause-pending'); + await animation.ready; + assert_false(animation.pending, + 'animation should no longer be pause-pending'); + assert_equals(animation.startTime, null, 'start time should be unresolved'); + assert_equals(animation.currentTime, 0, 'current time should be set to zero'); + +}, 'Pausing a canceled animation sets the current time'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/play-states.html b/testing/web-platform/tests/web-animations/timing-model/animations/play-states.html new file mode 100644 index 0000000000..ec7d8c842f --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/play-states.html @@ -0,0 +1,185 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Play states</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#play-states"> +<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(t => { + const animation = new Animation( + new KeyframeEffect(null, {}, 100 * MS_PER_SEC) + ); + assert_equals(animation.currentTime, null, + 'Current time should be initially unresolved'); + + assert_equals(animation.playState, 'idle'); +}, 'reports \'idle\' for an animation with an unresolved current time' + + ' and no pending tasks') + +test(t => { + const animation = createDiv(t).animate({}, 100 * MS_PER_SEC); + + animation.pause(); + + assert_equals(animation.playState, 'paused'); +}, 'reports \'paused\' for an animation with a pending pause task'); + +test(t => { + const animation = new Animation( + new KeyframeEffect(null, {}, 100 * MS_PER_SEC) + ); + + animation.currentTime = 0; + assert_equals(animation.startTime, null, + 'Start time should still be unresolved after setting current' + + ' time'); + + assert_equals(animation.playState, 'paused'); +}, 'reports \'paused\' for an animation with a resolved current time and' + + ' unresolved start time') + +test(t => { + const animation = new Animation( + new KeyframeEffect(null, {}, 100 * MS_PER_SEC) + ); + + animation.startTime = document.timeline.currentTime; + assert_not_equals(animation.currentTime, null, + 'Current time should be resolved after setting start time'); + + assert_equals(animation.playState, 'running'); +}, 'reports \'running\' for an animation with a resolved start time and' + + ' current time'); + +test(t => { + const animation = new Animation( + new KeyframeEffect(null, {}, 100 * MS_PER_SEC) + ); + animation.startTime = document.timeline.currentTime; + + animation.currentTime = 100 * MS_PER_SEC; + + assert_equals(animation.playState, 'finished'); +}, 'reports \'finished\' when playback rate > 0 and' + + ' current time = target effect end'); + +test(t => { + const animation = new Animation( + new KeyframeEffect(null, {}, 100 * MS_PER_SEC) + ); + animation.startTime = document.timeline.currentTime; + + animation.playbackRate = 0; + animation.currentTime = 100 * MS_PER_SEC; + + assert_equals(animation.playState, 'running'); +}, 'reports \'running\' when playback rate = 0 and' + + ' current time = target effect end'); + +test(t => { + const animation = new Animation( + new KeyframeEffect(null, {}, 100 * MS_PER_SEC) + ); + animation.startTime = document.timeline.currentTime; + + animation.playbackRate = -1; + animation.currentTime = 100 * MS_PER_SEC; + + assert_equals(animation.playState, 'running'); +}, 'reports \'running\' when playback rate < 0 and' + + ' current time = target effect end'); + +test(t => { + const animation = new Animation( + new KeyframeEffect(null, {}, 100 * MS_PER_SEC) + ); + animation.startTime = document.timeline.currentTime; + + animation.currentTime = 0; + + assert_equals(animation.playState, 'running'); +}, 'reports \'running\' when playback rate > 0 and' + + ' current time = 0'); + +test(t => { + const animation = new Animation( + new KeyframeEffect(null, {}, 100 * MS_PER_SEC) + ); + animation.startTime = document.timeline.currentTime; + + animation.playbackRate = 0; + animation.currentTime = 0; + + assert_equals(animation.playState, 'running'); +}, 'reports \'running\' when playback rate = 0 and' + + ' current time = 0'); + +test(t => { + const animation = new Animation( + new KeyframeEffect(null, {}, 100 * MS_PER_SEC) + ); + animation.startTime = document.timeline.currentTime; + + animation.playbackRate = -1; + animation.currentTime = 0; + + assert_equals(animation.playState, 'finished'); +}, 'reports \'finished\' when playback rate < 0 and' + + ' current time = 0'); + +test(t => { + const animation = createDiv(t).animate({}, 0); + assert_equals(animation.startTime, null, + 'Sanity check: start time should be unresolved'); + + assert_equals(animation.playState, 'finished'); +}, 'reports \'finished\' when playback rate > 0 and' + + ' current time = target effect end and there is a pending play task'); + +test(t => { + const animation = createDiv(t).animate({}, 100 * MS_PER_SEC); + assert_equals(animation.startTime, null, + 'Sanity check: start time should be unresolved'); + + assert_equals(animation.playState, 'running'); +}, 'reports \'running\' when playback rate > 0 and' + + ' current time < target effect end and there is a pending play task'); + +test(t => { + const animation = createDiv(t).animate({}, 100 * MS_PER_SEC); + assert_equals(animation.playState, 'running'); + assert_true(animation.pending); +}, 'reports \'running\' for a play-pending animation'); + +test(t => { + const animation = createDiv(t).animate({}, 100 * MS_PER_SEC); + animation.pause(); + assert_equals(animation.playState, 'paused'); + assert_true(animation.pending); +}, 'reports \'paused\' for a pause-pending animation'); + +test(t => { + const animation = createDiv(t).animate({}, 0); + assert_equals(animation.playState, 'finished'); + assert_true(animation.pending); +}, 'reports \'finished\' for a finished-pending animation'); + +test(t => { + const animation = createDiv(t).animate({}, 100 * MS_PER_SEC); + // Set up the pending playback rate + animation.updatePlaybackRate(-1); + // Call play again so that we seek to the end while remaining play-pending + animation.play(); + // For a pending animation, the play state should always report what the + // play state _will_ be once we finish pending. + assert_equals(animation.playState, 'running'); + assert_true(animation.pending); +}, 'reports the play state based on the pending playback rate'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/playing-an-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/playing-an-animation.html new file mode 100644 index 0000000000..01e036ae57 --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/playing-an-animation.html @@ -0,0 +1,177 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Playing an animation</title> +<link rel="help" + href="https://drafts.csswg.org/web-animations/#playing-an-animation-section"> +<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(t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.currentTime = 1 * MS_PER_SEC; + assert_time_equals_literal(animation.currentTime, 1 * MS_PER_SEC); + animation.play(); + assert_time_equals_literal(animation.currentTime, 1 * MS_PER_SEC); +}, 'Playing a running animation leaves the current time unchanged'); + +test(t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.finish(); + assert_time_equals_literal(animation.currentTime, 100 * MS_PER_SEC); + animation.play(); + assert_time_equals_literal(animation.currentTime, 0); +}, 'Playing a finished animation seeks back to the start'); + +test(t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.playbackRate = -1; + animation.currentTime = 0; + assert_time_equals_literal(animation.currentTime, 0); + animation.play(); + assert_time_equals_literal(animation.currentTime, 100 * MS_PER_SEC); +}, 'Playing a finished and reversed animation seeks to end'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.finish(); + + // Initiate a pause then abort it + animation.pause(); + animation.play(); + + // Wait to return to running state + await animation.ready; + + assert_true(animation.currentTime < 100 * 1000, + 'After aborting a pause when finished, the current time should' + + ' jump back to the start of the animation'); +}, 'Playing a pause-pending but previously finished animation seeks back to' + + ' to the start'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.finish(); + await animation.ready; + + animation.play(); + assert_equals(animation.startTime, null, 'start time is unresolved'); +}, 'Playing a finished animation clears the start time'); + +test(t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.cancel(); + const promise = animation.ready; + animation.play(); + assert_not_equals(animation.ready, promise); +}, 'The ready promise should be replaced if the animation is not already' + + ' pending'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + const promise = animation.ready; + const promiseResult = await promise; + assert_equals(promiseResult, animation); + assert_equals(animation.ready, promise); +}, 'A pending ready promise should be resolved and not replaced when the' + + ' animation enters the running state'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.currentTime = 50 * MS_PER_SEC; + await animation.ready; + + animation.pause(); + await animation.ready; + + const holdTime = animation.currentTime; + + animation.play(); + await animation.ready; + + assert_less_than_equal( + animation.startTime, + animation.timeline.currentTime - holdTime + TIME_PRECISION + ); +}, 'Resuming an animation from paused calculates start time from hold time'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + await animation.ready; + + // Go to pause-pending state + animation.pause(); + assert_true(animation.pending, 'Animation is pending'); + const pauseReadyPromise = animation.ready; + + // Now play again immediately (abort the pause) + animation.play(); + assert_true(animation.pending, 'Animation is still pending'); + assert_equals(animation.ready, pauseReadyPromise, + 'The pause Promise is re-used when playing while waiting' + + ' to pause'); + + // Sanity check: Animation proceeds to running state + await animation.ready; + assert_true(!animation.pending && animation.playState === 'running', + 'Animation is running after aborting a pause'); +}, 'If a pause operation is interrupted, the ready promise is reused'); + +promise_test(async t => { + // Seek animation beyond target end + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.currentTime = -100 * MS_PER_SEC; + await animation.ready; + + // Set pending playback rate to the opposite direction + animation.updatePlaybackRate(-1); + assert_true(animation.pending); + assert_equals(animation.playbackRate, 1); + + // When we play, we should seek to the target end, NOT to zero (which + // is where we would seek to if we used the playbackRate of 1. + animation.play(); + assert_time_equals_literal(animation.currentTime, 100 * MS_PER_SEC); +}, 'A pending playback rate is used when determining auto-rewind behavior'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.cancel(); + assert_equals(animation.startTime, null, + 'Start time should be unresolved'); + + const playTime = animation.timeline.currentTime; + animation.play(); + assert_true(animation.pending, 'Animation should be play-pending'); + + await animation.ready; + + assert_false(animation.pending, 'animation should no longer be pending'); + assert_time_greater_than_equal(animation.startTime, playTime, + 'The start time of the playing animation should be set'); +}, 'Playing a canceled animation sets the start time'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.playbackRate = -1; + animation.cancel(); + assert_equals(animation.startTime, null, + 'Start time should be unresolved'); + + const playTime = animation.timeline.currentTime; + animation.play(); + assert_true(animation.pending, 'Animation should be play-pending'); + + await animation.ready; + + assert_false(animation.pending, 'Animation should no longer be pending'); + assert_time_greater_than_equal(animation.startTime, playTime + 100 * MS_PER_SEC, + 'The start time of the playing animation should be set'); +}, 'Playing a canceled animation backwards sets the start time'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/reverse-running-animation-ref.html b/testing/web-platform/tests/web-animations/timing-model/animations/reverse-running-animation-ref.html new file mode 100644 index 0000000000..7bf5b03c1e --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/reverse-running-animation-ref.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<title>Reference for reverse running animation</title> +<style> + #box { + background: green; + height: 40px; + width: 40px; + } +</style> +<body> + <div id="box"></div> + <p id="notes"> + This test animates the box color from green to red and reverses the play + direction shortly after the midpoint. If the box remains red, the test + failed. + </p> +</body> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/reverse-running-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/reverse-running-animation.html new file mode 100644 index 0000000000..c9eb2a068e --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/reverse-running-animation.html @@ -0,0 +1,50 @@ + +<!DOCTYPE html> +<html class="reftest-wait"> +<meta charset="UTF-8"> +<title>reverse running animation</title> +<link rel="match" href="reverse-running-animation-ref.html"> +<script src="/common/reftest-wait.js"></script> +<script src="../../testcommon.js"></script> +<style> + #box { + background: green; + height: 40px; + width: 40px; + } +</style> +<body> + <div id="box"></div> + <p id="notes"> + This test animates the box color from green to red and reverses the play + direction shortly after the midpoint. If the box remains red, the test + failed. + </p> +</body> +<script> + onload = async function() { + const box = document.getElementById('box'); + const duration = 10000; + const anim = + box.animate({ bacground: [ 'green', 'red' ] }, + { duration: duration, easing: 'steps(2, jump-none)' }); + anim.currentTime = duration / 2; + anim.ready.then(() => { + const startTime = anim.timeline.currentTime; + waitForAnimationFrames(2).then(() => { + anim.reverse(); + anim.ready.then(() => { + const reversalTime = anim.timeline.currentTime; + const forwardPlayingTime = reversalTime - startTime; + const checkIfDone = () => { + if (anim.timeline.currentTime - reversalTime > forwardPlayingTime) + takeScreenshot(); + else + requestAnimationFrame(checkIfDone); + }; + requestAnimationFrame(checkIfDone); + }); + }); + }); + }; +</script> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/reversing-an-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/reversing-an-animation.html new file mode 100644 index 0000000000..8d869d72aa --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/reversing-an-animation.html @@ -0,0 +1,266 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Reversing an animation</title> +<link rel="help" + href="https://drafts.csswg.org/web-animations/#reversing-an-animation-section"> +<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'; + +promise_test(async t => { + const div = createDiv(t); + const animation = div.animate({}, { duration: 100 * MS_PER_SEC, + iterations: Infinity }); + + await animation.ready; + // Wait a frame because if currentTime is still 0 when we call + // reverse(), it will throw (per spec). + await waitForAnimationFrames(1); + + assert_greater_than_equal(animation.currentTime, 0, + 'currentTime expected to be greater than 0, one frame after starting'); + animation.currentTime = 50 * MS_PER_SEC; + const previousPlaybackRate = animation.playbackRate; + animation.reverse(); + assert_equals(animation.playbackRate, previousPlaybackRate, + 'Playback rate should not have changed'); + await animation.ready; + + assert_equals(animation.playbackRate, -previousPlaybackRate, + 'Playback rate should be inverted'); +}, 'Reversing an animation inverts the playback rate'); + +promise_test(async t => { + const div = createDiv(t); + const animation = div.animate({}, { duration: 100 * MS_PER_SEC, + iterations: Infinity }); + animation.currentTime = 50 * MS_PER_SEC; + animation.pause(); + + await animation.ready; + + animation.reverse(); + await animation.ready; + + assert_equals(animation.playState, 'running', + 'Animation.playState should be "running" after reverse()'); +}, 'Reversing an animation plays a pausing animation'); + +test(t => { + const div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + animation.currentTime = 50 * MS_PER_SEC; + animation.reverse(); + + assert_equals(animation.currentTime, 50 * MS_PER_SEC, + 'The current time should not change it is in the middle of ' + + 'the animation duration'); +}, 'Reversing an animation maintains the same current time'); + +test(t => { + const div = createDiv(t); + const animation = div.animate({}, { duration: 200 * MS_PER_SEC, + delay: -100 * MS_PER_SEC }); + assert_true(animation.pending, + 'The animation is pending before we call reverse'); + + animation.reverse(); + + assert_true(animation.pending, + 'The animation is still pending after calling reverse'); +}, 'Reversing an animation does not cause it to leave the pending state'); + +promise_test(async t => { + const div = createDiv(t); + const animation = div.animate({}, { duration: 200 * MS_PER_SEC, + delay: -100 * MS_PER_SEC }); + let readyResolved = false; + animation.ready.then(() => { readyResolved = true; }); + + animation.reverse(); + + await Promise.resolve(); + assert_false(readyResolved, + 'ready promise should not have been resolved yet'); +}, 'Reversing an animation does not cause it to resolve the ready promise'); + +test(t => { + const div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + animation.currentTime = 200 * MS_PER_SEC; + animation.reverse(); + + assert_equals(animation.currentTime, 100 * MS_PER_SEC, + 'reverse() should start playing from the animation effect end ' + + 'if the playbackRate > 0 and the currentTime > effect end'); +}, 'Reversing an animation when playbackRate > 0 and currentTime > ' + + 'effect end should make it play from the end'); + +test(t => { + const div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + + animation.currentTime = -200 * MS_PER_SEC; + animation.reverse(); + + assert_equals(animation.currentTime, 100 * MS_PER_SEC, + 'reverse() should start playing from the animation effect end ' + + 'if the playbackRate > 0 and the currentTime < 0'); +}, 'Reversing an animation when playbackRate > 0 and currentTime < 0 ' + + 'should make it play from the end'); + +test(t => { + const div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + animation.playbackRate = -1; + animation.currentTime = -200 * MS_PER_SEC; + animation.reverse(); + + assert_equals(animation.currentTime, 0, + 'reverse() should start playing from the start of animation time ' + + 'if the playbackRate < 0 and the currentTime < 0'); +}, 'Reversing an animation when playbackRate < 0 and currentTime < 0 ' + + 'should make it play from the start'); + +test(t => { + const div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + animation.playbackRate = -1; + animation.currentTime = 200 * MS_PER_SEC; + animation.reverse(); + + assert_equals(animation.currentTime, 0, + 'reverse() should start playing from the start of animation time ' + + 'if the playbackRate < 0 and the currentTime > effect end'); +}, 'Reversing an animation when playbackRate < 0 and currentTime > effect ' + + 'end should make it play from the start'); + +test(t => { + const div = createDiv(t); + const animation = div.animate({}, { duration: 100 * MS_PER_SEC, + iterations: Infinity }); + animation.currentTime = -200 * MS_PER_SEC; + + assert_throws_dom('InvalidStateError', + () => { animation.reverse(); }, + 'reverse() should throw InvalidStateError ' + + 'if the playbackRate > 0 and the currentTime < 0 ' + + 'and the target effect is positive infinity'); +}, 'Reversing an animation when playbackRate > 0 and currentTime < 0 ' + + 'and the target effect end is positive infinity should throw an exception'); + +promise_test(async t => { + const animation = createDiv(t).animate({}, { duration: 100 * MS_PER_SEC, + iterations: Infinity }); + animation.currentTime = -200 * MS_PER_SEC; + + try { animation.reverse(); } catch(e) { } + + assert_equals(animation.playbackRate, 1, 'playbackRate is unchanged'); + + await animation.ready; + assert_equals(animation.playbackRate, 1, 'playbackRate remains unchanged'); +}, 'When reversing throws an exception, the playback rate remains unchanged'); + +test(t => { + const div = createDiv(t); + const animation = div.animate({}, { duration: 100 * MS_PER_SEC, + iterations: Infinity }); + animation.currentTime = -200 * MS_PER_SEC; + animation.playbackRate = 0; + + try { + animation.reverse(); + } catch (e) { + assert_unreached(`Unexpected exception when calling reverse(): ${e}`); + } +}, 'Reversing animation when playbackRate = 0 and currentTime < 0 ' + + 'and the target effect end is positive infinity should NOT throw an ' + + 'exception'); + +test(t => { + const div = createDiv(t); + const animation = div.animate({}, { duration: 100 * MS_PER_SEC, + iterations: Infinity }); + animation.playbackRate = -1; + animation.currentTime = -200 * MS_PER_SEC; + animation.reverse(); + + assert_equals(animation.currentTime, 0, + 'reverse() should start playing from the start of animation time ' + + 'if the playbackRate < 0 and the currentTime < 0 ' + + 'and the target effect is positive infinity'); +}, 'Reversing an animation when playbackRate < 0 and currentTime < 0 ' + + 'and the target effect end is positive infinity should make it play ' + + 'from the start'); + +promise_test(async t => { + const div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + animation.playbackRate = 0; + animation.currentTime = 50 * MS_PER_SEC; + animation.reverse(); + + await animation.ready; + assert_equals(animation.playbackRate, 0, + 'reverse() should preserve playbackRate if the playbackRate == 0'); + assert_equals(animation.currentTime, 50 * MS_PER_SEC, + 'reverse() should not affect the currentTime if the playbackRate == 0'); +}, 'Reversing when when playbackRate == 0 should preserve the current ' + + 'time and playback rate'); + +test(t => { + const div = createDiv(t); + const animation = + new Animation(new KeyframeEffect(div, null, 100 * MS_PER_SEC)); + assert_equals(animation.currentTime, null); + + animation.reverse(); + + assert_equals(animation.currentTime, 100 * MS_PER_SEC, + 'animation.currentTime should be at its effect end'); +}, 'Reversing an idle animation from starts playing the animation'); + +test(t => { + const div = createDiv(t); + const animation = + new Animation(new KeyframeEffect(div, null, 100 * MS_PER_SEC), null); + + assert_throws_dom('InvalidStateError', () => { animation.reverse(); }); +}, 'Reversing an animation without an active timeline throws an ' + + 'InvalidStateError'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + await animation.ready; + + animation.updatePlaybackRate(2); + animation.reverse(); + + await animation.ready; + assert_equals(animation.playbackRate, -2); +}, 'Reversing should use the negative pending playback rate'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, { + duration: 100 * MS_PER_SEC, + iterations: Infinity, + }); + animation.currentTime = -200 * MS_PER_SEC; + await animation.ready; + + animation.updatePlaybackRate(2); + assert_throws_dom('InvalidStateError', () => { animation.reverse(); }); + assert_equals(animation.playbackRate, 1); + + await animation.ready; + assert_equals(animation.playbackRate, 2); +}, 'When reversing fails, it should restore any previous pending playback' + + ' rate'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/seamlessly-updating-the-playback-rate-of-an-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/seamlessly-updating-the-playback-rate-of-an-animation.html new file mode 100644 index 0000000000..dffbeabd59 --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/seamlessly-updating-the-playback-rate-of-an-animation.html @@ -0,0 +1,171 @@ +<!doctype html> +<meta charset=utf-8> +<title>Seamlessly updating the playback rate of an animation</title> +<link rel="help" + href="https://drafts.csswg.org/web-animations-1/#seamlessly-updating-the-playback-rate-of-an-animation"> +<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'; + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + await animation.ready; + + animation.currentTime = 50 * MS_PER_SEC; + + animation.updatePlaybackRate(0.5); + await animation.ready; + // Since the animation is in motion (and we want to test it while it is in + // motion!) we can't assert that the current time == 50s but we can check + // that the current time is NOT re-calculated by simply substituting in the + // new playback rate (i.e. without adjusting the start time). If that were + // the case the currentTime would jump to 25s. So we just test the currentTime + // hasn't gone backwards. + assert_greater_than_equal(animation.currentTime, 50 * MS_PER_SEC, + 'Reducing the playback rate should not change the current time ' + + 'of a playing animation'); + + animation.updatePlaybackRate(2); + await animation.ready; + // Likewise, we test here that the current time does not jump to 100s as it + // would if we naively applied a playbackRate of 2 without adjusting the + // startTime. + assert_less_than(animation.currentTime, 100 * MS_PER_SEC, + 'Increasing the playback rate should not change the current time ' + + 'of a playing animation'); +}, 'Updating the playback rate maintains the current time'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + await animation.ready; + + assert_false(animation.pending); + animation.updatePlaybackRate(2); + assert_true(animation.pending); +}, 'Updating the playback rate while running makes the animation pending'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.currentTime = 50 * MS_PER_SEC; + assert_true(animation.pending); + + animation.updatePlaybackRate(0.5); + + // Check that the hold time is updated as expected + assert_time_equals_literal(animation.currentTime, 50 * MS_PER_SEC); + + await animation.ready; + + // As above, check that the currentTime is not calculated by simply + // substituting in the updated playbackRate without updating the startTime. + assert_greater_than_equal(animation.currentTime, 50 * MS_PER_SEC, + 'Reducing the playback rate should not change the current time ' + + 'of a play-pending animation'); +}, 'Updating the playback rate on a play-pending animation maintains' + + ' the current time'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.currentTime = 50 * MS_PER_SEC; + await animation.ready; + + animation.pause(); + animation.updatePlaybackRate(0.5); + + assert_greater_than_equal(animation.currentTime, 50 * MS_PER_SEC); +}, 'Updating the playback rate on a pause-pending animation maintains' + + ' the current time'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + + animation.updatePlaybackRate(2); + animation.updatePlaybackRate(3); + animation.updatePlaybackRate(4); + + assert_equals(animation.playbackRate, 1); + await animation.ready; + + assert_equals(animation.playbackRate, 4); +}, 'If a pending playback rate is set multiple times, the latest wins'); + +test(t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.cancel(); + + animation.updatePlaybackRate(2); + assert_equals(animation.playbackRate, 2); + assert_false(animation.pending); +}, 'In the idle state, the playback rate is applied immediately'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.pause(); + await animation.ready; + + animation.updatePlaybackRate(2); + assert_equals(animation.playbackRate, 2); + assert_false(animation.pending); +}, 'In the paused state, the playback rate is applied immediately'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.finish(); + assert_time_equals_literal(animation.currentTime, 100 * MS_PER_SEC); + assert_false(animation.pending); + + animation.updatePlaybackRate(2); + assert_equals(animation.playbackRate, 2); + assert_time_equals_literal(animation.currentTime, 100 * MS_PER_SEC); + assert_false(animation.pending); +}, 'Updating the playback rate on a finished animation maintains' + + ' the current time'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.finish(); + assert_time_equals_literal(animation.currentTime, 100 * MS_PER_SEC); + assert_false(animation.pending); + + animation.updatePlaybackRate(0); + assert_equals(animation.playbackRate, 0); + assert_time_equals_literal(animation.currentTime, 100 * MS_PER_SEC); + assert_false(animation.pending); +}, 'Updating the playback rate to zero on a finished animation maintains' + + ' the current time'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + await animation.ready; + + // Get the animation in a state where it has an unresolved current time, + // a resolved start time (so it is not 'idle') and but no pending play task. + animation.timeline = null; + animation.startTime = 0; + assert_equals(animation.currentTime, null); + assert_equals(animation.playState, 'running'); + + // Make the effect end infinite. + animation.effect.updateTiming({ endDelay: 1e38 }); + + // Now we want to check that when we go to set a negative playback rate we + // don't end up throwing an InvalidStateError (which would happen if we ended + // up applying the auto-rewind behavior). + animation.updatePlaybackRate(-1); + + // Furthermore, we should apply the playback rate immediately since the + // current time is unresolved. + assert_equals(animation.playbackRate, -1, + 'We apply the pending playback rate immediately if the current time is ' + + 'unresolved'); + assert_false(animation.pending); +}, 'Updating the negative playback rate with the unresolved current time and' + + ' a positive infinite associated effect end should not throw an' + + ' exception'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/setting-the-current-time-of-an-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/setting-the-current-time-of-an-animation.html new file mode 100644 index 0000000000..809877345f --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/setting-the-current-time-of-an-animation.html @@ -0,0 +1,167 @@ +<!doctype html> +<meta charset=utf-8> +<title>Setting the current time of an animation</title> +<link rel="help" + href="https://drafts.csswg.org/web-animations-1/#setting-the-current-time-of-an-animation"> +<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(t => { + const anim = new Animation(); + assert_equals(anim.playState, 'idle'); + assert_equals(anim.currentTime, null); + + // This should not throw because the currentTime is already null. + anim.currentTime = null; +}, 'Setting the current time of a pending animation to unresolved does not' + + ' throw a TypeError'); + +promise_test(async t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + await anim.ready; + + assert_greater_than_equal(anim.currentTime, 0); + assert_throws_js(TypeError, () => { + anim.currentTime = null; + }); +}, 'Setting the current time of a playing animation to unresolved throws a' + + ' TypeError'); + +promise_test(async t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + await anim.ready; + anim.pause(); + + assert_greater_than_equal(anim.currentTime, 0); + assert_throws_js(TypeError, () => { + anim.currentTime = null; + }); +}, 'Setting the current time of a paused animation to unresolved throws a' + + ' TypeError'); + + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + + assert_throws_js(TypeError, () => { + animation.currentTime = CSSNumericValue.parse("30%"); + }); + assert_throws_js(TypeError, () => { + animation.currentTime = CSSNumericValue.parse("30deg"); + }); + + animation.currentTime = 2000; + assert_equals(animation.currentTime, 2000, "Set current time using double"); + + animation.currentTime = CSSNumericValue.parse("3000"); + assert_equals(animation.currentTime, 3000, "Set current time using " + + "CSSNumericValue number value"); + + animation.currentTime = CSSNumericValue.parse("4000ms"); + assert_equals(animation.currentTime, 4000, "Set current time using " + + "CSSNumericValue milliseconds value"); + + animation.currentTime = CSSNumericValue.parse("50s"); + assert_equals(animation.currentTime, 50000, "Set current time using " + + "CSSNumericValue seconds value"); +}, 'Validate different value types that can be used to set current time'); + +promise_test(async t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + await anim.ready; + anim.pause(); + + // We should be pause-pending now + assert_true(anim.pending); + assert_equals(anim.playState, 'paused'); + + // Apply a pending playback rate + anim.updatePlaybackRate(2); + assert_equals(anim.playbackRate, 1); + + // Setting the current time should apply the pending playback rate + anim.currentTime = 50 * MS_PER_SEC; + assert_equals(anim.playbackRate, 2); + assert_false(anim.pending); + + // Sanity check that the current time is preserved + assert_time_equals_literal(anim.currentTime, 50 * MS_PER_SEC); +}, 'Setting the current time of a pausing animation applies a pending playback' + + ' rate'); + + +// The following tests verify that currentTime can be set outside of the normal +// bounds of an animation. + +promise_test(async t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + await anim.ready; + + anim.currentTime = 200 * MS_PER_SEC; + assert_equals(anim.playState, 'finished'); + assert_time_equals_literal(anim.currentTime, 200 * MS_PER_SEC); +}, 'Setting the current time after the end with a positive playback rate'); + +promise_test(async t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + await anim.ready; + + anim.currentTime = -100 * MS_PER_SEC; + assert_equals(anim.playState, 'running'); + assert_time_equals_literal(anim.currentTime, -100 * MS_PER_SEC); + + await waitForAnimationFrames(2); + assert_greater_than(anim.currentTime, -100 * MS_PER_SEC); +}, 'Setting a negative current time with a positive playback rate'); + +promise_test(async t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + anim.updatePlaybackRate(-1); + await anim.ready; + + anim.currentTime = 200 * MS_PER_SEC; + assert_equals(anim.playState, 'running'); + assert_time_equals_literal(anim.currentTime, 200 * MS_PER_SEC); + + await waitForAnimationFrames(2); + assert_less_than(anim.currentTime, 200 * MS_PER_SEC); +}, 'Setting the current time after the end with a negative playback rate'); + +promise_test(async t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + anim.updatePlaybackRate(-1); + await anim.ready; + + anim.currentTime = -100 * MS_PER_SEC; + assert_equals(anim.playState, 'finished'); + assert_time_equals_literal(anim.currentTime, -100 * MS_PER_SEC); +}, 'Setting a negative current time with a negative playback rate'); + +promise_test(async t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + anim.updatePlaybackRate(0); + await anim.ready; + + // An animation with a playback rate of zero is never in the finished state + // even if currentTime is outside the normal range of [0, effect end]. + anim.currentTime = 200 * MS_PER_SEC; + assert_equals(anim.playState, 'running'); + assert_time_equals_literal(anim.currentTime, 200 * MS_PER_SEC); + await waitForAnimationFrames(2); + assert_time_equals_literal(anim.currentTime, 200 * MS_PER_SEC); + + anim.currentTime = -200 * MS_PER_SEC; + assert_equals(anim.playState, 'running'); + assert_time_equals_literal(anim.currentTime, -200 * MS_PER_SEC); + await waitForAnimationFrames(2); + assert_time_equals_literal(anim.currentTime, -200 * MS_PER_SEC); + +}, 'Setting the current time on an animation with a zero playback rate'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/setting-the-playback-rate-of-an-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/setting-the-playback-rate-of-an-animation.html new file mode 100644 index 0000000000..a1f9e4f3ac --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/setting-the-playback-rate-of-an-animation.html @@ -0,0 +1,110 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Setting the playback rate of an animation</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#setting-the-playback-rate-of-an-animation"> +<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'; + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.playbackRate = 2; + await animation.ready; + + const previousAnimationCurrentTime = animation.currentTime; + const previousTimelineCurrentTime = animation.timeline.currentTime; + + await waitForAnimationFrames(1); + + const animationCurrentTimeDifference = + animation.currentTime - previousAnimationCurrentTime; + const timelineCurrentTimeDifference = + animation.timeline.currentTime - previousTimelineCurrentTime; + + assert_times_equal( + animationCurrentTimeDifference, + timelineCurrentTimeDifference * animation.playbackRate, + 'The current time should increase two times faster than timeline' + ); +}, 'The playback rate affects the rate of progress of the current time'); + +test(t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.currentTime = 50 * MS_PER_SEC; + animation.playbackRate = 2; + assert_time_equals_literal(animation.currentTime, 50 * MS_PER_SEC); +}, 'Setting the playback rate while play-pending preserves the current time'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.currentTime = 50 * MS_PER_SEC; + await animation.ready; + animation.playbackRate = 2; + assert_greater_than_equal(animation.currentTime, 50 * MS_PER_SEC); + assert_less_than(animation.currentTime, 100 * MS_PER_SEC); +}, 'Setting the playback rate while playing preserves the current time'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.currentTime = 50 * MS_PER_SEC; + animation.updatePlaybackRate(2); + animation.playbackRate = 1; + await animation.ready; + assert_equals(animation.playbackRate, 1); +}, 'Setting the playback rate should clear any pending playback rate'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.currentTime = 50 * MS_PER_SEC; + animation.pause(); + await animation.ready; + animation.playbackRate = 2; + // Ensure that the animation remains paused and current time is preserved. + assert_equals(animation.playState, 'paused'); + assert_time_equals_literal(animation.currentTime, 50 * MS_PER_SEC); +}, 'Setting the playback rate while paused preserves the current time and ' + + 'state'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.currentTime = 150 * MS_PER_SEC; + await animation.ready; + animation.playbackRate = 2; + // Ensure that current time is preserved and does not snap to the effect end + // time. + assert_equals(animation.playState, 'finished'); + assert_time_equals_literal(animation.currentTime, 150 * MS_PER_SEC); +}, 'Setting the playback rate while finished preserves the current time'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.currentTime = 150 * MS_PER_SEC; + await animation.ready; + assert_equals(animation.playState, 'finished'); + animation.playbackRate = -1; + // Ensure that current time does not snap to the effect end time and that the + // animation resumes playing. + assert_equals(animation.playState, 'running'); + assert_time_equals_literal(animation.currentTime, 150 * MS_PER_SEC); + await waitForAnimationFrames(2); + assert_less_than(animation.currentTime, 150 * MS_PER_SEC); +}, 'Reversing the playback rate while finished restarts the animation'); + + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + await animation.ready; + animation.currentTime = 50 * MS_PER_SEC; + animation.playbackRate = 0; + // Ensure that current time does not drift. + assert_equals(animation.playState, 'running'); + await waitForAnimationFrames(2); + assert_time_equals_literal(animation.currentTime, 50 * MS_PER_SEC); +}, 'Setting a zero playback rate while running preserves the current time'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/setting-the-start-time-of-an-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/setting-the-start-time-of-an-animation.html new file mode 100644 index 0000000000..fee3f1e0de --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/setting-the-start-time-of-an-animation.html @@ -0,0 +1,329 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Setting the start time of an animation</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#setting-the-start-time-of-an-animation"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<script src="../../resources/timing-override.js"></script> +<body> +<div id="log"></div> +<script> +'use strict'; + +promise_test(async t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + null); + + assert_throws_js(TypeError, () => { + animation.startTime = CSSNumericValue.parse("30%"); + }); + assert_throws_js(TypeError, () => { + animation.startTime = CSSNumericValue.parse("30deg"); + }); + + animation.startTime = 2000; + assert_equals(animation.startTime, 2000, "Set start time using double"); + + animation.startTime = CSSNumericValue.parse("3000"); + assert_equals(animation.startTime, 3000, "Set start time using " + + "CSSNumericValue number value"); + + animation.startTime = CSSNumericValue.parse("4000ms"); + assert_equals(animation.startTime, 4000, "Set start time using " + + "CSSNumericValue milliseconds value"); + + animation.startTime = CSSNumericValue.parse("50s"); + assert_equals(animation.startTime, 50000, "Set start time using " + + "CSSNumericValue seconds value"); +}, 'Validate different value types that can be used to set start time'); + +test(t => { + // It should only be possible to set *either* the start time or the current + // time for an animation that does not have an active timeline. + + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + null); + + assert_equals(animation.currentTime, null, 'Intial current time'); + assert_equals(animation.startTime, null, 'Intial start time'); + + animation.currentTime = 1000; + assert_equals(animation.currentTime, 1000, + 'Setting the current time succeeds'); + assert_equals(animation.startTime, null, + 'Start time remains null after setting current time'); + + animation.startTime = 1000; + assert_equals(animation.startTime, 1000, + 'Setting the start time succeeds'); + assert_equals(animation.currentTime, null, + 'Setting the start time clears the current time'); + + animation.startTime = null; + assert_equals(animation.startTime, null, + 'Setting the start time to an unresolved time succeeds'); + assert_equals(animation.currentTime, null, 'The current time is unaffected'); + +}, 'Setting the start time of an animation without an active timeline'); + +test(t => { + // Setting an unresolved start time on an animation without an active + // timeline should not clear the current time. + + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + null); + + assert_equals(animation.currentTime, null, 'Intial current time'); + assert_equals(animation.startTime, null, 'Intial start time'); + + animation.currentTime = 1000; + assert_equals(animation.currentTime, 1000, + 'Setting the current time succeeds'); + assert_equals(animation.startTime, null, + 'Start time remains null after setting current time'); + + animation.startTime = null; + assert_equals(animation.startTime, null, 'Start time remains unresolved'); + assert_equals(animation.currentTime, 1000, 'Current time is unaffected'); + +}, 'Setting an unresolved start time an animation without an active timeline' + + ' does not clear the current time'); + +test(t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + document.timeline); + + // So long as a hold time is set, querying the current time will return + // the hold time. + + // Since the start time is unresolved at this point, setting the current time + // will set the hold time + animation.currentTime = 1000; + assert_equals(animation.currentTime, 1000, + 'The current time is calculated from the hold time'); + + // If we set the start time, however, we should clear the hold time. + animation.startTime = document.timeline.currentTime - 2000; + assert_time_equals_literal(animation.currentTime, 2000, + 'The current time is calculated from the start' + + ' time, not the hold time'); + + // Sanity check + assert_equals(animation.playState, 'running', + 'Animation reports it is running after setting a resolved' + + ' start time'); +}, 'Setting the start time clears the hold time'); + +test(t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + document.timeline); + + // Set up a running animation (i.e. both start time and current time + // are resolved). + animation.startTime = document.timeline.currentTime - 1000; + assert_equals(animation.playState, 'running'); + assert_time_equals_literal(animation.currentTime, 1000, + 'Current time is resolved for a running animation'); + + // Clear start time + animation.startTime = null; + assert_time_equals_literal(animation.currentTime, 1000, + 'Hold time is set after start time is made' + + ' unresolved'); + assert_equals(animation.playState, 'paused', + 'Animation reports it is paused after setting an unresolved' + + ' start time'); +}, 'Setting an unresolved start time sets the hold time'); + +promise_test(async t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + document.timeline); + + let readyPromiseCallbackCalled = false; + animation.ready.then(() => { readyPromiseCallbackCalled = true; } ); + + // Put the animation in the play-pending state + animation.play(); + + // Sanity check + assert_true(animation.pending && animation.playState === 'running', + 'Animation is in play-pending state'); + + // Setting the start time should resolve the 'ready' promise, i.e. + // it should schedule a microtask to run the promise callbacks. + animation.startTime = document.timeline.currentTime; + assert_false(readyPromiseCallbackCalled, + 'Ready promise callback is not called synchronously'); + + // If we schedule another microtask then it should run immediately after + // the ready promise resolution microtask. + await Promise.resolve(); + assert_true(readyPromiseCallbackCalled, + 'Ready promise callback called after setting startTime'); +}, 'Setting the start time resolves a pending ready promise'); + +promise_test(async t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + document.timeline); + + let readyPromiseCallbackCalled = false; + animation.ready.then(() => { readyPromiseCallbackCalled = true; } ); + + // Put the animation in the pause-pending state + animation.startTime = document.timeline.currentTime; + animation.pause(); + + // Sanity check + assert_true(animation.pending && animation.playState === 'paused', + 'Animation is in pause-pending state'); + + // Setting the start time should resolve the 'ready' promise although + // the resolution callbacks when be run in a separate microtask. + animation.startTime = null; + assert_false(readyPromiseCallbackCalled, + 'Ready promise callback is not called synchronously'); + + await Promise.resolve(); + assert_true(readyPromiseCallbackCalled, + 'Ready promise callback called after setting startTime'); +}, 'Setting the start time resolves a pending pause task'); + +promise_test(async t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + document.timeline); + + // Put the animation in the play-pending state + animation.play(); + + // Sanity check + assert_true(animation.pending, 'Animation is pending'); + assert_equals(animation.playState, 'running', + 'Animation is play-pending'); + assert_equals(animation.startTime, null, 'Start time is null'); + + // Even though the startTime is already null, setting it to the same value + // should still cancel the pending task. + animation.startTime = null; + assert_false(animation.pending, 'Animation is no longer pending'); + assert_equals(animation.playState, 'paused', 'Animation is paused'); +}, 'Setting an unresolved start time on a play-pending animation makes it' + + ' paused'); + +promise_test(async t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + document.timeline); + + // Set start time such that the current time is past the end time + animation.startTime = document.timeline.currentTime + - 110 * MS_PER_SEC; + assert_equals(animation.playState, 'finished', + 'Seeked to finished state using the startTime'); + + // If the 'did seek' flag is true, the current time should be greater than + // the effect end. + assert_greater_than(animation.currentTime, + animation.effect.getComputedTiming().endTime, + 'Setting the start time updated the finished state with' + + ' the \'did seek\' flag set to true'); + + // Furthermore, that time should persist if we have correctly updated + // the hold time + const finishedCurrentTime = animation.currentTime; + await waitForAnimationFrames(1); + assert_equals(animation.currentTime, finishedCurrentTime, + 'Current time does not change after seeking past the effect' + + ' end time by setting the current time'); +}, 'Setting the start time updates the finished state'); + +promise_test(async t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + + // We should be play-pending now + assert_true(anim.pending); + assert_equals(anim.playState, 'running'); + + // Apply a pending playback rate + anim.updatePlaybackRate(2); + assert_equals(anim.playbackRate, 1); + assert_true(anim.pending); + + // Setting the start time should apply the pending playback rate + anim.startTime = anim.timeline.currentTime - 25 * MS_PER_SEC; + assert_equals(anim.playbackRate, 2); + assert_false(anim.pending); + + // Sanity check that the start time is preserved and current time is + // calculated using the new playback rate + assert_times_equal(anim.startTime, + anim.timeline.currentTime - 25 * MS_PER_SEC); + assert_time_equals_literal(anim.currentTime, 50 * MS_PER_SEC); +}, 'Setting the start time of a play-pending animation applies a pending playback rate'); + +promise_test(async t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + await anim.ready; + + // We should be running now + assert_false(anim.pending); + assert_equals(anim.playState, 'running'); + + // Apply a pending playback rate + anim.updatePlaybackRate(2); + assert_equals(anim.playbackRate, 1); + assert_true(anim.pending); + + // Setting the start time should apply the pending playback rate + anim.startTime = anim.timeline.currentTime - 25 * MS_PER_SEC; + assert_equals(anim.playbackRate, 2); + assert_false(anim.pending); + + // Sanity check that the start time is preserved and current time is + // calculated using the new playback rate + assert_times_equal(anim.startTime, + anim.timeline.currentTime - 25 * MS_PER_SEC); + assert_time_equals_literal(parseInt(anim.currentTime.toPrecision(5), 10), 50 * MS_PER_SEC); +}, 'Setting the start time of a playing animation applies a pending playback rate'); + +promise_test(async t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + await anim.ready; + assert_equals(anim.playState, 'running'); + + // Setting the start time updates the finished state. The hold time is not + // constrained by the effect end time. + anim.startTime = -200 * MS_PER_SEC; + assert_equals(anim.playState, 'finished'); + + assert_times_equal(anim.currentTime, + document.timeline.currentTime + 200 * MS_PER_SEC); +}, 'Setting the start time on a running animation updates the play state'); + +promise_test(async t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + await anim.ready; + + // Setting the start time updates the finished state. The hold time is not + // constrained by the normal range of the animation time. + anim.currentTime = 100 * MS_PER_SEC; + assert_equals(anim.playState, 'finished'); + anim.playbackRate = -1; + assert_equals(anim.playState, 'running'); + anim.startTime = -200 * MS_PER_SEC; + assert_equals(anim.playState, 'finished'); + assert_times_equal(anim.currentTime, + -document.timeline.currentTime - 200 * MS_PER_SEC); +}, 'Setting the start time on a reverse running animation updates the play ' + + 'state'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/setting-the-target-effect-of-an-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/setting-the-target-effect-of-an-animation.html new file mode 100644 index 0000000000..60ea1850fc --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/setting-the-target-effect-of-an-animation.html @@ -0,0 +1,129 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Setting the target effect of an animation</title> +<link rel='help' href='https://drafts.csswg.org/web-animations/#setting-the-target-effect'> +<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'; + +promise_test(t => { + const anim = createDiv(t).animate({ marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + assert_true(anim.pending); + + const originalReadyPromise = anim.ready.catch(err => { + assert_unreached('Original ready promise should not be rejected'); + }); + + anim.effect = null; + assert_equals(anim.playState, 'finished'); + assert_true(anim.pending); + + return originalReadyPromise; +}, 'If new effect is null and old effect is not null the animation becomes' + + ' finish-pending'); + +promise_test(async t => { + const anim = new Animation(); + anim.pause(); + assert_true(anim.pending); + + anim.effect = new KeyframeEffect(createDiv(t), + { marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + assert_true(anim.pending); + await anim.ready; + + assert_false(anim.pending); + assert_equals(anim.playState, 'paused'); +}, 'If animation has a pending pause task, reschedule that task to run ' + + 'as soon as animation is ready.'); + +promise_test(async t => { + const anim = new Animation(); + anim.play(); + assert_true(anim.pending); + + anim.effect = new KeyframeEffect(createDiv(t), + { marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + assert_true(anim.pending); + await anim.ready; + + assert_false(anim.pending); + assert_equals(anim.playState, 'running'); +}, 'If animation has a pending play task, reschedule that task to run ' + + 'as soon as animation is ready to play new effect.'); + +promise_test(async t => { + const anim = createDiv(t).animate({ marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + assert_equals(anim.playState, 'running'); + assert_true(anim.pending); + + const originalEffect = anim.effect; + const originalReadyPromise = anim.ready; + + anim.effect = null; + assert_equals(anim.playState, 'finished'); + assert_true(anim.pending); + + anim.effect = originalEffect; + assert_equals(anim.playState, 'running'); + assert_true(anim.pending); + + await originalReadyPromise; + + assert_equals(anim.playState, 'running'); + assert_false(anim.pending); +}, 'The pending play task should be rescheduled even after temporarily setting' + + ' the effect to null'); + +promise_test(async t => { + const animA = createDiv(t).animate({ marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + const animB = new Animation(); + + await animA.ready; + + animB.effect = animA.effect; + assert_equals(animA.effect, null); + assert_equals(animA.playState, 'finished'); +}, 'When setting the effect of an animation to the effect of an existing ' + + 'animation, the existing animation\'s target effect should be set to null.'); + +test(t => { + const animA = createDiv(t).animate({ marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + const animB = new Animation(); + const effect = animA.effect; + animA.currentTime = 50 * MS_PER_SEC; + animB.currentTime = 20 * MS_PER_SEC; + assert_equals(effect.getComputedTiming().progress, 0.5, + 'Original timing comes from first animation'); + animB.effect = effect; + assert_equals(effect.getComputedTiming().progress, 0.2, + 'After setting the effect on a different animation, ' + + 'it uses the new animation\'s timing'); +}, 'After setting the target effect of animation to the target effect of an ' + + 'existing animation, the target effect\'s timing is updated to reflect ' + + 'the current time of the new animation.'); + +promise_test(async t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + anim.updatePlaybackRate(2); + assert_equals(anim.playbackRate, 1); + + anim.effect = null; + await anim.ready; + + assert_equals(anim.playbackRate, 2); +}, 'Setting the target effect to null causes a pending playback rate to be' + + ' applied'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/setting-the-timeline-of-an-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/setting-the-timeline-of-an-animation.html new file mode 100644 index 0000000000..d4f802152c --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/setting-the-timeline-of-an-animation.html @@ -0,0 +1,255 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Setting the timeline of an animation</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#setting-the-timeline"> +<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'; + +// --------------------------------------------------------------------- +// +// Tests from no timeline to timeline +// +// --------------------------------------------------------------------- + +test(t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + null); + animation.currentTime = 50 * MS_PER_SEC; + assert_equals(animation.playState, 'paused'); + + animation.timeline = document.timeline; + + assert_equals(animation.playState, 'paused'); + assert_time_equals_literal(animation.currentTime, 50 * MS_PER_SEC); +}, 'After setting timeline on paused animation it is still paused'); + +test(t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + null); + animation.currentTime = 200 * MS_PER_SEC; + assert_equals(animation.playState, 'paused'); + + animation.timeline = document.timeline; + + assert_equals(animation.playState, 'paused'); + assert_time_equals_literal(animation.currentTime, 200 * MS_PER_SEC); +}, 'After setting timeline on animation paused outside active interval' + + ' it is still paused'); + +test(t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + null); + assert_equals(animation.playState, 'idle'); + + animation.timeline = document.timeline; + + assert_equals(animation.playState, 'idle'); +}, 'After setting timeline on an idle animation without a start time' + + ' it is still idle'); + +test(t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + null); + animation.startTime = document.timeline.currentTime; + assert_equals(animation.playState, 'running'); + + animation.timeline = document.timeline; + + assert_equals(animation.playState, 'running'); +}, 'After transitioning from a null timeline on an animation with a start time' + + ' it is still running'); + +test(t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + null); + animation.startTime = document.timeline.currentTime - 200 * MS_PER_SEC; + assert_equals(animation.playState, 'running'); + + animation.timeline = document.timeline; + + assert_equals(animation.playState, 'finished'); +}, 'After transitioning from a null timeline on an animation with a ' + + 'sufficiently ancient start time it is finished'); + +promise_test(async t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + null); + animation.play(); + assert_true(animation.pending && animation.playState === 'running', + 'Animation is initially play-pending'); + + animation.timeline = document.timeline; + + assert_true(animation.pending && animation.playState === 'running', + 'Animation is still play-pending after setting timeline'); + + await animation.ready; + assert_true(!animation.pending && animation.playState === 'running', + 'Animation plays after it finishes pending'); +}, 'After setting timeline on a play-pending animation it begins playing' + + ' after pending'); + +promise_test(async t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + null); + animation.startTime = document.timeline.currentTime; + animation.pause(); + animation.timeline = null; + assert_true(animation.pending && animation.playState === 'paused', + 'Animation is initially pause-pending'); + + animation.timeline = document.timeline; + + assert_true(animation.pending && animation.playState === 'paused', + 'Animation is still pause-pending after setting timeline'); + + await animation.ready; + assert_true(!animation.pending && animation.playState === 'paused', + 'Animation pauses after it finishes pending'); +}, 'After setting timeline on a pause-pending animation it becomes paused' + + ' after pending'); + +// --------------------------------------------------------------------- +// +// Tests from timeline to no timeline +// +// --------------------------------------------------------------------- + +test(t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + document.timeline); + animation.currentTime = 50 * MS_PER_SEC; + assert_false(animation.pending); + assert_equals(animation.playState, 'paused'); + + animation.timeline = null; + + assert_false(animation.pending); + assert_equals(animation.playState, 'paused'); + assert_time_equals_literal(animation.currentTime, 50 * MS_PER_SEC); +}, 'After clearing timeline on paused animation it is still paused'); + +test(t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + document.timeline); + const initialStartTime = document.timeline.currentTime - 200 * MS_PER_SEC; + animation.startTime = initialStartTime; + assert_equals(animation.playState, 'finished'); + + animation.timeline = null; + + assert_equals(animation.playState, 'running'); + assert_times_equal(animation.startTime, initialStartTime); +}, 'After clearing timeline on finished animation it is running'); + +test(t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + document.timeline); + const initialStartTime = document.timeline.currentTime - 50 * MS_PER_SEC; + animation.startTime = initialStartTime; + assert_equals(animation.playState, 'running'); + + animation.timeline = null; + + assert_equals(animation.playState, 'running'); + assert_times_equal(animation.startTime, initialStartTime); +}, 'After clearing timeline on running animation it is still running'); + +test(t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + document.timeline); + assert_equals(animation.playState, 'idle'); + + animation.timeline = null; + + assert_equals(animation.playState, 'idle'); + assert_equals(animation.startTime, null); +}, 'After clearing timeline on idle animation it is still idle'); + +test(t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + assert_true(animation.pending && animation.playState === 'running'); + + animation.timeline = null; + + assert_true(animation.pending && animation.playState === 'running'); +}, 'After clearing timeline on play-pending animation it is still pending'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + assert_true(animation.pending && animation.playState === 'running'); + + animation.timeline = null; + animation.timeline = document.timeline; + + assert_true(animation.pending && animation.playState === 'running'); + await animation.ready; + assert_true(!animation.pending && animation.playState === 'running'); +}, 'After clearing and re-setting timeline on play-pending animation it' + + ' begins to play'); + +test(t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + document.timeline); + animation.startTime = document.timeline.currentTime; + animation.pause(); + assert_true(animation.pending && animation.playState === 'paused'); + + animation.timeline = null; + + assert_true(animation.pending && animation.playState === 'paused'); +}, 'After clearing timeline on a pause-pending animation it is still pending'); + +promise_test(async t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + document.timeline); + animation.startTime = document.timeline.currentTime; + animation.pause(); + assert_true(animation.pending && animation.playState === 'paused'); + + animation.timeline = null; + animation.timeline = document.timeline; + + assert_true(animation.pending && animation.playState === 'paused'); + await animation.ready; + assert_true(!animation.pending && animation.playState === 'paused'); +}, 'After clearing and re-setting timeline on a pause-pending animation it' + + ' completes pausing'); + +promise_test(async t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + document.timeline); + const initialStartTime = document.timeline.currentTime - 50 * MS_PER_SEC; + animation.startTime = initialStartTime; + animation.pause(); + animation.play(); + + animation.timeline = null; + animation.timeline = document.timeline; + + await animation.ready; + assert_times_equal(animation.startTime, initialStartTime); +}, 'After clearing and re-setting timeline on an animation in the middle of' + + ' an aborted pause, it continues playing using the same start time'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/sync-start-times-ref.html b/testing/web-platform/tests/web-animations/timing-model/animations/sync-start-times-ref.html new file mode 100644 index 0000000000..fc843a132f --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/sync-start-times-ref.html @@ -0,0 +1,20 @@ + +<!DOCTYPE html> +<meta charset="UTF-8"> +<title>Reference for sync start times</title> +<style> + #notes { + position: absolute; + left: 0px; + top: 100px; + } +</style> + +<body> + <p id="notes"> + This test creates a pair of animations, starts the first animation and then + syncs the second animation to align with the first. The test passes if the + box associated with the first animation is completely occluded by the + second. + </p> +</body> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/sync-start-times.html b/testing/web-platform/tests/web-animations/timing-model/animations/sync-start-times.html new file mode 100644 index 0000000000..e9ef6762ea --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/sync-start-times.html @@ -0,0 +1,72 @@ + +<!DOCTYPE html> +<html class="reftest-wait"> +<meta charset="UTF-8"> +<title>sync start times</title> +<link rel="match" href="sync-start-times-ref.html"> +<script src="/common/reftest-wait.js"></script> +<style> + #box-1, #box-2 { + border: 1px solid white; + height: 40px; + left: 40px; + position: absolute; + top: 40px; + width: 40px; + /* To ensure Chrome to render the two boxes (one actively + animating and the other not) with the same subpixel offset + when there is subpixel translation during animation. */ + will-change: transform; + } + #box-1 { + background: blue; + z-index: 1; + } + #box-2 { + background: white; + z-index: 2; + } + #notes { + position: absolute; + left: 0px; + top: 100px; + } +</style> + +<body> + <div id="box-1"></div> + <div id="box-2"></div> + <p id="notes"> + This test creates a pair of animations, starts the first animation and then + syncs the second animation to align with the first. The test passes if the + box associated with the first animation is completely occluded by the + second. + </p> +</body> +<script> + onload = function() { + function createAnimation(elementId) { + const elem = document.getElementById(elementId); + const keyframes = [ + { transform: 'translateX(0px)' }, + { transform: 'translateX(200px)' } + ]; + const anim = elem.animate(keyframes, { duration: 1000 }); + anim.pause(); + return anim; + }; + + const anim1 = createAnimation('box-1'); + const anim2 = createAnimation('box-2'); + + anim1.currentTime = 500; + anim1.play(); + + anim1.ready.then(() => { + anim2.startTime = anim1.startTime; + requestAnimationFrame(() => { + takeScreenshot(); + }); + }); + }; +</script> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/the-current-time-of-an-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/the-current-time-of-an-animation.html new file mode 100644 index 0000000000..77a6b716d2 --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/the-current-time-of-an-animation.html @@ -0,0 +1,75 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>The current time of an animation</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#the-current-time-of-an-animation"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<script src="../../resources/timing-override.js"></script> +<body> +<div id="log"></div> +<script> +'use strict'; + +test(t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + document.timeline); + + animation.play(); + assert_equals(animation.currentTime, 0, + 'Current time returns the hold time set when entering the play-pending ' + + 'state'); +}, 'The current time returns the hold time when set'); + +promise_test(async t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + null); + + await animation.ready; + assert_equals(animation.currentTime, null); +}, 'The current time is unresolved when there is no associated timeline ' + + '(and no hold time is set)'); + +// FIXME: Test that the current time is unresolved when we have an inactive +// timeline if we find a way of creating an inactive timeline! + +test(t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + document.timeline); + + animation.startTime = null; + assert_equals(animation.currentTime, null); +}, 'The current time is unresolved when the start time is unresolved ' + + '(and no hold time is set)'); + +test(t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + document.timeline); + + animation.playbackRate = 2; + animation.startTime = document.timeline.currentTime - 25 * MS_PER_SEC; + + const timelineTime = document.timeline.currentTime; + const startTime = animation.startTime; + const playbackRate = animation.playbackRate; + assert_times_equal(animation.currentTime, + (timelineTime - startTime) * playbackRate, + 'Animation has a unresolved start time'); +}, 'The current time is calculated from the timeline time, start time and ' + + 'playback rate'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.playbackRate = 0; + + await animation.ready; + await waitForAnimationFrames(1); + assert_time_equals_literal(animation.currentTime, 0); +}, 'The current time does not progress if playback rate is 0'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/update-playback-rate-fast-ref.html b/testing/web-platform/tests/web-animations/timing-model/animations/update-playback-rate-fast-ref.html new file mode 100644 index 0000000000..e996815da8 --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/update-playback-rate-fast-ref.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<title>Reference for update playback rate zero</title> +<style> + #box { + background: green; + height: 40px; + width: 40px; + } +</style> +<body> + <div id="box"></div> + <p id="notes"> + This test creates a running animation and changes its playback rate + part way through. If the box remains red when the screenshot is captured + the test fails. + </p> +</body> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/update-playback-rate-fast.html b/testing/web-platform/tests/web-animations/timing-model/animations/update-playback-rate-fast.html new file mode 100644 index 0000000000..c3df1c1bf0 --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/update-playback-rate-fast.html @@ -0,0 +1,52 @@ + +<!DOCTYPE html> +<html class="reftest-wait"> +<meta charset="UTF-8"> +<title>Update playback rate zero</title> +<link rel="match" href="update-playback-rate-fast-ref.html"> +<script src="/common/reftest-wait.js"></script> +<script src="../../testcommon.js"></script> +<style> + #box { + background: green; + height: 40px; + width: 40px; + } +</style> +<body> + <div id="box"></div> + <p id="notes"> + This test creates a running animation and changes its playback rate + part way through. If the box remains red when the screenshot is captured + the test fails. + </p> +</body> +<script> + onload = async function() { + const box = document.getElementById('box'); + const duration = 2000; + const anim = + box.animate({ bacground: [ 'red', 'green' ] }, + { duration: duration, easing: 'steps(2, jump-none)' }); + anim.ready.then(() => { + const startTime = anim.timeline.currentTime; + waitForAnimationFrames(2).then(() => { + anim.updatePlaybackRate(2); + anim.ready.then(() => { + const updateTime = anim.timeline.currentTime; + const baseProgress = (updateTime - startTime) / duration; + const checkIfDone = () => { + const progress = + 2 * (anim.timeline.currentTime - updateTime) / duration + + baseProgress; + if (progress > 0.5) + takeScreenshot(); + else + requestAnimationFrame(checkIfDone); + }; + requestAnimationFrame(checkIfDone); + }); + }); + }); + }; +</script> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/update-playback-rate-zero-ref.html b/testing/web-platform/tests/web-animations/timing-model/animations/update-playback-rate-zero-ref.html new file mode 100644 index 0000000000..399fd5ce7d --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/update-playback-rate-zero-ref.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<title>Reference for update playback rate zero</title> +<style> + #box { + background: green; + height: 40px; + width: 40px; + } +</style> +<body> + <div id="box"></div> + <p id="notes"> + This test creates a running animation and halts its playback rate + part way through. If the box transitions to red the test fails. + </p> +</body> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/update-playback-rate-zero.html b/testing/web-platform/tests/web-animations/timing-model/animations/update-playback-rate-zero.html new file mode 100644 index 0000000000..db1544ee92 --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/update-playback-rate-zero.html @@ -0,0 +1,46 @@ + +<!DOCTYPE html> +<html class="reftest-wait"> +<meta charset="UTF-8"> +<title>Update playback rate zero</title> +<link rel="match" href="update-playback-rate-zero-ref.html"> +<script src="/common/reftest-wait.js"></script> +<script src="../../testcommon.js"></script> +<style> + #box { + background: green; + height: 40px; + width: 40px; + } +</style> +<body> + <div id="box"></div> + <p id="notes"> + This test creates a running animation and halts its playback rate + part way through. If the box transitions to red the test fails. + </p> +</body> +<script> + onload = async function() { + const box = document.getElementById('box'); + const duration = 2000; + const anim = + box.animate({ bacground: [ 'green', 'red' ] }, + { duration: duration, easing: 'steps(2, jump-none)' }); + anim.ready.then(() => { + const startTime = anim.timeline.currentTime; + waitForAnimationFrames(2).then(() => { + anim.updatePlaybackRate(0); + anim.ready.then(() => { + const checkIfDone = () => { + if (anim.timeline.currentTime - startTime > duration / 2) + takeScreenshot(); + else + requestAnimationFrame(checkIfDone); + }; + requestAnimationFrame(checkIfDone); + }); + }); + }); + }; +</script> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/updating-the-finished-state.html b/testing/web-platform/tests/web-animations/timing-model/animations/updating-the-finished-state.html new file mode 100644 index 0000000000..4d3cc7950b --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/updating-the-finished-state.html @@ -0,0 +1,457 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Updating the finished state</title> +<meta name="timeout" content="long"> +<link rel="help" href="https://drafts.csswg.org/web-animations/#updating-the-finished-state"> +<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'; + +// -------------------------------------------------------------------- +// +// TESTS FOR UPDATING THE HOLD TIME +// +// -------------------------------------------------------------------- + +// CASE 1: playback rate > 0 and current time >= target effect end +// (Also the start time is resolved and there is pending task) + +// Did seek = false +promise_test(async t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + + // Here and in the following tests we wait until ready resolves as + // otherwise we don't have a resolved start time. We test the case + // where the start time is unresolved in a subsequent test. + await anim.ready; + + // Seek to 1ms before the target end and then wait 1ms + anim.currentTime = 100 * MS_PER_SEC - 1; + await waitForAnimationFramesWithDelay(1); + + assert_equals(anim.currentTime, 100 * MS_PER_SEC, + 'Hold time is set to target end clamping current time'); +}, 'Updating the finished state when playing past end'); + +// Did seek = true +promise_test(async t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + + await anim.ready; + + anim.currentTime = 200 * MS_PER_SEC; + await waitForNextFrame(); + + assert_equals(anim.currentTime, 200 * MS_PER_SEC, + 'Hold time is set so current time should NOT change'); +}, 'Updating the finished state when seeking past end'); + +// Test current time == target end +// +// We can't really write a test for current time == target end with +// did seek = false since that would imply setting up an animation where +// the next animation frame time happens to exactly align with the target end. +// +// Fortunately, we don't need to test that case since even if the implementation +// fails to set the hold time on such a tick, it should be mostly unobservable +// (on the subsequent tick the hold time will be set to the same value anyway). + +// Did seek = true +promise_test(async t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + await anim.ready; + + anim.currentTime = 100 * MS_PER_SEC; + await waitForNextFrame(); + + assert_equals(anim.currentTime, 100 * MS_PER_SEC, + 'Hold time is set so current time should NOT change'); +}, 'Updating the finished state when seeking exactly to end'); + + +// CASE 2: playback rate < 0 and current time <= 0 +// (Also the start time is resolved and there is pending task) + +// Did seek = false +promise_test(async t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + anim.playbackRate = -1; + anim.play(); // Make sure animation is not initially finished + + await anim.ready; + + // Seek to 1ms before 0 and then wait 1ms + anim.currentTime = 1; + await waitForAnimationFramesWithDelay(1); + + assert_equals(anim.currentTime, 0 * MS_PER_SEC, + 'Hold time is set to zero clamping current time'); +}, 'Updating the finished state when playing in reverse past zero'); + +// Did seek = true +promise_test(async t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + anim.playbackRate = -1; + anim.play(); + + await anim.ready; + + anim.currentTime = -100 * MS_PER_SEC; + await waitForNextFrame(); + + assert_equals(anim.currentTime, -100 * MS_PER_SEC, + 'Hold time is set so current time should NOT change'); +}, 'Updating the finished state when seeking a reversed animation past zero'); + +// As before, it's difficult to test current time == 0 for did seek = false but +// it doesn't really matter. + +// Did seek = true +promise_test(async t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + anim.playbackRate = -1; + anim.play(); + await anim.ready; + + anim.currentTime = 0; + await waitForNextFrame(); + + assert_equals(anim.currentTime, 0 * MS_PER_SEC, + 'Hold time is set so current time should NOT change'); +}, 'Updating the finished state when seeking a reversed animation exactly' + + ' to zero'); + +// CASE 3: playback rate > 0 and current time < target end OR +// playback rate < 0 and current time > 0 +// (Also the start time is resolved and there is pending task) + +// Did seek = false; playback rate > 0 +promise_test(async t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + + // We want to test that the hold time is cleared so first we need to + // put the animation in a state where the hold time is set. + anim.finish(); + await anim.ready; + + assert_equals(anim.currentTime, 100 * MS_PER_SEC, + 'Hold time is initially set'); + // Then extend the duration so that the hold time is cleared and on + // the next tick the current time will increase. + anim.effect.updateTiming({ + duration: anim.effect.getComputedTiming().duration * 2, + }); + await waitForNextFrame(); + + assert_greater_than(anim.currentTime, 100 * MS_PER_SEC, + 'Hold time is not set so current time should increase'); +}, 'Updating the finished state when playing before end'); + +// Did seek = true; playback rate > 0 +promise_test(async t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + anim.finish(); + await anim.ready; + + anim.currentTime = 50 * MS_PER_SEC; + // When did seek = true, updating the finished state: (i) updates + // the animation's start time and (ii) clears the hold time. + // We can test both by checking that the currentTime is initially + // updated and then increases. + assert_equals(anim.currentTime, 50 * MS_PER_SEC, 'Start time is updated'); + await waitForNextFrame(); + + assert_greater_than(anim.currentTime, 50 * MS_PER_SEC, + 'Hold time is not set so current time should increase'); +}, 'Updating the finished state when seeking before end'); + +// Did seek = false; playback rate < 0 +// +// Unfortunately it is not possible to test this case. We need to have +// a hold time set, a resolved start time, and then perform some +// operation that updates the finished state with did seek set to true. +// +// However, the only situation where this could arrive is when we +// replace the timeline and that procedure is likely to change. For all +// other cases we either have an unresolved start time (e.g. when +// paused), we don't have a set hold time (e.g. regular playback), or +// the current time is zero (and anything that gets us out of that state +// will set did seek = true). + +// Did seek = true; playback rate < 0 +promise_test(async t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + anim.playbackRate = -1; + await anim.ready; + + anim.currentTime = 50 * MS_PER_SEC; + assert_equals(anim.currentTime, 50 * MS_PER_SEC, 'Start time is updated'); + await waitForNextFrame(); + + assert_less_than(anim.currentTime, 50 * MS_PER_SEC, + 'Hold time is not set so current time should decrease'); +}, 'Updating the finished state when seeking a reversed animation before end'); + +// CASE 4: playback rate == 0 + +// current time < 0 +promise_test(async t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + anim.playbackRate = 0; + await anim.ready; + + anim.currentTime = -100 * MS_PER_SEC; + await waitForNextFrame(); + + assert_equals(anim.currentTime, -100 * MS_PER_SEC, + 'Hold time should not be cleared so current time should' + + ' NOT change'); +}, 'Updating the finished state when playback rate is zero and the' + + ' current time is less than zero'); + +// current time < target end +promise_test(async t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + anim.playbackRate = 0; + await anim.ready; + + anim.currentTime = 50 * MS_PER_SEC; + await waitForNextFrame(); + + assert_equals(anim.currentTime, 50 * MS_PER_SEC, + 'Hold time should not be cleared so current time should' + + ' NOT change'); +}, 'Updating the finished state when playback rate is zero and the' + + ' current time is less than end'); + +// current time > target end +promise_test(async t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + anim.playbackRate = 0; + await anim.ready; + + anim.currentTime = 200 * MS_PER_SEC; + await waitForNextFrame(); + + assert_equals(anim.currentTime, 200 * MS_PER_SEC, + 'Hold time should not be cleared so current time should' + + ' NOT change'); +}, 'Updating the finished state when playback rate is zero and the' + + ' current time is greater than end'); + +// CASE 5: current time unresolved + +promise_test(async t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + anim.cancel(); + // Trigger a change that will cause the "update the finished state" + // procedure to run. + anim.effect.updateTiming({ duration: 200 * MS_PER_SEC }); + assert_equals(anim.currentTime, null, + 'The animation hold time / start time should not be updated'); + // The "update the finished state" procedure is supposed to run after any + // change to timing, but just in case an implementation defers that, let's + // wait a frame and check that the hold time / start time has still not been + // updated. + await waitForAnimationFrames(1); + + assert_equals(anim.currentTime, null, + 'The animation hold time / start time should not be updated'); +}, 'Updating the finished state when current time is unresolved'); + +// CASE 6: has a pending task + +test(t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + anim.cancel(); + anim.currentTime = 75 * MS_PER_SEC; + anim.play(); + // We now have a pending task and a resolved current time. + // + // In the next step we will adjust the timing so that the current time + // is greater than the target end. At this point the "update the finished + // state" procedure should run and if we fail to check for a pending task + // we will set the hold time to the target end, i.e. 50ms. + anim.effect.updateTiming({ duration: 50 * MS_PER_SEC }); + assert_equals(anim.currentTime, 75 * MS_PER_SEC, + 'Hold time should not be updated'); +}, 'Updating the finished state when there is a pending task'); + +// CASE 7: start time unresolved + +// Did seek = false +promise_test(async t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + anim.cancel(); + // Make it so that only the start time is unresolved (to avoid overlapping + // with the test case where current time is unresolved) + anim.currentTime = 150 * MS_PER_SEC; + // Trigger a change that will cause the "update the finished state" + // procedure to run (did seek = false). + anim.effect.updateTiming({ duration: 200 * MS_PER_SEC }); + await waitForAnimationFrames(1); + + assert_equals(anim.currentTime, 150 * MS_PER_SEC, + 'The animation hold time should not be updated'); + assert_equals(anim.startTime, null, + 'The animation start time should not be updated'); +}, 'Updating the finished state when start time is unresolved and' + + ' did seek = false'); + +// Did seek = true +test(t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + anim.cancel(); + anim.currentTime = 150 * MS_PER_SEC; + // Trigger a change that will cause the "update the finished state" + // procedure to run. + anim.currentTime = 50 * MS_PER_SEC; + assert_equals(anim.currentTime, 50 * MS_PER_SEC, + 'The animation hold time should not be updated'); + assert_equals(anim.startTime, null, + 'The animation start time should not be updated'); +}, 'Updating the finished state when start time is unresolved and' + + ' did seek = true'); + +// -------------------------------------------------------------------- +// +// TESTS FOR RUNNING FINISH NOTIFICATION STEPS +// +// -------------------------------------------------------------------- + +function waitForFinishEventAndPromise(animation) { + const eventPromise = new Promise(resolve => { + animation.onfinish = resolve; + }); + return Promise.all([eventPromise, animation.finished]); +} + +promise_test(t => { + const animation = createDiv(t).animate(null, 1); + animation.onfinish = + t.unreached_func('Seeking to finish should not fire finish event'); + animation.finished.then( + t.unreached_func('Seeking to finish should not resolve finished promise')); + animation.currentTime = 1; + animation.currentTime = 0; + animation.pause(); + return waitForAnimationFrames(3); +}, 'Finish notification steps don\'t run when the animation seeks to finish' + + ' and then seeks back again'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 1); + await animation.ready; + + return waitForFinishEventAndPromise(animation); +}, 'Finish notification steps run when the animation completes normally'); + +promise_test(async t => { + const effect = new KeyframeEffect(null, null, 1); + const animation = new Animation(effect, document.timeline); + animation.play(); + await animation.ready; + + return waitForFinishEventAndPromise(animation); +}, 'Finish notification steps run when an animation without a target' + + ' effect completes normally'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 1); + await animation.ready; + + animation.currentTime = 10; + return waitForFinishEventAndPromise(animation); +}, 'Finish notification steps run when the animation seeks past finish'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 1); + await animation.ready; + + // Register for notifications now since once we seek away from being + // finished the 'finished' promise will be replaced. + const finishNotificationSteps = waitForFinishEventAndPromise(animation); + animation.finish(); + animation.currentTime = 0; + animation.pause(); + return finishNotificationSteps; +}, 'Finish notification steps run when the animation completes with .finish(),' + + ' even if we then seek away'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 1); + const initialFinishedPromise = animation.finished; + await animation.finished; + + animation.currentTime = 0; + assert_not_equals(initialFinishedPromise, animation.finished); +}, 'Animation finished promise is replaced after seeking back to start'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 1); + const initialFinishedPromise = animation.finished; + await animation.finished; + + animation.play(); + assert_not_equals(initialFinishedPromise, animation.finished); +}, 'Animation finished promise is replaced after replaying from start'); + +async_test(t => { + const animation = createDiv(t).animate(null, 1); + animation.onfinish = event => { + animation.currentTime = 0; + animation.onfinish = event => { + t.done(); + }; + }; +}, 'Animation finish event is fired again after seeking back to start'); + +async_test(t => { + const animation = createDiv(t).animate(null, 1); + animation.onfinish = event => { + animation.play(); + animation.onfinish = event => { + t.done(); + }; + }; +}, 'Animation finish event is fired again after replaying from start'); + +async_test(t => { + const anim = createDiv(t).animate(null, + { duration: 100000, endDelay: 50000 }); + anim.onfinish = t.step_func(event => { + assert_unreached('finish event should not be fired'); + }); + + anim.ready.then(() => { + anim.currentTime = 100000; + return waitForAnimationFrames(2); + }).then(t.step_func(() => { + t.done(); + })); +}, 'finish event is not fired at the end of the active interval when the' + + ' endDelay has not expired'); + +async_test(t => { + const anim = createDiv(t).animate(null, + { duration: 100000, endDelay: 30000 }); + anim.ready.then(() => { + anim.currentTime = 110000; // during endDelay + anim.onfinish = t.step_func(event => { + assert_unreached('onfinish event should not be fired during endDelay'); + }); + return waitForAnimationFrames(2); + }).then(t.step_func(() => { + anim.onfinish = t.step_func(event => { + t.done(); + }); + anim.currentTime = 130000; // after endTime + })); +}, 'finish event is fired after the endDelay has expired'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/timing-model/time-transformations/transformed-progress.html b/testing/web-platform/tests/web-animations/timing-model/time-transformations/transformed-progress.html new file mode 100644 index 0000000000..960e333c09 --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/time-transformations/transformed-progress.html @@ -0,0 +1,391 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Transformed progress</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#calculating-the-transformed-progress"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<script src="../../resources/easing-tests.js"></script> +<body> +<div id="log"></div> +<div id="target"></div> +<script> +'use strict'; + +for (const params of gEasingTests) { + test(t => { + const target = createDiv(t); + const anim = target.animate(null, { duration: 1000, + fill: 'forwards', + easing: params.easing }); + + for (const sampleTime of [0, 250, 500, 750, 1000]) { + anim.currentTime = sampleTime; + const portion = sampleTime / anim.effect.getComputedTiming().duration; + const expectedProgress = params.easingFunction(portion); + assert_approx_equals(anim.effect.getComputedTiming().progress, + expectedProgress, + 0.01, + 'The progress should be approximately ' + + `${expectedProgress} at ${sampleTime}ms`); + } + }, `Transformed progress for ${params.desc}`); +} + +// Additional tests for various boundary conditions of step timing functions. + +const gStepTimingFunctionTests = [ + { + description: 'Test bounds point of step-start easing', + effect: { + delay: 1000, + duration: 1000, + fill: 'both', + easing: 'steps(2, start)' + }, + conditions: [ + { currentTime: 0, progress: 0 }, + { currentTime: 999, progress: 0 }, + { currentTime: 1000, progress: 0.5 }, + { currentTime: 1499, progress: 0.5 }, + { currentTime: 1500, progress: 1 }, + { currentTime: 2000, progress: 1 } + ] + }, + { + description: 'Test bounds point of step-start easing with reverse direction', + effect: { + delay: 1000, + duration: 1000, + fill: 'both', + direction: 'reverse', + easing: 'steps(2, start)' + }, + conditions: [ + { currentTime: 0, progress: 1 }, + { currentTime: 1001, progress: 1 }, + { currentTime: 1500, progress: 1 }, + { currentTime: 1501, progress: 0.5 }, + { currentTime: 2000, progress: 0 }, + { currentTime: 2500, progress: 0 } + ] + }, + { + description: 'Test bounds point of step-start easing ' + + 'with iterationStart not at a transition point', + effect: { + delay: 1000, + duration: 1000, + fill: 'both', + iterationStart: 0.25, + easing: 'steps(2, start)' + }, + conditions: [ + { currentTime: 0, progress: 0.5 }, + { currentTime: 999, progress: 0.5 }, + { currentTime: 1000, progress: 0.5 }, + { currentTime: 1249, progress: 0.5 }, + { currentTime: 1250, progress: 1 }, + { currentTime: 1749, progress: 1 }, + { currentTime: 1750, progress: 0.5 }, + { currentTime: 2000, progress: 0.5 }, + { currentTime: 2500, progress: 0.5 }, + ] + }, + { + description: 'Test bounds point of step-start easing ' + + 'with iterationStart and delay', + effect: { + delay: 1000, + duration: 1000, + fill: 'both', + iterationStart: 0.5, + easing: 'steps(2, start)' + }, + conditions: [ + { currentTime: 0, progress: 0.5 }, + { currentTime: 999, progress: 0.5 }, + { currentTime: 1000, progress: 1 }, + { currentTime: 1499, progress: 1 }, + { currentTime: 1500, progress: 0.5 }, + { currentTime: 2000, progress: 1 } + ] + }, + { + description: 'Test bounds point of step-start easing ' + + 'with iterationStart and reverse direction', + effect: { + delay: 1000, + duration: 1000, + fill: 'both', + iterationStart: 0.5, + direction: 'reverse', + easing: 'steps(2, start)' + }, + conditions: [ + { currentTime: 0, progress: 1 }, + { currentTime: 1000, progress: 1 }, + { currentTime: 1001, progress: 0.5 }, + { currentTime: 1499, progress: 0.5 }, + { currentTime: 1500, progress: 1 }, + { currentTime: 1999, progress: 1 }, + { currentTime: 2000, progress: 0.5 }, + { currentTime: 2500, progress: 0.5 } + ] + }, + { + description: 'Test bounds point of step(4, start) easing ' + + 'with iterationStart 0.75 and delay', + effect: { + duration: 1000, + fill: 'both', + delay: 1000, + iterationStart: 0.75, + easing: 'steps(4, start)' + }, + conditions: [ + { currentTime: 0, progress: 0.75 }, + { currentTime: 999, progress: 0.75 }, + { currentTime: 1000, progress: 1 }, + { currentTime: 2000, progress: 1 }, + { currentTime: 2500, progress: 1 } + ] + }, + { + description: 'Test bounds point of step-start easing ' + + 'with alternate direction', + effect: { + duration: 1000, + fill: 'both', + delay: 1000, + iterations: 2, + iterationStart: 1.5, + direction: 'alternate', + easing: 'steps(2, start)' + }, + conditions: [ + { currentTime: 0, progress: 1 }, + { currentTime: 1000, progress: 1 }, + { currentTime: 1001, progress: 0.5 }, + { currentTime: 2999, progress: 1 }, + { currentTime: 3000, progress: 0.5 }, + { currentTime: 3500, progress: 0.5 } + ] + }, + { + description: 'Test bounds point of step-start easing ' + + 'with alternate-reverse direction', + effect: { + duration: 1000, + fill: 'both', + delay: 1000, + iterations: 2, + iterationStart: 0.5, + direction: 'alternate-reverse', + easing: 'steps(2, start)' + }, + conditions: [ + { currentTime: 0, progress: 1 }, + { currentTime: 1000, progress: 1 }, + { currentTime: 1001, progress: 0.5 }, + { currentTime: 2999, progress: 1 }, + { currentTime: 3000, progress: 0.5 }, + { currentTime: 3500, progress: 0.5 } + ] + }, + { + description: 'Test bounds point of step-end easing', + effect: { + delay: 1000, + duration: 1000, + fill: 'both', + easing: 'steps(2, end)' + }, + conditions: [ + { currentTime: 0, progress: 0 }, + { currentTime: 999, progress: 0 }, + { currentTime: 1000, progress: 0 }, + { currentTime: 1499, progress: 0 }, + { currentTime: 1500, progress: 0.5 }, + { currentTime: 2000, progress: 1 } + ] + }, + { + description: 'Test bounds point of step-end easing ' + + 'with iterationStart and delay', + effect: { + duration: 1000, + fill: 'both', + delay: 1000, + iterationStart: 0.5, + easing: 'steps(2, end)' + }, + conditions: [ + { currentTime: 0, progress: 0 }, + { currentTime: 999, progress: 0 }, + { currentTime: 1000, progress: 0.5 }, + { currentTime: 1499, progress: 0.5 }, + { currentTime: 1500, progress: 0 }, + { currentTime: 1999, progress: 0 }, + { currentTime: 2000, progress: 0.5 }, + { currentTime: 2500, progress: 0.5 } + ] + }, + { + description: 'Test bounds point of step-end easing ' + + 'with iterationStart not at a transition point', + effect: { + delay: 1000, + duration: 1000, + fill: 'both', + iterationStart: 0.75, + easing: 'steps(2, end)' + }, + conditions: [ + { currentTime: 0, progress: 0.5 }, + { currentTime: 999, progress: 0.5 }, + { currentTime: 1000, progress: 0.5 }, + { currentTime: 1249, progress: 0.5 }, + { currentTime: 1250, progress: 0 }, + { currentTime: 1749, progress: 0 }, + { currentTime: 1750, progress: 0.5 }, + { currentTime: 2000, progress: 0.5 }, + { currentTime: 2500, progress: 0.5 }, + ] + }, + { + description: 'Test bounds point of steps(jump-both) easing', + effect: { + delay: 1000, + duration: 1000, + fill: 'both', + easing: 'steps(2, jump-both)' + }, + conditions: [ + { currentTime: 0, progress: 0 }, + { currentTime: 999, progress: 0 }, + { currentTime: 1000, progress: 1/3 }, + { currentTime: 1499, progress: 1/3 }, + { currentTime: 1500, progress: 2/3 }, + { currentTime: 2000, progress: 1 } + ] + }, + { + description: 'Test bounds point of steps(jump-both) easing ' + + 'with iterationStart and delay', + effect: { + duration: 1000, + fill: 'both', + delay: 1000, + iterationStart: 0.5, + easing: 'steps(2, jump-both)' + }, + conditions: [ + { currentTime: 0, progress: 1/3 }, + { currentTime: 999, progress: 1/3 }, + { currentTime: 1000, progress: 2/3 }, + { currentTime: 1499, progress: 2/3 }, + { currentTime: 1500, progress: 1/3 }, + { currentTime: 1999, progress: 1/3 }, + { currentTime: 2000, progress: 2/3 }, + { currentTime: 2500, progress: 2/3 } + ] + }, + { + description: 'Test bounds point of steps(jump-both) easing ' + + 'with iterationStart not at a transition point', + effect: { + delay: 1000, + duration: 1000, + fill: 'both', + iterationStart: 0.75, + easing: 'steps(2, jump-both)' + }, + conditions: [ + { currentTime: 0, progress: 2/3 }, + { currentTime: 999, progress: 2/3 }, + { currentTime: 1000, progress: 2/3 }, + { currentTime: 1249, progress: 2/3 }, + { currentTime: 1250, progress: 1/3 }, + { currentTime: 1749, progress: 1/3 }, + { currentTime: 1750, progress: 2/3 }, + { currentTime: 2000, progress: 2/3 }, + { currentTime: 2500, progress: 2/3 } + ] + }, + { + description: 'Test bounds point of steps(jump-none) easing', + effect: { + delay: 1000, + duration: 1000, + fill: 'both', + easing: 'steps(2, jump-none)' + }, + conditions: [ + { currentTime: 0, progress: 0 }, + { currentTime: 1000, progress: 0 }, + { currentTime: 1499, progress: 0 }, + { currentTime: 1500, progress: 1 }, + { currentTime: 2000, progress: 1 } + ] + }, + { + description: 'Test bounds point of steps(jump-none) easing ' + + 'with iterationStart and delay', + effect: { + duration: 1000, + fill: 'both', + delay: 1000, + iterationStart: 0.5, + easing: 'steps(2, jump-none)' + }, + conditions: [ + { currentTime: 0, progress: 0 }, + { currentTime: 999, progress: 0 }, + { currentTime: 1000, progress: 1 }, + { currentTime: 1499, progress: 1 }, + { currentTime: 1500, progress: 0 }, + { currentTime: 1999, progress: 0 }, + { currentTime: 2000, progress: 1 }, + { currentTime: 2500, progress: 1 } + ] + }, + { + description: 'Test bounds point of steps(jump-none) easing ' + + 'with iterationStart not at a transition point', + effect: { + delay: 1000, + duration: 1000, + fill: 'both', + iterationStart: 0.75, + easing: 'steps(2, jump-none)' + }, + conditions: [ + { currentTime: 0, progress: 1 }, + { currentTime: 999, progress: 1 }, + { currentTime: 1000, progress: 1 }, + { currentTime: 1249, progress: 1 }, + { currentTime: 1250, progress: 0 }, + { currentTime: 1749, progress: 0 }, + { currentTime: 1750, progress: 1 }, + { currentTime: 2000, progress: 1 }, + { currentTime: 2500, progress: 1 } + ] + }, +]; + +for (const options of gStepTimingFunctionTests) { + test(t => { + const target = createDiv(t); + const animation = target.animate(null, options.effect); + for (const condition of options.conditions) { + animation.currentTime = condition.currentTime; + assert_equals(animation.effect.getComputedTiming().progress, + condition.progress, + `Progress at ${animation.currentTime}ms`); + } + }, options.description); +} + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/timing-model/timelines/document-timelines.html b/testing/web-platform/tests/web-animations/timing-model/timelines/document-timelines.html new file mode 100644 index 0000000000..c71d73331c --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/timelines/document-timelines.html @@ -0,0 +1,50 @@ +<!doctype html> +<meta charset=utf-8> +<title>Document timelines</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#document-timelines"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<div id="log"></div> +<script> +'use strict'; + +async_test(t => { + assert_greater_than_equal(document.timeline.currentTime, 0, + 'The current time is initially is positive or zero'); + + // document.timeline.currentTime should be set even before document + // load fires. We expect this code to be run before document load and hence + // the above assertion is sufficient. + // If the following assertion fails, this test needs to be redesigned. + assert_not_equals(document.readyState, 'complete', + 'Test is running prior to document load'); + + // Test that the document timeline's current time is measured from + // navigationStart. + // + // We can't just compare document.timeline.currentTime to + // window.performance.now() because currentTime is only updated on a sample + // so we use requestAnimationFrame instead. + window.requestAnimationFrame(rafTime => { + t.step(() => { + assert_equals(document.timeline.currentTime, rafTime, + 'The current time matches requestAnimationFrame time'); + }); + t.done(); + }); +}, 'Document timelines report current time relative to navigationStart'); + +async_test(t => { + window.requestAnimationFrame(rafTime => { + t.step(() => { + const iframe = document.createElement('iframe'); + document.body.appendChild(iframe); + assert_greater_than_equal(iframe.contentDocument.timeline.currentTime, 0, + 'The current time of a new iframe is initially is positive or zero'); + }); + t.done(); + }); +}, 'Child frames do not report negative initial times'); + +</script> diff --git a/testing/web-platform/tests/web-animations/timing-model/timelines/resources/target-frame.html b/testing/web-platform/tests/web-animations/timing-model/timelines/resources/target-frame.html new file mode 100644 index 0000000000..18ee4fd8a2 --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/timelines/resources/target-frame.html @@ -0,0 +1,76 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <style type="text/css"> + #target { + background: green; + height: 50px; + width: 50px; + } + </style> +</head> +<body> + <div id="target"></div> +</body> +<script src="../../../testcommon.js"></script> +<script type="text/javascript"> + function sendResult(message) { + top.postMessage(message, '*'); + } + + function waitForAnimationReady(anim) { + // Resolution of the ready promise, though UA dependent, is expected to + // happen within a few frames. Throttling rendering of the frame owning + // the animation's timeline may delay resolution of the ready promise, + // resulting in a test failure. + let frameTimeout = 10; + let resolved = false; + return new Promise((resolve, reject) => { + anim.ready.then(() => { + resolved = true; + resolve('PASS'); + }); + const tick = () => { + requestAnimationFrame(() => { + if (!resolved) { + if (--frameTimeout == 0) + resolve('FAIL: Animation is still pending'); + else + tick(); + } + }); + }; + tick(); + }); + } + + function verifyAnimationIsUpdating() { + return new Promise(resolve => { + waitForAnimationFrames(3).then(() => { + const opacity = getComputedStyle(target).opacity; + const result = + (opacity == 1) ? 'FAIL: opacity remained unchanged' : 'PASS'; + resolve(result); + }); + }); + } + + async function runTest() { + const anim = document.getAnimations()[0]; + if (!anim) { + setResult('FAIL: Failed to create animation'); + return; + } + waitForAnimationReady(anim).then(result => { + if (result != 'PASS') { + sendResult(result); + return; + } + verifyAnimationIsUpdating().then(result => { + sendResult(result); + }); + }); + } +</script> +</html> diff --git a/testing/web-platform/tests/web-animations/timing-model/timelines/resources/timeline-frame.html b/testing/web-platform/tests/web-animations/timing-model/timelines/resources/timeline-frame.html new file mode 100644 index 0000000000..9c8cdabc9d --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/timelines/resources/timeline-frame.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> +</head> +<script type="text/javascript"> + const targetWindow = window.top.a; + const element = targetWindow.document.getElementById('target'); + const keyframes = { opacity: [1, 0.2] }; + const options = { + duration: 1000, + // Use this document's timeline rather then the timeline of the + // element's document. + timeline: document.timeline, + fill: 'forwards' + }; + element.animate(keyframes, options); + targetWindow.runTest(); +</script> +</html> diff --git a/testing/web-platform/tests/web-animations/timing-model/timelines/sibling-iframe-timeline.html b/testing/web-platform/tests/web-animations/timing-model/timelines/sibling-iframe-timeline.html new file mode 100644 index 0000000000..8a611c8579 --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/timelines/sibling-iframe-timeline.html @@ -0,0 +1,58 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Animate using sibling iframe's timeline</title> +</head> +<body></body> +<script src="/common/get-host-info.sub.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script type="text/javascript"> + 'use strict'; + + function crossSiteUrl(filename) { + const url = + get_host_info().HTTP_REMOTE_ORIGIN + + '/web-animations/timing-model/timelines/resources/' + + filename; + return url; + } + + function loadFrame(name, path, hidden) { + return new Promise(resolve => { + const frame = document.createElement('iframe'); + if (hidden) + frame.style = 'visibility: hidden;'; + frame.name = name; + document.body.appendChild(frame); + frame.onload = () => { + resolve(); + } + frame.src = crossSiteUrl(path); + }); + } + + function waitForTestResults() { + return new Promise(resolve => { + const listener = (evt) => { + window.removeEventListener('message', listener); + resolve(evt.data); + }; + window.addEventListener('message', listener); + }); + } + + promise_test(async t => { + const promise = waitForTestResults().then((data) => { + assert_equals(data, 'PASS'); + }); + // Animate an element in frame A. + await loadFrame('a', 'target-frame.html', false); + // Animation's timeline is in hidden frame B. + await loadFrame('b', 'timeline-frame.html', true); + + return promise; + }, 'animation tied to another frame\'s timeline runs properly'); +</script> +</html> diff --git a/testing/web-platform/tests/web-animations/timing-model/timelines/timelines.html b/testing/web-platform/tests/web-animations/timing-model/timelines/timelines.html new file mode 100644 index 0000000000..1609f85939 --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/timelines/timelines.html @@ -0,0 +1,144 @@ +<!doctype html> +<meta charset=utf-8> +<title>Timelines</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#timelines"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<style> +@keyframes opacity-animation { + from { opacity: 1; } + to { opacity: 0; } +} +</style> +<div id="log"></div> +<script> +'use strict'; + +promise_test(t => { + const valueAtStart = document.timeline.currentTime; + const timeAtStart = window.performance.now(); + while (window.performance.now() - timeAtStart < 50) { + // Wait 50ms + } + assert_equals(document.timeline.currentTime, valueAtStart, + 'Timeline time does not change within an animation frame'); + return waitForAnimationFrames(1).then(() => { + assert_greater_than(document.timeline.currentTime, valueAtStart, + 'Timeline time increases between animation frames'); + }); +}, 'Timeline time increases once per animation frame'); + +async_test(t => { + const iframe = document.createElement('iframe'); + iframe.width = 10; + iframe.height = 10; + + iframe.addEventListener('load', t.step_func(() => { + const iframeTimeline = iframe.contentDocument.timeline; + const valueAtStart = iframeTimeline.currentTime; + const timeAtStart = window.performance.now(); + while (iframe.contentWindow.performance.now() - timeAtStart < 50) { + // Wait 50ms + } + assert_equals(iframeTimeline.currentTime, valueAtStart, + 'Timeline time within an iframe does not change within an ' + + ' animation frame'); + + iframe.contentWindow.requestAnimationFrame(t.step_func_done(() => { + assert_greater_than(iframeTimeline.currentTime, valueAtStart, + 'Timeline time within an iframe increases between animation frames'); + iframe.remove(); + })); + })); + + document.body.appendChild(iframe); +}, 'Timeline time increases once per animation frame in an iframe'); + +async_test(t => { + const startTime = document.timeline.currentTime; + let firstRafTime; + + requestAnimationFrame(() => { + t.step(() => { + assert_greater_than_equal(document.timeline.currentTime, startTime, + 'Timeline time should have progressed'); + firstRafTime = document.timeline.currentTime; + }); + }); + + requestAnimationFrame(() => { + t.step(() => { + assert_equals(document.timeline.currentTime, firstRafTime, + 'Timeline time should be the same'); + }); + t.done(); + }); +}, 'Timeline time should be the same for all RAF callbacks in an animation' + + ' frame'); + +promise_test(async t => { + // A microtask checkpoint is run as part of the process of updating + // timelines to ensure that any microtasks queued during promise + // resolution are handled before dispatching animation events. + const div = createDiv(t); + const events = []; + let microtaskFrameTime = undefined; + let finishFrameTime = undefined; + const waitForMicrotask = (animation) => { + return new Promise(resolve => { + queueMicrotask(() => { + events.push('microtask'); + microtaskFrameTime = document.timeline.currentTime; + resolve(); + }); + }); + } + const waitForFinishEvent = (animation) => { + return new Promise(resolve => { + animation.onfinish = (event) => { + events.push('finish'); + finishFrameTime = event.timelineTime; + resolve(); + }; + }); + } + + await waitForNextFrame(); + + const animation = div.animate({ opacity: [0, 1] }, 1000 * MS_PER_SEC); + const finishPromise = waitForFinishEvent(animation); + await animation.ready; + + // Advance the timing to effect end, to asynchronously queue up a finish task. + // Queue up microtask, which must be processed ahead of the finish event. + // See "Perform a microtask checkpoint" step in + // https://www.w3.org/TR/web-animations-1/#timelines. + animation.currentTime = animation.effect.getComputedTiming().duration; + const microtaskPromise = waitForMicrotask(animation); + await Promise.all([finishPromise, microtaskPromise]); + assert_array_equals(events, ['microtask', 'finish']); + assert_times_equal(microtaskFrameTime, finishFrameTime); + +}, 'Performs a microtask checkpoint after updating timelines'); + +async_test(t => { + const div = createDiv(t); + let readyPromiseRan = false; + let finishedPromiseRan = false; + div.style.animation = 'opacity-animation 1ms'; + let anim = div.getAnimations()[0]; + anim.ready.then(t.step_func(() => { + readyPromiseRan = true; + })); + div.addEventListener('animationstart', t.step_func(() => { + assert_true(readyPromiseRan, 'It should run ready promise before animationstart event'); + })); + anim.finished.then(t.step_func(() => { + finishedPromiseRan = true; + })); + div.addEventListener('animationend', t.step_func_done(() => { + assert_true(finishedPromiseRan, 'It should run finished promise before animationend event'); + })); +}, 'Runs finished promise before animation events'); +</script> diff --git a/testing/web-platform/tests/web-animations/timing-model/timelines/update-and-send-events-replacement.html b/testing/web-platform/tests/web-animations/timing-model/timelines/update-and-send-events-replacement.html new file mode 100644 index 0000000000..d6ed734831 --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/timelines/update-and-send-events-replacement.html @@ -0,0 +1,1017 @@ +<!doctype html> +<meta charset=utf-8> +<title>Update animations and send events (replacement)</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#update-animations-and-send-events"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<style> +@keyframes opacity-animation { + to { opacity: 1 } +} +</style> +<div id="log"></div> +<script> +'use strict'; + +promise_test(async t => { + const div = createDiv(t); + + const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + await animA.finished; + + assert_equals(animA.replaceState, 'removed'); + assert_equals(animB.replaceState, 'active'); +}, 'Removes an animation when another covers the same properties'); + +promise_test(async t => { + const div = createDiv(t); + + const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + await animA.finished; + + assert_equals(animA.replaceState, 'active'); + + const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + await animB.finished; + + assert_equals(animA.replaceState, 'removed'); + assert_equals(animB.replaceState, 'active'); +}, 'Removes an animation after another animation finishes'); + +promise_test(async t => { + const div = createDiv(t); + + const animA = div.animate( + { opacity: 1, width: '100px' }, + { duration: 1, fill: 'forwards' } + ); + await animA.finished; + + assert_equals(animA.replaceState, 'active'); + + const animB = div.animate( + { width: '200px' }, + { duration: 1, fill: 'forwards' } + ); + await animB.finished; + + assert_equals(animA.replaceState, 'active'); + assert_equals(animB.replaceState, 'active'); + + const animC = div.animate( + { opacity: 0.5 }, + { duration: 1, fill: 'forwards' } + ); + await animC.finished; + + assert_equals(animA.replaceState, 'removed'); + assert_equals(animB.replaceState, 'active'); + assert_equals(animC.replaceState, 'active'); +}, 'Removes an animation after multiple other animations finish'); + +promise_test(async t => { + const div = createDiv(t); + + const animA = div.animate( + { opacity: 1 }, + { duration: 100 * MS_PER_SEC, fill: 'forwards' } + ); + const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + await animB.finished; + + assert_equals(animB.replaceState, 'active'); + assert_equals(animB.replaceState, 'active'); + + // Seek animA to just before it finishes since we want to test the behavior + // when the animation finishes by the ticking of the timeline, not by seeking + // (that is covered in a separate test). + + animA.currentTime = 99.99 * MS_PER_SEC; + await animA.finished; + + assert_equals(animA.replaceState, 'removed'); + assert_equals(animB.replaceState, 'active'); +}, 'Removes an animation after it finishes'); + +promise_test(async t => { + const div = createDiv(t); + + const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + const animB = div.animate( + { opacity: 1 }, + { duration: 100 * MS_PER_SEC, fill: 'forwards' } + ); + await animA.finished; + + assert_equals(animA.replaceState, 'active'); + assert_equals(animB.replaceState, 'active'); + + animB.finish(); + + // Replacement should not happen until the next time the "update animations + // and send events" procedure runs. + + assert_equals(animA.replaceState, 'active'); + assert_equals(animB.replaceState, 'active'); + + await waitForNextFrame(); + + assert_equals(animA.replaceState, 'removed'); + assert_equals(animB.replaceState, 'active'); +}, 'Removes an animation after seeking another animation'); + +promise_test(async t => { + const div = createDiv(t); + + const animA = div.animate( + { opacity: 1 }, + { duration: 100 * MS_PER_SEC, fill: 'forwards' } + ); + const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + await animB.finished; + + assert_equals(animA.replaceState, 'active'); + assert_equals(animB.replaceState, 'active'); + + animA.finish(); + + // Replacement should not happen until the next time the "update animations + // and send events" procedure runs. + + assert_equals(animA.replaceState, 'active'); + assert_equals(animB.replaceState, 'active'); + + await waitForNextFrame(); + + assert_equals(animA.replaceState, 'removed'); + assert_equals(animB.replaceState, 'active'); +}, 'Removes an animation after seeking it'); + +promise_test(async t => { + const div = createDiv(t); + + const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + const animB = div.animate({ opacity: 1 }, 1); + await animA.finished; + + assert_equals(animA.replaceState, 'active'); + assert_equals(animB.replaceState, 'active'); + + animB.effect.updateTiming({ fill: 'forwards' }); + + // Replacement should not happen until the next time the "update animations + // and send events" procedure runs. + + assert_equals(animA.replaceState, 'active'); + assert_equals(animB.replaceState, 'active'); + + await waitForNextFrame(); + + assert_equals(animA.replaceState, 'removed'); + assert_equals(animB.replaceState, 'active'); +}, 'Removes an animation after updating the fill mode of another animation'); + +promise_test(async t => { + const div = createDiv(t); + + const animA = div.animate({ opacity: 1 }, 1); + const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + await animA.finished; + + assert_equals(animA.replaceState, 'active'); + assert_equals(animB.replaceState, 'active'); + + animA.effect.updateTiming({ fill: 'forwards' }); + + // Replacement should not happen until the next time the "update animations + // and send events" procedure runs. + + assert_equals(animA.replaceState, 'active'); + assert_equals(animB.replaceState, 'active'); + + await waitForNextFrame(); + + assert_equals(animA.replaceState, 'removed'); + assert_equals(animB.replaceState, 'active'); +}, 'Removes an animation after updating its fill mode'); + +promise_test(async t => { + const div = createDiv(t); + + const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + const animB = div.animate({ opacity: 1 }, 1); + await animA.finished; + + assert_equals(animA.replaceState, 'active'); + assert_equals(animB.replaceState, 'active'); + + animB.effect = new KeyframeEffect( + div, + { opacity: 1 }, + { + duration: 1, + fill: 'forwards', + } + ); + + assert_equals(animA.replaceState, 'active'); + assert_equals(animB.replaceState, 'active'); + + await waitForNextFrame(); + + assert_equals(animA.replaceState, 'removed'); + assert_equals(animB.replaceState, 'active'); +}, "Removes an animation after updating another animation's effect to one with different timing"); + +promise_test(async t => { + const div = createDiv(t); + + const animA = div.animate({ opacity: 1 }, 1); + const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + await animB.finished; + + assert_equals(animA.replaceState, 'active'); + assert_equals(animB.replaceState, 'active'); + + animA.effect = new KeyframeEffect( + div, + { opacity: 1 }, + { + duration: 1, + fill: 'forwards', + } + ); + + assert_equals(animA.replaceState, 'active'); + assert_equals(animB.replaceState, 'active'); + + await waitForNextFrame(); + + assert_equals(animA.replaceState, 'removed'); + assert_equals(animB.replaceState, 'active'); +}, 'Removes an animation after updating its effect to one with different timing'); + +promise_test(async t => { + const div = createDiv(t); + + const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + const animB = div.animate( + { opacity: 1 }, + { duration: 100 * MS_PER_SEC, fill: 'forwards' } + ); + + await animA.finished; + + // Set up a timeline that makes animB finished + animB.timeline = new DocumentTimeline({ + originTime: + document.timeline.currentTime - 100 * MS_PER_SEC - animB.startTime, + }); + + assert_equals(animA.replaceState, 'active'); + assert_equals(animB.replaceState, 'active'); + + await waitForNextFrame(); + + assert_equals(animA.replaceState, 'removed'); + assert_equals(animB.replaceState, 'active'); +}, "Removes an animation after updating another animation's timeline"); + +promise_test(async t => { + const div = createDiv(t); + + const animA = div.animate( + { opacity: 1 }, + { duration: 100 * MS_PER_SEC, fill: 'forwards' } + ); + const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + + await animB.finished; + + // Set up a timeline that makes animA finished + animA.timeline = new DocumentTimeline({ + originTime: + document.timeline.currentTime - 100 * MS_PER_SEC - animA.startTime, + }); + + assert_equals(animA.replaceState, 'active'); + assert_equals(animB.replaceState, 'active'); + + await waitForNextFrame(); + + assert_equals(animA.replaceState, 'removed'); + assert_equals(animB.replaceState, 'active'); +}, 'Removes an animation after updating its timeline'); + +promise_test(async t => { + const div = createDiv(t); + + const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + const animB = div.animate( + { width: '100px' }, + { duration: 1, fill: 'forwards' } + ); + await animA.finished; + + assert_equals(animA.replaceState, 'active'); + assert_equals(animB.replaceState, 'active'); + + animB.effect.setKeyframes({ width: '100px', opacity: 1 }); + + assert_equals(animA.replaceState, 'active'); + assert_equals(animB.replaceState, 'active'); + + await waitForNextFrame(); + + assert_equals(animA.replaceState, 'removed'); + assert_equals(animB.replaceState, 'active'); +}, "Removes an animation after updating another animation's effect's properties"); + +promise_test(async t => { + const div = createDiv(t); + + const animA = div.animate( + { opacity: 1, width: '100px' }, + { duration: 1, fill: 'forwards' } + ); + const animB = div.animate( + { width: '200px' }, + { duration: 1, fill: 'forwards' } + ); + await animA.finished; + + assert_equals(animA.replaceState, 'active'); + assert_equals(animB.replaceState, 'active'); + + animA.effect.setKeyframes({ width: '100px' }); + + assert_equals(animA.replaceState, 'active'); + assert_equals(animB.replaceState, 'active'); + + await waitForNextFrame(); + + assert_equals(animA.replaceState, 'removed'); + assert_equals(animB.replaceState, 'active'); +}, "Removes an animation after updating its effect's properties"); + +promise_test(async t => { + const div = createDiv(t); + + const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + const animB = div.animate( + { width: '100px' }, + { duration: 1, fill: 'forwards' } + ); + await animA.finished; + + assert_equals(animA.replaceState, 'active'); + assert_equals(animB.replaceState, 'active'); + + animB.effect = new KeyframeEffect( + div, + { width: '100px', opacity: 1 }, + { duration: 1, fill: 'forwards' } + ); + + assert_equals(animA.replaceState, 'active'); + assert_equals(animB.replaceState, 'active'); + + await waitForNextFrame(); + + assert_equals(animA.replaceState, 'removed'); + assert_equals(animB.replaceState, 'active'); +}, "Removes an animation after updating another animation's effect to one with different properties"); + +promise_test(async t => { + const div = createDiv(t); + + const animA = div.animate( + { opacity: 1, width: '100px' }, + { duration: 1, fill: 'forwards' } + ); + const animB = div.animate( + { width: '200px' }, + { duration: 1, fill: 'forwards' } + ); + await animA.finished; + + assert_equals(animA.replaceState, 'active'); + assert_equals(animB.replaceState, 'active'); + + animA.effect = new KeyframeEffect( + div, + { width: '100px' }, + { + duration: 1, + fill: 'forwards', + } + ); + + assert_equals(animA.replaceState, 'active'); + assert_equals(animB.replaceState, 'active'); + + await waitForNextFrame(); + + assert_equals(animA.replaceState, 'removed'); + assert_equals(animB.replaceState, 'active'); +}, 'Removes an animation after updating its effect to one with different properties'); + +promise_test(async t => { + const div = createDiv(t); + + const animA = div.animate( + { marginLeft: '10px' }, + { duration: 1, fill: 'forwards' } + ); + const animB = div.animate( + { margin: '20px' }, + { duration: 1, fill: 'forwards' } + ); + await animA.finished; + + assert_equals(animA.replaceState, 'removed'); + assert_equals(animB.replaceState, 'active'); +}, 'Removes an animation when another animation uses a shorthand'); + +promise_test(async t => { + const div = createDiv(t); + + const animA = div.animate( + { margin: '10px' }, + { duration: 1, fill: 'forwards' } + ); + const animB = div.animate( + { + marginLeft: '10px', + marginTop: '20px', + marginRight: '30px', + marginBottom: '40px', + }, + { duration: 1, fill: 'forwards' } + ); + await animA.finished; + + assert_equals(animA.replaceState, 'removed'); + assert_equals(animB.replaceState, 'active'); +}, 'Removes an animation that uses a shorthand'); + +promise_test(async t => { + const div = createDiv(t); + + const animA = div.animate( + { marginLeft: '10px' }, + { duration: 1, fill: 'forwards' } + ); + const animB = div.animate( + { marginInlineStart: '20px' }, + { duration: 1, fill: 'forwards' } + ); + await animA.finished; + + assert_equals(animA.replaceState, 'removed'); + assert_equals(animB.replaceState, 'active'); +}, 'Removes an animation by another animation using logical properties'); + +promise_test(async t => { + const div = createDiv(t); + + const animA = div.animate( + { marginInlineStart: '10px' }, + { duration: 1, fill: 'forwards' } + ); + const animB = div.animate( + { marginLeft: '20px' }, + { duration: 1, fill: 'forwards' } + ); + await animA.finished; + + assert_equals(animA.replaceState, 'removed'); + assert_equals(animB.replaceState, 'active'); +}, 'Removes an animation using logical properties'); + +promise_test(async t => { + const div = createDiv(t); + + const animA = div.animate( + { marginTop: '10px' }, + { duration: 1, fill: 'forwards' } + ); + const animB = div.animate( + { marginInlineStart: '20px' }, + { duration: 1, fill: 'forwards' } + ); + await animA.finished; + + assert_equals(animA.replaceState, 'active'); + assert_equals(animB.replaceState, 'active'); + + div.style.writingMode = 'vertical-rl'; + + assert_equals(animA.replaceState, 'active'); + assert_equals(animB.replaceState, 'active'); + + await waitForNextFrame(); + + assert_equals(animA.replaceState, 'removed'); + assert_equals(animB.replaceState, 'active'); +}, 'Removes an animation by another animation using logical properties after updating the context'); + +promise_test(async t => { + const divA = createDiv(t); + const divB = createDiv(t); + + const animA = divA.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + const animB = divB.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + await animA.finished; + + assert_equals(animA.replaceState, 'active'); + assert_equals(animB.replaceState, 'active'); + + animB.effect.target = divA; + + assert_equals(animA.replaceState, 'active'); + assert_equals(animB.replaceState, 'active'); + + await waitForNextFrame(); + + assert_equals(animA.replaceState, 'removed'); + assert_equals(animB.replaceState, 'active'); +}, "Removes an animation after updating another animation's effect's target"); + +promise_test(async t => { + const divA = createDiv(t); + const divB = createDiv(t); + + const animA = divA.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + const animB = divB.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + await animA.finished; + + assert_equals(animA.replaceState, 'active'); + assert_equals(animB.replaceState, 'active'); + + animA.effect.target = divB; + + assert_equals(animA.replaceState, 'active'); + assert_equals(animB.replaceState, 'active'); + + await waitForNextFrame(); + + assert_equals(animA.replaceState, 'removed'); + assert_equals(animB.replaceState, 'active'); +}, "Removes an animation after updating its effect's target"); + +promise_test(async t => { + const divA = createDiv(t); + const divB = createDiv(t); + + const animA = divA.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + const animB = divB.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + await animA.finished; + + assert_equals(animA.replaceState, 'active'); + assert_equals(animB.replaceState, 'active'); + + animB.effect = new KeyframeEffect( + divA, + { opacity: 1 }, + { + duration: 1, + fill: 'forwards', + } + ); + + assert_equals(animA.replaceState, 'active'); + assert_equals(animB.replaceState, 'active'); + + await waitForNextFrame(); + + assert_equals(animA.replaceState, 'removed'); + assert_equals(animB.replaceState, 'active'); +}, "Removes an animation after updating another animation's effect to one with a different target"); + +promise_test(async t => { + const divA = createDiv(t); + const divB = createDiv(t); + + const animA = divA.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + const animB = divB.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + await animA.finished; + + assert_equals(animA.replaceState, 'active'); + assert_equals(animB.replaceState, 'active'); + + animA.effect = new KeyframeEffect( + divB, + { opacity: 1 }, + { + duration: 1, + fill: 'forwards', + } + ); + + assert_equals(animA.replaceState, 'active'); + assert_equals(animB.replaceState, 'active'); + + await waitForNextFrame(); + + assert_equals(animA.replaceState, 'removed'); + assert_equals(animB.replaceState, 'active'); +}, 'Removes an animation after updating its effect to one with a different target'); + +promise_test(async t => { + const div = createDiv(t); + div.style.animation = 'opacity-animation 1ms forwards'; + const cssAnimation = div.getAnimations()[0]; + + const scriptAnimation = div.animate( + { opacity: 1 }, + { + duration: 1, + fill: 'forwards', + } + ); + await scriptAnimation.finished; + + assert_equals(cssAnimation.replaceState, 'active'); + assert_equals(scriptAnimation.replaceState, 'active'); +}, 'Does NOT remove a CSS animation tied to markup'); + +promise_test(async t => { + const div = createDiv(t); + div.style.animation = 'opacity-animation 1ms forwards'; + const cssAnimation = div.getAnimations()[0]; + + // Break tie to markup + div.style.animationName = 'none'; + assert_equals(cssAnimation.playState, 'idle'); + + // Restart animation + cssAnimation.play(); + + const scriptAnimation = div.animate( + { opacity: 1 }, + { + duration: 1, + fill: 'forwards', + } + ); + await scriptAnimation.finished; + + assert_equals(cssAnimation.replaceState, 'removed'); + assert_equals(scriptAnimation.replaceState, 'active'); +}, 'Removes a CSS animation no longer tied to markup'); + +promise_test(async t => { + // Setup transition + const div = createDiv(t); + div.style.opacity = '0'; + div.style.transition = 'opacity 1ms'; + getComputedStyle(div).opacity; + div.style.opacity = '1'; + const cssTransition = div.getAnimations()[0]; + cssTransition.effect.updateTiming({ fill: 'forwards' }); + + const scriptAnimation = div.animate( + { opacity: 1 }, + { + duration: 1, + fill: 'forwards', + } + ); + await scriptAnimation.finished; + + assert_equals(cssTransition.replaceState, 'active'); + assert_equals(scriptAnimation.replaceState, 'active'); +}, 'Does NOT remove a CSS transition tied to markup'); + +promise_test(async t => { + // Setup transition + const div = createDiv(t); + div.style.opacity = '0'; + div.style.transition = 'opacity 1ms'; + getComputedStyle(div).opacity; + div.style.opacity = '1'; + const cssTransition = div.getAnimations()[0]; + cssTransition.effect.updateTiming({ fill: 'forwards' }); + + // Break tie to markup + div.style.transitionProperty = 'none'; + assert_equals(cssTransition.playState, 'idle'); + + // Restart transition + cssTransition.play(); + + const scriptAnimation = div.animate( + { opacity: 1 }, + { + duration: 1, + fill: 'forwards', + } + ); + await scriptAnimation.finished; + + assert_equals(cssTransition.replaceState, 'removed'); + assert_equals(scriptAnimation.replaceState, 'active'); +}, 'Removes a CSS transition no longer tied to markup'); + +promise_test(async t => { + const div = createDiv(t); + + const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + const eventWatcher = new EventWatcher(t, animA, 'remove'); + + const event = await eventWatcher.wait_for('remove'); + + assert_times_equal(event.timelineTime, document.timeline.currentTime); + assert_times_equal(event.currentTime, 1); +}, 'Dispatches an event when removing'); + +promise_test(async t => { + const div = createDiv(t); + + const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + const eventWatcher = new EventWatcher(t, animA, 'remove'); + + await eventWatcher.wait_for('remove'); + + // Check we don't get another event + animA.addEventListener( + 'remove', + t.step_func(() => { + assert_unreached('remove event should not be fired a second time'); + }) + ); + + // Restart animation + animA.play(); + + await waitForNextFrame(); + + // Finish animation + animA.finish(); + + await waitForNextFrame(); +}, 'Does NOT dispatch a remove event twice'); + +promise_test(async t => { + const div = createDiv(t); + + const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + const animB = div.animate( + { opacity: 1 }, + { duration: 100 * MS_PER_SEC, fill: 'forwards' } + ); + await animA.finished; + + assert_equals(animA.replaceState, 'active'); + + animB.finish(); + animB.currentTime = 0; + + await waitForNextFrame(); + + assert_equals(animA.replaceState, 'active'); +}, "Does NOT remove an animation after making a redundant change to another animation's current time"); + +promise_test(async t => { + const div = createDiv(t); + + const animA = div.animate( + { opacity: 1 }, + { duration: 100 * MS_PER_SEC, fill: 'forwards' } + ); + const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + await animB.finished; + + assert_equals(animA.replaceState, 'active'); + + animA.finish(); + animA.currentTime = 0; + + await waitForNextFrame(); + + assert_equals(animA.replaceState, 'active'); +}, 'Does NOT remove an animation after making a redundant change to its current time'); + +promise_test(async t => { + const div = createDiv(t); + + const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + const animB = div.animate( + { opacity: 1 }, + { duration: 100 * MS_PER_SEC, fill: 'forwards' } + ); + await animA.finished; + + assert_equals(animA.replaceState, 'active'); + + // Set up a timeline that makes animB finished but then restore it + animB.timeline = new DocumentTimeline({ + originTime: + document.timeline.currentTime - 100 * MS_PER_SEC - animB.startTime, + }); + animB.timeline = document.timeline; + + await waitForNextFrame(); + + assert_equals(animA.replaceState, 'active'); +}, "Does NOT remove an animation after making a redundant change to another animation's timeline"); + +promise_test(async t => { + const div = createDiv(t); + + const animA = div.animate( + { opacity: 1 }, + { duration: 100 * MS_PER_SEC, fill: 'forwards' } + ); + const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + await animB.finished; + + assert_equals(animA.replaceState, 'active'); + + // Set up a timeline that makes animA finished but then restore it + animA.timeline = new DocumentTimeline({ + originTime: + document.timeline.currentTime - 100 * MS_PER_SEC - animA.startTime, + }); + animA.timeline = document.timeline; + + await waitForNextFrame(); + + assert_equals(animA.replaceState, 'active'); +}, 'Does NOT remove an animation after making a redundant change to its timeline'); + +promise_test(async t => { + const div = createDiv(t); + const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + const animB = div.animate( + { marginLeft: '100px' }, + { + duration: 1, + fill: 'forwards', + } + ); + await animA.finished; + + assert_equals(animA.replaceState, 'active'); + + // Redundant change + animB.effect.setKeyframes({ marginLeft: '100px', opacity: 1 }); + animB.effect.setKeyframes({ marginLeft: '100px' }); + + await waitForNextFrame(); + + assert_equals(animA.replaceState, 'active'); +}, "Does NOT remove an animation after making a redundant change to another animation's effect's properties"); + +promise_test(async t => { + const div = createDiv(t); + const animA = div.animate( + { marginLeft: '100px' }, + { + duration: 1, + fill: 'forwards', + } + ); + const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + await animA.finished; + + assert_equals(animA.replaceState, 'active'); + + // Redundant change + animA.effect.setKeyframes({ opacity: 1 }); + animA.effect.setKeyframes({ marginLeft: '100px' }); + + await waitForNextFrame(); + + assert_equals(animA.replaceState, 'active'); +}, "Does NOT remove an animation after making a redundant change to its effect's properties"); + +promise_test(async t => { + const div = createDiv(t); + + const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + animB.timeline = new DocumentTimeline(); + + await animA.finished; + + // If, for example, we only update the timeline for animA before checking + // replacement state, then animB will not be finished and animA will not be + // replaced. + + assert_equals(animA.replaceState, 'removed'); + assert_equals(animB.replaceState, 'active'); +}, 'Updates ALL timelines before checking for replacement'); + +promise_test(async t => { + const div = createDiv(t); + const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + + const events = []; + const logEvent = (targetName, eventType) => { + events.push(`${targetName}:${eventType}`); + }; + + animA.addEventListener('finish', () => logEvent('animA', 'finish')); + animA.addEventListener('remove', () => logEvent('animA', 'remove')); + animB.addEventListener('finish', () => logEvent('animB', 'finish')); + animB.addEventListener('remove', () => logEvent('animB', 'remove')); + + await animA.finished; + + // Allow all events to be dispatched + + await waitForNextFrame(); + + assert_array_equals(events, [ + 'animA:finish', + 'animB:finish', + 'animA:remove', + ]); +}, 'Dispatches remove events after finish events'); + +promise_test(async t => { + const div = createDiv(t); + const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + + const eventWatcher = new EventWatcher(t, animA, 'remove'); + + await animA.finished; + + let rAFReceived = false; + requestAnimationFrame(() => (rAFReceived = true)); + + await eventWatcher.wait_for('remove'); + + assert_false( + rAFReceived, + 'remove event should be fired before requestAnimationFrame' + ); +}, 'Fires remove event before requestAnimationFrame'); + +promise_test(async t => { + const div = createDiv(t); + const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + const animB = div.animate( + { width: '100px' }, + { duration: 1, fill: 'forwards' } + ); + const animC = div.animate( + { opacity: 0.5, width: '200px' }, + { duration: 1, fill: 'forwards' } + ); + + // In the event handler for animA (which should be fired before that of animB) + // we make a change to animC so that it no longer covers animB. + // + // If the remove event for animB is not already queued by this point, it will + // fail to fire. + animA.addEventListener('remove', () => { + animC.effect.setKeyframes({ + opacity: 0.5, + }); + }); + + const eventWatcher = new EventWatcher(t, animB, 'remove'); + await eventWatcher.wait_for('remove'); + + assert_equals(animA.replaceState, 'removed'); + assert_equals(animB.replaceState, 'removed'); + assert_equals(animC.replaceState, 'active'); +}, 'Queues all remove events before running them'); + +promise_test(async t => { + const outerIframe = document.createElement('iframe'); + outerIframe.width = 10; + outerIframe.height = 10; + await insertFrameAndAwaitLoad(t, outerIframe, document); + + const innerIframe = document.createElement('iframe'); + innerIframe.width = 10; + innerIframe.height = 10; + await insertFrameAndAwaitLoad(t, innerIframe, outerIframe.contentDocument); + + const div = createDiv(t, innerIframe.contentDocument); + + const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + + // Sanity check: The timeline for these animations should be the default + // document timeline for div. + assert_equals(animA.timeline, innerIframe.contentDocument.timeline); + assert_equals(animB.timeline, innerIframe.contentDocument.timeline); + + await animA.finished; + + assert_equals(animA.replaceState, 'removed'); + assert_equals(animB.replaceState, 'active'); +}, 'Performs removal in deeply nested iframes'); + +</script> diff --git a/testing/web-platform/tests/web-animations/timing-model/timelines/update-and-send-events.html b/testing/web-platform/tests/web-animations/timing-model/timelines/update-and-send-events.html new file mode 100644 index 0000000000..255e013f27 --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/timelines/update-and-send-events.html @@ -0,0 +1,257 @@ +<!doctype html> +<meta charset=utf-8> +<title>Update animations and send events</title> +<meta name="timeout" content="long"> +<link rel="help" href="https://drafts.csswg.org/web-animations/#update-animations-and-send-events"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<div id="log"></div> +<script> +'use strict'; + +promise_test(async t => { + const div = createDiv(t); + const animation = div.animate(null, 100 * MS_PER_SEC); + + // The ready promise should be resolved as part of micro-task checkpoint + // after updating the current time of all timeslines in the procedure to + // "update animations and send events". + await animation.ready; + + let rAFReceived = false; + requestAnimationFrame(() => rAFReceived = true); + + const eventWatcher = new EventWatcher(t, animation, 'cancel'); + animation.cancel(); + + await eventWatcher.wait_for('cancel'); + + assert_false(rAFReceived, + 'cancel event should be fired before requestAnimationFrame'); +}, 'Fires cancel event before requestAnimationFrame'); + +promise_test(async t => { + const div = createDiv(t); + const animation = div.animate(null, 100 * MS_PER_SEC); + + // Like the above test, the ready promise should be resolved micro-task + // checkpoint after updating the current time of all timeslines in the + // procedure to "update animations and send events". + await animation.ready; + + let rAFReceived = false; + requestAnimationFrame(() => rAFReceived = true); + + const eventWatcher = new EventWatcher(t, animation, 'finish'); + animation.finish(); + + await eventWatcher.wait_for('finish'); + + assert_false(rAFReceived, + 'finish event should be fired before requestAnimationFrame'); +}, 'Fires finish event before requestAnimationFrame'); + +function animationType(anim) { + if (anim instanceof CSSAnimation) { + return 'CSSAnimation'; + } else if (anim instanceof CSSTransition) { + return 'CSSTransition'; + } else { + return 'ScriptAnimation'; + } +} + +promise_test(async t => { + createStyle(t, { '@keyframes anim': '' }); + const div = createDiv(t); + + getComputedStyle(div).marginLeft; + div.style = 'animation: anim 100s; ' + + 'transition: margin-left 100s; ' + + 'margin-left: 100px;'; + div.animate(null, 100 * MS_PER_SEC); + const animations = div.getAnimations(); + + let receivedEvents = []; + animations.forEach(anim => { + anim.onfinish = event => { + receivedEvents.push({ + type: animationType(anim) + ':' + event.type, + timeStamp: event.timeStamp + }); + }; + }); + + await Promise.all(animations.map(anim => anim.ready)); + + // Setting current time to the time just before the effect end. + animations.forEach(anim => anim.currentTime = 100 * MS_PER_SEC - 1); + + await waitForNextFrame(); + + assert_array_equals(receivedEvents.map(event => event.type), + [ 'CSSTransition:finish', 'CSSAnimation:finish', + 'ScriptAnimation:finish' ], + 'finish events for various animation type should be sorted by composite ' + + 'order'); +}, 'Sorts finish events by composite order'); + +promise_test(async t => { + createStyle(t, { '@keyframes anim': '' }); + const div = createDiv(t); + + let receivedEvents = []; + function receiveEvent(type, timeStamp) { + receivedEvents.push({ type, timeStamp }); + } + + div.onanimationcancel = event => receiveEvent(event.type, event.timeStamp); + div.ontransitioncancel = event => receiveEvent(event.type, event.timeStamp); + + getComputedStyle(div).marginLeft; + div.style = 'animation: anim 100s; ' + + 'transition: margin-left 100s; ' + + 'margin-left: 100px;'; + div.animate(null, 100 * MS_PER_SEC); + const animations = div.getAnimations(); + + animations.forEach(anim => { + anim.oncancel = event => { + receiveEvent(animationType(anim) + ':' + event.type, event.timeStamp); + }; + }); + + await Promise.all(animations.map(anim => anim.ready)); + + const timeInAnimationReady = document.timeline.currentTime; + + // Call cancel() in reverse composite order. I.e. canceling for script + // animation happen first, then for CSS animation and CSS transition. + // 'cancel' events for these animations should be sorted by composite + // order. + animations.reverse().forEach(anim => anim.cancel()); + + // requestAnimationFrame callback which is actually the _same_ frame since we + // are currently operating in the `ready` callbac of the animations which + // happens as part of the "Update animations and send events" procedure + // _before_ we run animation frame callbacks. + await waitForAnimationFrames(1); + + assert_times_equal(timeInAnimationReady, document.timeline.currentTime, + 'A rAF callback should happen in the same frame'); + + assert_array_equals(receivedEvents.map(event => event.type), + // This ordering needs more clarification in the spec, but the intention is + // that the cancel playback event fires before the equivalent CSS cancel + // event in each case. + [ 'CSSTransition:cancel', 'CSSAnimation:cancel', 'ScriptAnimation:cancel', + 'transitioncancel', 'animationcancel' ], + 'cancel events should be sorted by composite order'); +}, 'Sorts cancel events by composite order'); + +promise_test(async t => { + const div = createDiv(t); + getComputedStyle(div).marginLeft; + div.style = 'transition: margin-left 100s; margin-left: 100px;'; + const anim = div.getAnimations()[0]; + + let receivedEvents = []; + anim.oncancel = event => receivedEvents.push(event); + + const eventWatcher = new EventWatcher(t, div, 'transitionstart'); + await eventWatcher.wait_for('transitionstart'); + + const timeInEventCallback = document.timeline.currentTime; + + // Calling cancel() queues a cancel event + anim.cancel(); + + await waitForAnimationFrames(1); + assert_times_equal(timeInEventCallback, document.timeline.currentTime, + 'A rAF callback should happen in the same frame'); + + assert_array_equals(receivedEvents, [], + 'The queued cancel event shouldn\'t be dispatched in the same frame'); + + await waitForAnimationFrames(1); + assert_array_equals(receivedEvents.map(event => event.type), ['cancel'], + 'The cancel event should be dispatched in a later frame'); +}, 'Queues a cancel event in transitionstart event callback'); + +promise_test(async t => { + const div = createDiv(t); + getComputedStyle(div).marginLeft; + div.style = 'transition: margin-left 100s; margin-left: 100px;'; + const anim = div.getAnimations()[0]; + + let receivedEvents = []; + anim.oncancel = event => receivedEvents.push(event); + div.ontransitioncancel = event => receivedEvents.push(event); + + await anim.ready; + + anim.cancel(); + + await waitForAnimationFrames(1); + + assert_array_equals(receivedEvents.map(event => event.type), + [ 'cancel', 'transitioncancel' ], + 'Playback and CSS events for the same transition should be sorted by ' + + 'schedule event time and composite order'); +}, 'Sorts events for the same transition'); + +promise_test(async t => { + const div = createDiv(t); + const anim = div.animate(null, 100 * MS_PER_SEC); + + let receivedEvents = []; + anim.oncancel = event => receivedEvents.push(event); + anim.onfinish = event => receivedEvents.push(event); + + await anim.ready; + + anim.finish(); + anim.cancel(); + + await waitForAnimationFrames(1); + + assert_array_equals(receivedEvents.map(event => event.type), + [ 'finish', 'cancel' ], + 'Calling finish() synchronously queues a finish event when updating the ' + + 'finish state so it should appear before the cancel event'); +}, 'Playback events with the same timeline retain the order in which they are' + + 'queued'); + +promise_test(async t => { + const div = createDiv(t); + + // Create two animations with separate timelines + + const timelineA = document.timeline; + const animA = div.animate(null, 100 * MS_PER_SEC); + + const timelineB = new DocumentTimeline(); + const animB = new Animation( + new KeyframeEffect(div, null, 100 * MS_PER_SEC), + timelineB + ); + animB.play(); + + animA.currentTime = 99.9 * MS_PER_SEC; + animB.currentTime = 99.9 * MS_PER_SEC; + + // When the next tick happens both animations should be updated, and we will + // notice that they are now finished. As a result their finished promise + // callbacks should be queued. All of that should happen before we run the + // next microtask checkpoint and actually run the promise callbacks and + // hence the calls to cancel should not stop the existing callbacks from + // being run. + + animA.finished.then(() => { animB.cancel() }); + animB.finished.then(() => { animA.cancel() }); + + await Promise.all([animA.finished, animB.finished]); +}, 'All timelines are updated before running microtasks'); + +</script> |