diff options
Diffstat (limited to 'testing/web-platform/tests/web-animations')
159 files changed, 23109 insertions, 0 deletions
diff --git a/testing/web-platform/tests/web-animations/META.yml b/testing/web-platform/tests/web-animations/META.yml new file mode 100644 index 0000000000..9ba1245bde --- /dev/null +++ b/testing/web-platform/tests/web-animations/META.yml @@ -0,0 +1,5 @@ +spec: https://drafts.csswg.org/web-animations/ +suggested_reviewers: + - birtles + - flackr + - graouts diff --git a/testing/web-platform/tests/web-animations/README.md b/testing/web-platform/tests/web-animations/README.md new file mode 100644 index 0000000000..c41e0e048e --- /dev/null +++ b/testing/web-platform/tests/web-animations/README.md @@ -0,0 +1,116 @@ +Web Animations Test Suite +========================= + +Specification: https://drafts.csswg.org/web-animations/ + + +Guidelines for writing tests +---------------------------- + +* Try to follow the spec outline where possible. + + For example, if you want to test setting the start time, you might be + tempted to put all the tests in: + + > `/web-animations/interfaces/Animation/startTime.html` + + However, in the spec most of the logic is in the “Set the animation + start time“ procedure in the “Timing model” section. + + Instead, try something like: + + > * `/web-animations/timing-model/animations/setting-the-start-time-of-an-animation.html`<br> + > Tests all the branches and inputs to the procedure as defined in the + > spec (using the `Animation.startTime` API). + > * `/web-animations/interfaces/Animation/startTime.html`<br> + > Tests API-layer specific issues like mapping unresolved values to + > null, etc. + + On that note, two levels of subdirectories is enough even if the spec has + deeper nesting. + + Note that most of the existing tests in the suite _don't_ do this well yet. + That's the direction we're heading, however. + +* Test the spec. + + * If the spec defines a timing calculation that is directly + reflected in the iteration progress + (i.e. `anim.effect.getComputedTiming().progress`), test that instead + of calling `getComputedStyle(elem).marginLeft`. + + * Likewise, don't add needless tests for `anim.playbackState`. + The playback state is a calculated value based on other values. + It's rarely necessary to test directly unless you need, for example, + to check that a pending task is scheduled (which isn't observable + elsewhere other than waiting for the corresponding promise to + complete). + +* Try to keep tests as simple and focused as possible. + + e.g. + + ```javascript + test(t => { + const animation = createDiv(t).animate(null); + assert_class_string(animation, 'Animation', 'Returned object is an Animation'); + }, 'Element.animate() creates an Animation object'); + ``` + + ```javascript + test(t => { + assert_throws_js(TypeError, () => { + createDiv(t).animate(null, -1); + }); + }, 'Setting a negative duration throws a TypeError'); + ``` + + ```javascript + promise_test(t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + return animation.ready.then(() => { + assert_greater_than(animation.startTime, 0, 'startTime when running'); + }); + }, 'startTime is resolved when running'); + ``` + + If you're generating complex test loops and factoring out utility functions + that affect the logic of the test (other than, say, simple assertion utility + functions), you're probably doing it wrong. + + It should be possible to understand exactly what the test is doing at a + glance without having to scroll up and down the test file and refer to + other files. + + See Justin Searls' presentation, [“How to stop hating your + tests”](http://blog.testdouble.com/posts/2015-11-16-how-to-stop-hating-your-tests.html) + for some tips on making your tests simpler. + +* Assume tests will run on under-performing hardware where the time between + animation frames might run into 10s of seconds. + As a result, animations that are expected to still be running during + the test should be at least 100s in length. + +* Avoid using `GLOBAL_CONSTS` that make the test harder to read. + It's fine to repeat the the same parameter values like `100 * MS_PER_SEC` + over and over again since it makes it easy to read and debug a test in + isolation. + Remember, even if we do need to make all tests take, say 200s each, text + editors are very good at search and replace. + +* Use the `assert_times_equal` assertion for comparing times returned from + the API. This asserts that the time values are equal using a tolerance + based on the precision recommended in the spec. This tolerance is applied + to *both* of the values being compared. That is, it effectively allows + double the epsilon that is used when comparing with an absolute value. + + For comparing a time value returned from the API to an absolute value, use + `assert_time_equals_literal`. This tests that the actual value is equal to + the expected value within the precision recommended in the spec. + + Both `assert_times_equal` and `assert_time_equals_literal` are defined in a + way that implementations can override them to meet their own precision + requirements. + +* There are quite a few bad tests in the repository. We're learning as + we go. Don't just copy them blindly—please fix them! diff --git a/testing/web-platform/tests/web-animations/animation-model/animation-types/accumulation-per-property-001.html b/testing/web-platform/tests/web-animations/animation-model/animation-types/accumulation-per-property-001.html new file mode 100644 index 0000000000..a3fd115563 --- /dev/null +++ b/testing/web-platform/tests/web-animations/animation-model/animation-types/accumulation-per-property-001.html @@ -0,0 +1,24 @@ +<!doctype html> +<meta charset=utf-8> +<title>Accumulation for each property</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#animation-types"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<script src="property-list.js"></script> +<script src="property-types.js"></script> +<script src="property-utils.js"></script> +<style> +html { + font-size: 10px; +} +</style> +<body> +<div id="log"></div> +<script> +'use strict'; + +test(function() { + runAnimationTypeTest(gCSSProperties1, 'testAccumulation'); +}, 'Setup'); +</script> diff --git a/testing/web-platform/tests/web-animations/animation-model/animation-types/accumulation-per-property-002.html b/testing/web-platform/tests/web-animations/animation-model/animation-types/accumulation-per-property-002.html new file mode 100644 index 0000000000..5cf411edf6 --- /dev/null +++ b/testing/web-platform/tests/web-animations/animation-model/animation-types/accumulation-per-property-002.html @@ -0,0 +1,24 @@ +<!doctype html> +<meta charset=utf-8> +<title>Accumulation for each property</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#animation-types"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<script src="property-list.js"></script> +<script src="property-types.js"></script> +<script src="property-utils.js"></script> +<style> +html { + font-size: 10px; +} +</style> +<body> +<div id="log"></div> +<script> +'use strict'; + +test(function() { + runAnimationTypeTest(gCSSProperties2, 'testAccumulation'); +}, 'Setup'); +</script> diff --git a/testing/web-platform/tests/web-animations/animation-model/animation-types/addition-per-property-001.html b/testing/web-platform/tests/web-animations/animation-model/animation-types/addition-per-property-001.html new file mode 100644 index 0000000000..2fbec2c4cd --- /dev/null +++ b/testing/web-platform/tests/web-animations/animation-model/animation-types/addition-per-property-001.html @@ -0,0 +1,24 @@ +<!doctype html> +<meta charset=utf-8> +<title>Addition for each property</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#animation-types"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<script src="property-list.js"></script> +<script src="property-types.js"></script> +<script src="property-utils.js"></script> +<style> +html { + font-size: 10px; +} +</style> +<body> +<div id="log"></div> +<script> +'use strict'; + +test(function() { + runAnimationTypeTest(gCSSProperties1, 'testAddition'); +}, "Setup"); +</script> diff --git a/testing/web-platform/tests/web-animations/animation-model/animation-types/addition-per-property-002.html b/testing/web-platform/tests/web-animations/animation-model/animation-types/addition-per-property-002.html new file mode 100644 index 0000000000..3b1c40e3c7 --- /dev/null +++ b/testing/web-platform/tests/web-animations/animation-model/animation-types/addition-per-property-002.html @@ -0,0 +1,24 @@ +<!doctype html> +<meta charset=utf-8> +<title>Addition for each property</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#animation-types"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<script src="property-list.js"></script> +<script src="property-types.js"></script> +<script src="property-utils.js"></script> +<style> +html { + font-size: 10px; +} +</style> +<body> +<div id="log"></div> +<script> +'use strict'; + +test(function() { + runAnimationTypeTest(gCSSProperties2, 'testAddition'); +}, "Setup"); +</script> diff --git a/testing/web-platform/tests/web-animations/animation-model/animation-types/discrete.html b/testing/web-platform/tests/web-animations/animation-model/animation-types/discrete.html new file mode 100644 index 0000000000..76f42bc7a4 --- /dev/null +++ b/testing/web-platform/tests/web-animations/animation-model/animation-types/discrete.html @@ -0,0 +1,135 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Discrete animation type</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#discrete-animation-type"> +<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 div = createDiv(t); + + const anim = div.animate({ fontStyle: [ 'normal', 'italic' ] }, + { duration: 1000, fill: 'forwards' }); + + assert_equals(getComputedStyle(div).fontStyle, 'normal', + 'Animation produces \'from\' value at start of interval'); + anim.currentTime = anim.effect.getComputedTiming().duration / 2 - 1; + assert_equals(getComputedStyle(div).fontStyle, 'normal', + 'Animation produces \'from\' value just before the middle of' + + ' the interval'); + anim.currentTime++; + assert_equals(getComputedStyle(div).fontStyle, 'italic', + 'Animation produces \'to\' value at exact middle of' + + ' the interval'); + anim.finish(); + assert_equals(getComputedStyle(div).fontStyle, 'italic', + 'Animation produces \'to\' value during forwards fill'); +}, 'Test animating discrete values'); + +test(t => { + const div = createDiv(t); + const originalHeight = getComputedStyle(div).height; + + const anim = div.animate({ height: [ 'auto', '200px' ] }, + { duration: 1000, fill: 'forwards' }); + + assert_equals(getComputedStyle(div).height, originalHeight, + 'Animation produces \'from\' value at start of interval'); + anim.currentTime = anim.effect.getComputedTiming().duration / 2 - 1; + assert_equals(getComputedStyle(div).height, originalHeight, + 'Animation produces \'from\' value just before the middle of' + + ' the interval'); + anim.currentTime++; + assert_equals(getComputedStyle(div).height, '200px', + 'Animation produces \'to\' value at exact middle of' + + ' the interval'); + anim.finish(); + assert_equals(getComputedStyle(div).height, '200px', + 'Animation produces \'to\' value during forwards fill'); +}, 'Test discrete animation is used when interpolation fails'); + +test(t => { + const div = createDiv(t); + const originalHeight = getComputedStyle(div).height; + + const anim = div.animate({ height: [ 'auto', + '200px', + '300px', + 'auto', + '400px' ] }, + { duration: 1000, fill: 'forwards' }); + + // There are five values, so there are four pairs to try to interpolate. + // We test at the middle of each pair. + assert_equals(getComputedStyle(div).height, originalHeight, + 'Animation produces \'from\' value at start of interval'); + anim.currentTime = 125; + assert_equals(getComputedStyle(div).height, '200px', + 'First non-interpolable pair uses discrete interpolation'); + anim.currentTime += 250; + assert_equals(getComputedStyle(div).height, '250px', + 'Second interpolable pair uses linear interpolation'); + anim.currentTime += 250; + assert_equals(getComputedStyle(div).height, originalHeight, + 'Third non-interpolable pair uses discrete interpolation'); + anim.currentTime += 250; + assert_equals(getComputedStyle(div).height, '400px', + 'Fourth non-interpolable pair uses discrete interpolation'); +}, 'Test discrete animation is used only for pairs of values that cannot' + + ' be interpolated'); + +test(t => { + const div = createDiv(t); + const originalHeight = getComputedStyle(div).height; + + // Easing: http://cubic-bezier.com/#.68,0,1,.01 + // With this curve, we don't reach the 50% point until about 95% of + // the time has expired. + const anim = div.animate({ fontStyle: [ 'normal', 'italic' ] }, + { duration: 1000, fill: 'forwards', + easing: 'cubic-bezier(0.68,0,1,0.01)' }); + + assert_equals(getComputedStyle(div).fontStyle, 'normal', + 'Animation produces \'from\' value at start of interval'); + anim.currentTime = 940; + assert_equals(getComputedStyle(div).fontStyle, 'normal', + 'Animation produces \'from\' value at 94% of the iteration' + + ' time'); + anim.currentTime = 960; + assert_equals(getComputedStyle(div).fontStyle, 'italic', + 'Animation produces \'to\' value at 96% of the iteration' + + ' time'); +}, 'Test the 50% switch point for discrete animation is based on the' + + ' effect easing'); + +test(t => { + const div = createDiv(t); + const originalHeight = getComputedStyle(div).height; + + // Easing: http://cubic-bezier.com/#.68,0,1,.01 + // With this curve, we don't reach the 50% point until about 95% of + // the time has expired. + const anim = div.animate([ { fontStyle: 'normal', + easing: 'cubic-bezier(0.68,0,1,0.01)' }, + { fontStyle: 'italic' } ], + { duration: 1000, fill: 'forwards' }); + + assert_equals(getComputedStyle(div).fontStyle, 'normal', + 'Animation produces \'from\' value at start of interval'); + anim.currentTime = 940; + assert_equals(getComputedStyle(div).fontStyle, 'normal', + 'Animation produces \'from\' value at 94% of the iteration' + + ' time'); + anim.currentTime = 960; + assert_equals(getComputedStyle(div).fontStyle, 'italic', + 'Animation produces \'to\' value at 96% of the iteration' + + ' time'); +}, 'Test the 50% switch point for discrete animation is based on the' + + ' keyframe easing'); + +</script> diff --git a/testing/web-platform/tests/web-animations/animation-model/animation-types/interpolation-per-property-001.html b/testing/web-platform/tests/web-animations/animation-model/animation-types/interpolation-per-property-001.html new file mode 100644 index 0000000000..97f2822473 --- /dev/null +++ b/testing/web-platform/tests/web-animations/animation-model/animation-types/interpolation-per-property-001.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Interpolation for each property</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#animation-types"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<script src="property-list.js"></script> +<script src="property-types.js"></script> +<script src="property-utils.js"></script> +<style> +html { + font-size: 10px; +} +</style> +<body> +<div id="log"></div> +<script> +'use strict'; + +test(function() { + runAnimationTypeTest(gCSSProperties1, 'testInterpolation'); +}, 'Setup'); +</script> diff --git a/testing/web-platform/tests/web-animations/animation-model/animation-types/interpolation-per-property-002.html b/testing/web-platform/tests/web-animations/animation-model/animation-types/interpolation-per-property-002.html new file mode 100644 index 0000000000..9ccc613cfc --- /dev/null +++ b/testing/web-platform/tests/web-animations/animation-model/animation-types/interpolation-per-property-002.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Interpolation for each property</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#animation-types"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<script src="property-list.js"></script> +<script src="property-types.js"></script> +<script src="property-utils.js"></script> +<style> +html { + font-size: 10px; +} +</style> +<body> +<div id="log"></div> +<script> +'use strict'; + +test(function() { + runAnimationTypeTest(gCSSProperties2, 'testInterpolation'); +}, 'Setup'); +</script> diff --git a/testing/web-platform/tests/web-animations/animation-model/animation-types/property-list.js b/testing/web-platform/tests/web-animations/animation-model/animation-types/property-list.js new file mode 100644 index 0000000000..59866a5897 --- /dev/null +++ b/testing/web-platform/tests/web-animations/animation-model/animation-types/property-list.js @@ -0,0 +1,1564 @@ +'use strict'; + +const gCSSProperties1 = { + 'align-content': { + // https://drafts.csswg.org/css-align/#propdef-align-content + types: [ + { type: 'discrete' , options: [ [ 'flex-start', 'flex-end' ] ] } + ] + }, + 'align-items': { + // https://drafts.csswg.org/css-align/#propdef-align-items + types: [ + { type: 'discrete', options: [ [ 'flex-start', 'flex-end' ] ] } + ] + }, + 'align-self': { + // https://drafts.csswg.org/css-align/#propdef-align-self + types: [ + { type: 'discrete', options: [ [ 'flex-start', 'flex-end' ] ] } + ] + }, + 'backface-visibility': { + // https://drafts.csswg.org/css-transforms/#propdef-backface-visibility + types: [ + { type: 'discrete', options: [ [ 'visible', 'hidden' ] ] } + ] + }, + 'background-attachment': { + // https://drafts.csswg.org/css-backgrounds-3/#background-attachment + types: [ + { type: 'discrete', options: [ [ 'fixed', 'local' ] ] } + ] + }, + 'background-color': { + // https://drafts.csswg.org/css-backgrounds-3/#background-color + types: [ 'color' ] + }, + 'background-blend-mode': { + // https://drafts.fxtf.org/compositing-1/#propdef-background-blend-mode + types: [ + { type: 'discrete', options: [ [ 'multiply', 'screen' ] ] } + ] + }, + 'background-clip': { + // https://drafts.csswg.org/css-backgrounds-3/#background-clip + types: [ + { type: 'discrete', options: [ [ 'padding-box', 'content-box' ] ] } + ] + }, + 'background-image': { + // https://drafts.csswg.org/css-backgrounds-3/#background-image + types: [ + { type: 'discrete', + options: [ [ 'url("http://localhost/test-1")', + 'url("http://localhost/test-2")' ] ] } + ] + }, + 'background-origin': { + // https://drafts.csswg.org/css-backgrounds-3/#background-origin + types: [ + { type: 'discrete', options: [ [ 'padding-box', 'content-box' ] ] } + ] + }, + 'background-position': { + // https://drafts.csswg.org/css-backgrounds-3/#background-position + types: [ + ] + }, + 'background-position-x': { + // https://drafts.csswg.org/css-backgrounds-4/#propdef-background-position-x + types: [ + ] + }, + 'background-position-y': { + // https://drafts.csswg.org/css-backgrounds-4/#propdef-background-position-y + types: [ + ] + }, + 'background-repeat': { + // https://drafts.csswg.org/css-backgrounds-3/#background-repeat + types: [ + { type: 'discrete', options: [ [ 'space', 'round' ] ] } + ] + }, + 'background-size': { + // https://drafts.csswg.org/css-backgrounds-3/#background-size + types: [ + ] + }, + 'block-size': { + // https://drafts.csswg.org/css-logical-props/#propdef-block-size + types: [ + ] + }, + 'border-block-end-color': { + // https://drafts.csswg.org/css-logical-props/#propdef-border-block-end-color + types: [ + ] + }, + 'border-block-end-style': { + // https://drafts.csswg.org/css-logical-props/#propdef-border-block-end-style + types: [ + ] + }, + 'border-block-end-width': { + // https://drafts.csswg.org/css-logical-props/#propdef-border-block-end-width + types: [ + ] + }, + 'border-block-start-color': { + // https://drafts.csswg.org/css-logical-props/#propdef-border-block-start-color + types: [ + ] + }, + 'border-block-start-style': { + // https://drafts.csswg.org/css-logical-props/#propdef-border-block-start-style + types: [ + ] + }, + 'border-block-start-width': { + // https://drafts.csswg.org/css-logical-props/#propdef-border-block-start-width + types: [ + ] + }, + 'border-bottom-color': { + // https://drafts.csswg.org/css-backgrounds-3/#border-bottom-color + types: [ 'color' ] + }, + 'border-bottom-left-radius': { + // https://drafts.csswg.org/css-backgrounds-3/#border-bottom-left-radius + types: [ + ] + }, + 'border-bottom-right-radius': { + // https://drafts.csswg.org/css-backgrounds-3/#border-bottom-right-radius + types: [ + ] + }, + 'border-bottom-style': { + // https://drafts.csswg.org/css-backgrounds-3/#border-bottom-style + types: [ + { type: 'discrete', options: [ [ 'dotted', 'solid' ] ] } + ] + }, + 'border-bottom-width': { + // https://drafts.csswg.org/css-backgrounds-3/#border-bottom-width + types: [ 'length' ], + setup: t => { + const element = createElement(t); + element.style.borderBottomStyle = 'solid'; + return element; + } + }, + 'border-collapse': { + // https://drafts.csswg.org/css-tables/#propdef-border-collapse + types: [ + { type: 'discrete', options: [ [ 'collapse', 'separate' ] ] } + ] + }, + 'border-inline-end-color': { + // https://drafts.csswg.org/css-logical-props/#propdef-border-inline-end-color + types: [ + ] + }, + 'border-inline-end-style': { + // https://drafts.csswg.org/css-logical-props/#propdef-border-inline-end-style + types: [ + ] + }, + 'border-inline-end-width': { + // https://drafts.csswg.org/css-logical-props/#propdef-border-inline-end-width + types: [ + ] + }, + 'border-inline-start-color': { + // https://drafts.csswg.org/css-logical-props/#propdef-border-inline-start-color + types: [ + ] + }, + 'border-inline-start-style': { + // https://drafts.csswg.org/css-logical-props/#propdef-border-block-start-style + types: [ + ] + }, + 'border-inline-start-width': { + // https://drafts.csswg.org/css-logical-props/#propdef-border-inline-start-width + types: [ + ] + }, + 'border-image-outset': { + // https://drafts.csswg.org/css-backgrounds-3/#border-image-outset + types: [ + ] + }, + 'border-image-repeat': { + // https://drafts.csswg.org/css-backgrounds-3/#border-image-repeat + types: [ + { type: 'discrete', options: [ [ 'stretch repeat', 'round space' ] ] } + ] + }, + 'border-image-slice': { + // https://drafts.csswg.org/css-backgrounds-3/#border-image-slice + types: [ + ] + }, + 'border-image-source': { + // https://drafts.csswg.org/css-backgrounds-3/#border-image-source + types: [ + { type: 'discrete', + options: [ [ 'url("http://localhost/test-1")', + 'url("http://localhost/test-2")' ] ] } + ] + }, + 'border-image-width': { + // https://drafts.csswg.org/css-backgrounds-3/#border-image-width + types: [ + ] + }, + 'border-left-color': { + // https://drafts.csswg.org/css-backgrounds-3/#border-left-color + types: [ 'color' ] + }, + 'border-left-style': { + // https://drafts.csswg.org/css-backgrounds-3/#border-left-style + types: [ + { type: 'discrete', options: [ [ 'dotted', 'solid' ] ] } + ] + }, + 'border-left-width': { + // https://drafts.csswg.org/css-backgrounds-3/#border-left-width + types: [ 'length' ], + setup: t => { + const element = createElement(t); + element.style.borderLeftStyle = 'solid'; + return element; + } + }, + 'border-right-color': { + // https://drafts.csswg.org/css-backgrounds-3/#border-right-color + types: [ 'color' ] + }, + 'border-right-style': { + // https://drafts.csswg.org/css-backgrounds-3/#border-right-style + types: [ + { type: 'discrete', options: [ [ 'dotted', 'solid' ] ] } + ] + }, + 'border-right-width': { + // https://drafts.csswg.org/css-backgrounds-3/#border-right-width + types: [ 'length' ], + setup: t => { + const element = createElement(t); + element.style.borderRightStyle = 'solid'; + return element; + } + }, + 'border-spacing': { + // https://drafts.csswg.org/css-tables/#propdef-border-spacing + types: [ 'lengthPair' ] + }, + 'border-top-color': { + // https://drafts.csswg.org/css-backgrounds-3/#border-top-color + types: [ 'color' ] + }, + 'border-top-left-radius': { + // https://drafts.csswg.org/css-backgrounds-3/#border-top-left-radius + types: [ + ] + }, + 'border-top-right-radius': { + // https://drafts.csswg.org/css-backgrounds-3/#border-top-right-radius + types: [ + ] + }, + 'border-top-style': { + // https://drafts.csswg.org/css-backgrounds-3/#border-top-style + types: [ + { type: 'discrete', options: [ [ 'dotted', 'solid' ] ] } + ] + }, + 'border-top-width': { + // https://drafts.csswg.org/css-backgrounds-3/#border-top-width + types: [ 'length' ], + setup: t => { + const element = createElement(t); + element.style.borderTopStyle = 'solid'; + return element; + } + }, + 'bottom': { + // https://drafts.csswg.org/css-position/#propdef-bottom + types: [ + ] + }, + 'box-decoration-break': { + // https://drafts.csswg.org/css-break/#propdef-box-decoration-break + types: [ + { type: 'discrete', options: [ [ 'slice', 'clone' ] ] } + ] + }, + 'box-shadow': { + // https://drafts.csswg.org/css-backgrounds/#box-shadow + types: [ 'boxShadowList' ], + }, + 'box-sizing': { + // https://drafts.csswg.org/css-ui-4/#box-sizing + types: [ + { type: 'discrete', options: [ [ 'content-box', 'border-box' ] ] } + ] + }, + 'caption-side': { + // https://drafts.csswg.org/css-tables/#propdef-caption-side + types: [ + { type: 'discrete', options: [ [ 'top', 'bottom' ] ] } + ] + }, + 'caret-color': { + // https://drafts.csswg.org/css-ui/#propdef-caret-color + types: [ 'color' ] + }, + 'clear': { + // https://drafts.csswg.org/css-page-floats/#propdef-clear + types: [ + { type: 'discrete', options: [ [ 'left', 'right' ] ] } + ] + }, + 'clip': { + // https://drafts.fxtf.org/css-masking-1/#propdef-clip + types: [ + 'rect', + { type: 'discrete', options: [ [ 'rect(10px, 10px, 10px, 10px)', + 'auto' ], + [ 'rect(10px, 10px, 10px, 10px)', + 'rect(10px, 10px, 10px, auto)'] ] } + ] + }, + 'clip-path': { + // https://drafts.fxtf.org/css-masking-1/#propdef-clip-path + types: [ + ] + }, + 'clip-rule': { + // https://drafts.fxtf.org/css-masking-1/#propdef-clip-rule + types: [ + { type: 'discrete', options: [ [ 'evenodd', 'nonzero' ] ] } + ] + }, + 'color': { + // https://drafts.csswg.org/css-color/#propdef-color + types: [ 'color' ] + }, + 'color-adjust': { + // https://drafts.csswg.org/css-color-4/#color-adjust + types: [ + { type: 'discrete', options: [ [ 'economy', 'exact' ] ] } + ] + }, + 'color-interpolation': { + // https://svgwg.org/svg2-draft/painting.html#ColorInterpolationProperty + types: [ + { type: 'discrete', options: [ [ 'linearrgb', 'auto' ] ] } + ] + }, + 'color-interpolation-filters': { + // https://drafts.fxtf.org/filters-1/#propdef-color-interpolation-filters + types: [ + { type: 'discrete', options: [ [ 'srgb', 'linearrgb' ] ] } + ] + }, + 'column-count': { + // https://drafts.csswg.org/css-multicol/#propdef-column-count + types: [ 'positiveInteger', + { type: 'discrete', options: [ [ 'auto', '10' ] ] } + ] + }, + 'column-gap': { + // https://drafts.csswg.org/css-multicol/#propdef-column-gap + types: [ 'length', + { type: 'discrete', options: [ [ 'normal', '200px' ] ] } + ] + }, + 'column-rule-color': { + // https://drafts.csswg.org/css-multicol/#propdef-column-rule-color + types: [ 'color' ] + }, + 'column-fill': { + // https://drafts.csswg.org/css-multicol/#propdef-column-fill + types: [ + { type: 'discrete', options: [ [ 'auto', 'balance' ] ] } + ] + }, + 'column-rule-style': { + // https://drafts.csswg.org/css-multicol/#propdef-column-rule-style + types: [ + { type: 'discrete', options: [ [ 'none', 'dotted' ] ] } + ] + }, + 'column-rule-width': { + // https://drafts.csswg.org/css-multicol/#propdef-column-rule-width + types: [ 'length' ], + setup: t => { + const element = createElement(t); + element.style.columnRuleStyle = 'solid'; + return element; + } + }, + 'column-width': { + // https://drafts.csswg.org/css-multicol/#propdef-column-width + types: [ 'length', + { type: 'discrete', options: [ [ 'auto', '1px' ] ] } + ] + }, + 'counter-increment': { + // https://drafts.csswg.org/css-lists-3/#propdef-counter-increment + types: [ + { type: 'discrete', options: [ [ 'ident-1 1', 'ident-2 2' ] ] } + ] + }, + 'counter-reset': { + // https://drafts.csswg.org/css-lists-3/#propdef-counter-reset + types: [ + { type: 'discrete', options: [ [ 'ident-1 1', 'ident-2 2' ] ] } + ] + }, + 'cursor': { + // https://drafts.csswg.org/css2/ui.html#propdef-cursor + types: [ + { type: 'discrete', options: [ [ 'pointer', 'wait' ] ] } + ] + }, + 'dominant-baseline': { + // https://drafts.csswg.org/css-inline/#propdef-dominant-baseline + types: [ + { type: 'discrete', options: [ [ 'ideographic', 'alphabetic' ] ] } + ] + }, + 'empty-cells': { + // https://drafts.csswg.org/css-tables/#propdef-empty-cells + types: [ + { type: 'discrete', options: [ [ 'show', 'hide' ] ] } + ] + }, + 'fill': { + // https://svgwg.org/svg2-draft/painting.html#FillProperty + types: [ + ] + }, + 'fill-opacity': { + // https://svgwg.org/svg2-draft/painting.html#FillOpacityProperty + types: [ 'opacity' ] + }, + 'fill-rule': { + // https://svgwg.org/svg2-draft/painting.html#FillRuleProperty + types: [ + { type: 'discrete', options: [ [ 'evenodd', 'nonzero' ] ] } + ] + }, + 'filter': { + // https://drafts.fxtf.org/filters/#propdef-filter + types: [ 'filterList' ] + }, + 'flex-basis': { + // https://drafts.csswg.org/css-flexbox/#propdef-flex-basis + types: [ + 'lengthPercentageOrCalc', + { type: 'discrete', options: [ [ 'auto', '10px' ] ] } + ] + }, + 'flex-direction': { + // https://drafts.csswg.org/css-flexbox/#propdef-flex-direction + types: [ + { type: 'discrete', options: [ [ 'row', 'row-reverse' ] ] } + ] + }, + 'flex-grow': { + // https://drafts.csswg.org/css-flexbox/#flex-grow-property + types: [ 'positiveNumber' ] + }, + 'flex-shrink': { + // https://drafts.csswg.org/css-flexbox/#propdef-flex-shrink + types: [ 'positiveNumber' ] + }, + 'flex-wrap': { + // https://drafts.csswg.org/css-flexbox/#propdef-flex-wrap + types: [ + { type: 'discrete', options: [ [ 'nowrap', 'wrap' ] ] } + ] + }, + 'flood-color': { + // https://drafts.fxtf.org/filters/#FloodColorProperty + types: [ 'color' ] + }, + 'flood-opacity': { + // https://drafts.fxtf.org/filters/#propdef-flood-opacity + types: [ 'opacity' ] + }, + 'font-size': { + // https://drafts.csswg.org/css-fonts-3/#propdef-font-size + types: [ + ] + }, + 'font-size-adjust': { + // https://drafts.csswg.org/css-fonts-3/#propdef-font-size-adjust + types: [ + ] + }, + 'font-stretch': { + // https://drafts.csswg.org/css-fonts-4/#propdef-font-stretch + types: [ 'percentage' ] + }, + 'font-style': { + // https://drafts.csswg.org/css-fonts/#propdef-font-style + types: [ + { type: 'discrete', options: [ [ 'italic', 'oblique' ] ] } + ] + }, + 'float': { + // https://drafts.csswg.org/css-page-floats/#propdef-float + types: [ + { type: 'discrete', options: [ [ 'left', 'right' ] ] } + ] + }, + 'font-family': { + // https://drafts.csswg.org/css-fonts-3/#propdef-font-family + types: [ + { type: 'discrete', options: [ [ 'helvetica', 'verdana' ] ] } + ] + }, + 'font-feature-settings': { + // https://drafts.csswg.org/css-fonts/#descdef-font-feature-settings + types: [ + { type: 'discrete', options: [ [ '"liga" 5', 'normal' ] ] } + ] + }, + 'font-kerning': { + // https://drafts.csswg.org/css-fonts-3/#propdef-font-kerning + types: [ + { type: 'discrete', options: [ [ 'auto', 'normal' ] ] } + ] + }, + 'font-language-override': { + // https://drafts.csswg.org/css-fonts-3/#propdef-font-language-override + types: [ + { type: 'discrete', options: [ [ '"eng"', 'normal' ] ] } + ] + }, + 'font-style': { + // https://drafts.csswg.org/css-fonts-3/#propdef-font-style + types: [ + { type: 'discrete', options: [ [ 'italic', 'oblique' ] ] } + ] + }, + 'font-synthesis': { + // https://drafts.csswg.org/css-fonts-3/#propdef-font-synthesis + types: [ + { type: 'discrete', options: [ [ 'none', 'weight style' ] ] } + ] + }, + 'font-variant-alternates': { + // https://drafts.csswg.org/css-fonts-3/#propdef-font-variant-alternates + types: [ + { type: 'discrete', + options: [ [ 'swash(unknown)', 'stylistic(unknown)' ] ] } + ] + }, + 'font-variant-caps': { + // https://drafts.csswg.org/css-fonts-3/#propdef-font-variant-caps + types: [ + { type: 'discrete', options: [ [ 'small-caps', 'unicase' ] ] } + ] + }, + 'font-variant-east-asian': { + // https://drafts.csswg.org/css-fonts-3/#propdef-font-variant-east-asian + types: [ + { type: 'discrete', options: [ [ 'full-width', 'proportional-width' ] ] } + ] + }, + 'font-variant-ligatures': { + // https://drafts.csswg.org/css-fonts-3/#propdef-font-variant-ligatures + types: [ + { type: 'discrete', + options: [ [ 'common-ligatures', 'no-common-ligatures' ] ] } + ] + }, + 'font-variant-numeric': { + // https://drafts.csswg.org/css-fonts-3/#propdef-font-variant-numeric + types: [ + { type: 'discrete', options: [ [ 'lining-nums', 'oldstyle-nums' ] ] } + ] + }, + 'font-variant-position': { + // https://drafts.csswg.org/css-fonts-3/#propdef-font-variant-position + types: [ + { type: 'discrete', options: [ [ 'sub', 'super' ] ] } + ] + }, + 'font-variation-settings': { + // https://drafts.csswg.org/css-fonts-4/#descdef-font-face-font-variation-settings + types: [ + 'fontVariationSettings', + { type: 'discrete', + options: [ ['"wdth" 1, "wght" 1.1', '"wdth" 5'], + ['"wdth" 5', 'normal'] + ] }, + ] + }, + 'font-weight': { + // https://drafts.csswg.org/css-fonts-3/#propdef-font-weight + types: [ + ] + }, + 'grid-auto-columns': { + // https://drafts.csswg.org/css-grid/#propdef-grid-auto-columns + types: [ + { type: 'discrete', options: [ [ '1px', '5px' ] ] } + ] + }, + 'grid-auto-flow': { + // https://drafts.csswg.org/css-grid/#propdef-grid-auto-flow + types: [ + { type: 'discrete', options: [ [ 'row', 'column' ] ] } + ] + }, + 'grid-auto-rows': { + // https://drafts.csswg.org/css-grid/#propdef-grid-auto-rows + types: [ + { type: 'discrete', options: [ [ '1px', '5px' ] ] } + ] + }, + 'grid-column-end': { + // https://drafts.csswg.org/css-grid/#propdef-grid-column-end + types: [ + { type: 'discrete', options: [ [ '1', '5' ] ] } + ] + }, + 'grid-column-gap': { + // https://drafts.csswg.org/css-grid/#propdef-grid-column-gap + types: [ + ] + }, + 'grid-column-start': { + // https://drafts.csswg.org/css-grid/#propdef-grid-column-start + types: [ + { type: 'discrete', options: [ [ '1', '5' ] ] } + ] + }, + 'grid-row-end': { + // https://drafts.csswg.org/css-grid/#propdef-grid-row-end + types: [ + { type: 'discrete', options: [ [ '1', '5' ] ] } + ] + }, + 'grid-row-gap': { + // https://drafts.csswg.org/css-grid/#propdef-grid-row-gap + types: [ + ] + }, + 'grid-row-start': { + // https://drafts.csswg.org/css-grid/#propdef-grid-row-start + types: [ + { type: 'discrete', options: [ [ '1', '5' ] ] } + ] + }, + 'grid-template-areas': { + // https://drafts.csswg.org/css-template/#grid-template-areas + types: [ + { type: 'discrete', options: [ [ '". . a b" ". .a b"', 'none' ] ] } + ] + }, + 'height': { + // https://drafts.csswg.org/css21/visudet.html#propdef-height + types: [ + ] + }, + 'hyphens': { + // https://drafts.csswg.org/css-text-3/#propdef-hyphens + types: [ + { type: 'discrete', options: [ [ 'manual', 'none' ] ] } + ] + }, + 'image-orientation': { + // https://drafts.csswg.org/css-images-3/#propdef-image-orientation + types: [ + { type: 'discrete', options: [ [ 'none', 'from-image' ] ] } + ] + }, + 'image-rendering': { + // https://drafts.csswg.org/css-images-3/#propdef-image-rendering + types: [ + ] + }, + 'ime-mode': { + // https://drafts.csswg.org/css-ui/#input-method-editor + types: [ + { type: 'discrete', options: [ [ 'disabled', 'auto' ] ] } + ] + }, + 'initial-letter': { + // https://drafts.csswg.org/css-inline/#propdef-initial-letter + types: [ + { type: 'discrete', options: [ [ '1 2', '3 4' ] ] } + ] + }, +}; + +const gCSSProperties2 = { + 'inline-size': { + // https://drafts.csswg.org/css-logical-props/#propdef-inline-size + types: [ + ] + }, + 'isolation': { + // https://drafts.fxtf.org/compositing-1/#propdef-isolation + types: [ + { type: 'discrete', options: [ [ 'auto', 'isolate' ] ] } + ] + }, + 'justify-content': { + // https://drafts.csswg.org/css-align/#propdef-justify-content + types: [ + { type: 'discrete', options: [ [ 'start', 'end' ] ] } + ] + }, + 'justify-items': { + // https://drafts.csswg.org/css-align/#propdef-justify-items + types: [ + { type: 'discrete', options: [ [ 'start', 'end' ] ] } + ] + }, + 'justify-self': { + // https://drafts.csswg.org/css-align/#propdef-justify-self + types: [ + { type: 'discrete', options: [ [ 'start', 'end' ] ] } + ] + }, + 'left': { + // https://drafts.csswg.org/css-position/#propdef-left + types: [ + ] + }, + 'letter-spacing': { + // https://drafts.csswg.org/css-text-3/#propdef-letter-spacing + types: [ 'length' ] + }, + 'lighting-color': { + // https://drafts.fxtf.org/filters/#LightingColorProperty + types: [ 'color' ] + }, + 'line-height': { + // https://drafts.csswg.org/css21/visudet.html#propdef-line-height + types: [ + ] + }, + 'list-style-image': { + // https://drafts.csswg.org/css-lists-3/#propdef-list-style-image + types: [ + { type: 'discrete', + options: [ [ 'url("http://localhost/test-1")', + 'url("http://localhost/test-2")' ] ] } + ] + }, + 'list-style-position': { + // https://drafts.csswg.org/css-lists-3/#propdef-list-style-position + types: [ + { type: 'discrete', options: [ [ 'inside', 'outside' ] ] } + ] + }, + 'list-style-type': { + // https://drafts.csswg.org/css-lists-3/#propdef-list-style-type + types: [ + { type: 'discrete', options: [ [ 'circle', 'square' ] ] } + ] + }, + 'margin-block-end': { + // https://drafts.csswg.org/css-logical-props/#propdef-margin-block-end + types: [ + ] + }, + 'margin-block-start': { + // https://drafts.csswg.org/css-logical-props/#propdef-margin-block-start + types: [ + ] + }, + 'margin-bottom': { + // https://drafts.csswg.org/css-box/#propdef-margin-bottom + types: [ + ] + }, + 'margin-inline-end': { + // https://drafts.csswg.org/css-logical-props/#propdef-margin-inline-end + types: [ + ] + }, + 'margin-inline-start': { + // https://drafts.csswg.org/css-logical-props/#propdef-margin-inline-start + types: [ + ] + }, + 'margin-left': { + // https://drafts.csswg.org/css-box/#propdef-margin-left + types: [ + ] + }, + 'margin-right': { + // https://drafts.csswg.org/css-box/#propdef-margin-right + types: [ + ] + }, + 'margin-top': { + // https://drafts.csswg.org/css-box/#propdef-margin-top + types: [ + ] + }, + 'marker-end': { + // https://svgwg.org/specs/markers/#MarkerEndProperty + types: [ + { type: 'discrete', + options: [ [ 'url("http://localhost/test-1")', + 'url("http://localhost/test-2")' ] ] } + ] + }, + 'marker-mid': { + // https://svgwg.org/specs/markers/#MarkerMidProperty + types: [ + { type: 'discrete', + options: [ [ 'url("http://localhost/test-1")', + 'url("http://localhost/test-2")' ] ] } + ] + }, + 'marker-start': { + // https://svgwg.org/specs/markers/#MarkerStartProperty + types: [ + { type: 'discrete', + options: [ [ 'url("http://localhost/test-1")', + 'url("http://localhost/test-2")' ] ] } + ] + }, + 'mask': { + // https://drafts.fxtf.org/css-masking-1/#the-mask + types: [ + { type: 'discrete', + options: [ [ 'url("http://localhost/test-1")', + 'url("http://localhost/test-2")' ] ] } + ] + }, + 'mask-clip': { + // https://drafts.fxtf.org/css-masking-1/#propdef-mask-clip + types: [ + { type: 'discrete', options: [ [ 'content-box', 'border-box' ] ] } + ] + }, + 'mask-composite': { + // https://drafts.fxtf.org/css-masking-1/#propdef-mask-composite + types: [ + { type: 'discrete', options: [ [ 'add', 'subtract' ] ] } + ] + }, + 'mask-image': { + // https://drafts.fxtf.org/css-masking-1/#propdef-mask-image + types: [ + { type: 'discrete', + options: [ [ 'url("http://localhost/test-1")', + 'url("http://localhost/test-2")' ] ] } + ] + }, + 'mask-mode': { + // https://drafts.fxtf.org/css-masking-1/#propdef-mask-mode + types: [ + { type: 'discrete', options: [ [ 'alpha', 'luminance' ] ] } + ] + }, + 'mask-origin': { + // https://drafts.fxtf.org/css-masking-1/#propdef-mask-origin + types: [ + { type: 'discrete', options: [ [ 'content-box', 'border-box' ] ] } + ] + }, + 'mask-position': { + // https://drafts.fxtf.org/css-masking-1/#propdef-mask-position + types: [ + ] + }, + 'mask-position-x': { + // https://lists.w3.org/Archives/Public/www-style/2014Jun/0166.html + types: [ + ] + }, + 'mask-position-y': { + // https://lists.w3.org/Archives/Public/www-style/2014Jun/0166.html + types: [ + ] + }, + 'mask-repeat': { + // https://drafts.fxtf.org/css-masking-1/#propdef-mask-repeat + types: [ + { type: 'discrete', options: [ [ 'space', 'round' ] ] } + ] + }, + 'mask-size': { + // https://drafts.fxtf.org/css-masking-1/#propdef-mask-size + types: [ + ] + }, + 'mask-type': { + // https://drafts.fxtf.org/css-masking-1/#propdef-mask-type + types: [ + { type: 'discrete', options: [ [ 'alpha', 'luminance' ] ] } + ] + }, + 'max-block-size': { + // https://drafts.csswg.org/css-logical-props/#propdef-max-block-size + types: [ + ] + }, + 'max-height': { + // https://drafts.csswg.org/css21/visudet.html#propdef-max-height + types: [ + ] + }, + 'max-inline-size': { + // https://drafts.csswg.org/css-logical-props/#propdef-max-inline-size + types: [ + ] + }, + 'max-width': { + // https://drafts.csswg.org/css21/visudet.html#propdef-max-width + types: [ + ] + }, + 'min-block-size': { + // https://drafts.csswg.org/css-logical-props/#propdef-min-block-size + types: [ + ] + }, + 'min-height': { + // https://drafts.csswg.org/css21/visudet.html#propdef-min-height + types: [ + ] + }, + 'min-inline-size': { + // https://drafts.csswg.org/css-logical-props/#propdef-min-inline-size + types: [ + ] + }, + 'min-width': { + // https://drafts.csswg.org/css21/visudet.html#propdef-min-width + types: [ + ] + }, + 'mix-blend-mode': { + // https://drafts.fxtf.org/compositing-1/#propdef-mix-blend-mode + types: [ + { type: 'discrete', options: [ [ 'multiply', 'screen' ] ] } + ] + }, + 'object-fit': { + // https://drafts.csswg.org/css-images-3/#propdef-object-fit + types: [ + { type: 'discrete', options: [ [ 'fill', 'contain' ] ] } + ] + }, + 'object-position': { + // https://drafts.csswg.org/css-images-3/#propdef-object-position + types: [ + ] + }, + 'inset-block-end': { + // https://drafts.csswg.org/css-logical-props/#propdef-inset-block-end + types: [ + ] + }, + 'inset-block-start': { + // https://drafts.csswg.org/css-logical-props/#propdef-inset-block-start + types: [ + ] + }, + 'inset-inline-end': { + // https://drafts.csswg.org/css-logical-props/#propdef-inset-inline-end + types: [ + ] + }, + 'inset-inline-start': { + // https://drafts.csswg.org/css-logical-props/#propdef-inset-inline-start + types: [ + ] + }, + 'offset-distance': { + // https://drafts.fxtf.org/motion-1/#offset-distance-property + types: [ 'lengthPercentageOrCalc' ] + }, + 'offset-path': { + // https://drafts.fxtf.org/motion-1/#offset-path-property + types: [ + ] + }, + 'opacity': { + // https://drafts.csswg.org/css-color/#propdef-opacity + types: [ + ] + }, + 'order': { + // https://drafts.csswg.org/css-flexbox/#propdef-order + types: [ 'integer' ] + }, + 'outline-color': { + // https://drafts.csswg.org/css-ui-3/#propdef-outline-color + types: [ 'color' ] + }, + 'outline-offset': { + // https://drafts.csswg.org/css-ui-3/#propdef-outline-offset + types: [ 'length' ] + }, + 'outline-style': { + // https://drafts.csswg.org/css-ui/#propdef-outline-style + types: [ + { type: 'discrete', options: [ [ 'none', 'dotted' ] ] } + ] + }, + 'outline-width': { + // https://drafts.csswg.org/css-ui-3/#propdef-outline-width + types: [ 'length' ], + setup: t => { + const element = createElement(t); + element.style.outlineStyle = 'solid'; + return element; + } + }, + 'overflow': { + // https://drafts.csswg.org/css-overflow/#propdef-overflow + types: [ + ] + }, + 'overflow-wrap': { + // https://drafts.csswg.org/css-text-3/#propdef-overflow-wrap + types: [ + { type: 'discrete', options: [ [ 'normal', 'break-word' ] ] } + ] + }, + 'overflow-x': { + // https://drafts.csswg.org/css-overflow-3/#propdef-overflow-x + types: [ + { type: 'discrete', options: [ [ 'visible', 'hidden' ] ] } + ] + }, + 'overflow-y': { + // https://drafts.csswg.org/css-overflow-3/#propdef-overflow-y + types: [ + { type: 'discrete', options: [ [ 'visible', 'hidden' ] ] } + ] + }, + 'padding-block-end': { + // https://drafts.csswg.org/css-logical-props/#propdef-padding-block-end + types: [ + ] + }, + 'padding-block-start': { + // https://drafts.csswg.org/css-logical-props/#propdef-padding-block-start + types: [ + ] + }, + 'padding-bottom': { + // https://drafts.csswg.org/css-box/#propdef-padding-bottom + types: [ + ] + }, + 'padding-inline-end': { + // https://drafts.csswg.org/css-logical-props/#propdef-padding-inline-end + types: [ + ] + }, + 'padding-inline-start': { + // https://drafts.csswg.org/css-logical-props/#propdef-padding-inline-start + types: [ + ] + }, + 'padding-left': { + // https://drafts.csswg.org/css-box/#propdef-padding-left + types: [ + ] + }, + 'padding-right': { + // https://drafts.csswg.org/css-box/#propdef-padding-right + types: [ + ] + }, + 'padding-top': { + // https://drafts.csswg.org/css-box/#propdef-padding-top + types: [ + ] + }, + 'page-break-after': { + // https://drafts.csswg.org/css-break-3/#propdef-break-after + types: [ + { type: 'discrete', options: [ [ 'always', 'auto' ] ] } + ] + }, + 'page-break-before': { + // https://drafts.csswg.org/css-break-3/#propdef-break-before + types: [ + { type: 'discrete', options: [ [ 'always', 'auto' ] ] } + ] + }, + 'page-break-inside': { + // https://drafts.csswg.org/css-break-3/#propdef-break-inside + types: [ + { type: 'discrete', options: [ [ 'auto', 'avoid' ] ] } + ] + }, + 'paint-order': { + // https://svgwg.org/svg2-draft/painting.html#PaintOrderProperty + types: [ + { type: 'discrete', options: [ [ 'fill', 'stroke' ] ] } + ] + }, + 'perspective': { + // https://drafts.csswg.org/css-transforms-1/#propdef-perspective + types: [ 'length' ] + }, + 'perspective-origin': { + // https://drafts.csswg.org/css-transforms-1/#propdef-perspective-origin + types: [ 'position' ] + }, + 'pointer-events': { + // https://svgwg.org/svg2-draft/interact.html#PointerEventsProperty + types: [ + { type: 'discrete', options: [ [ 'fill', 'none' ] ] } + ] + }, + 'position': { + // https://drafts.csswg.org/css-position/#propdef-position + types: [ + { type: 'discrete', options: [ [ 'absolute', 'fixed' ] ] } + ] + }, + 'quotes': { + // https://drafts.csswg.org/css-content-3/#propdef-quotes + types: [ + { type: 'discrete', options: [ [ '"“" "”" "‘" "’"', '"‘" "’" "“" "”"' ] ] } + ] + }, + 'resize': { + // https://drafts.csswg.org/css-ui/#propdef-resize + types: [ + { type: 'discrete', options: [ [ 'both', 'horizontal' ] ] } + ] + }, + 'right': { + // https://drafts.csswg.org/css-position/#propdef-right + types: [ + ] + }, + 'ruby-align': { + // https://drafts.csswg.org/css-ruby-1/#propdef-ruby-align + types: [ + { type: 'discrete', options: [ [ 'start', 'center' ] ] } + ] + }, + 'ruby-position': { + // https://drafts.csswg.org/css-ruby-1/#propdef-ruby-position + types: [ + { type: 'discrete', options: [ [ 'under', 'over' ] ] } + ], + setup: t => { + return createElement(t, 'ruby'); + } + }, + 'scroll-behavior': { + // https://drafts.csswg.org/cssom-view/#propdef-scroll-behavior + types: [ + { type: 'discrete', options: [ [ 'auto', 'smooth' ] ] } + ] + }, + 'shape-outside': { + // http://dev.w3.org/csswg/css-shapes/#propdef-shape-outside + types: [ + { type: 'discrete', + options: [ [ 'url("http://localhost/test-1")', + 'url("http://localhost/test-2")' ] ] } + ] + }, + 'shape-rendering': { + // https://svgwg.org/svg2-draft/painting.html#ShapeRenderingProperty + types: [ + { type: 'discrete', options: [ [ 'optimizeSpeed', 'crispEdges' ] ] } + ] + }, + 'stop-color': { + // https://svgwg.org/svg2-draft/pservers.html#StopColorProperty + types: [ 'color' ] + }, + 'stop-opacity': { + // https://svgwg.org/svg2-draft/pservers.html#StopOpacityProperty + types: [ 'opacity' ] + }, + 'stroke': { + // https://svgwg.org/svg2-draft/painting.html#StrokeProperty + types: [ + ] + }, + 'stroke-dasharray': { + // https://svgwg.org/svg2-draft/painting.html#StrokeDasharrayProperty + types: [ + 'dasharray', + { type: 'discrete', options: [ [ 'none', '10px, 20px' ] ] } + ] + }, + 'stroke-dashoffset': { + // https://svgwg.org/svg2-draft/painting.html#StrokeDashoffsetProperty + types: [ + ] + }, + 'stroke-linecap': { + // https://svgwg.org/svg2-draft/painting.html#StrokeLinecapProperty + types: [ + { type: 'discrete', options: [ [ 'round', 'square' ] ] } + ] + }, + 'stroke-linejoin': { + // https://svgwg.org/svg2-draft/painting.html#StrokeLinejoinProperty + types: [ + { type: 'discrete', options: [ [ 'round', 'miter' ] ] } + ], + setup: t => { + return createElement(t, 'rect'); + } + }, + 'stroke-miterlimit': { + // https://svgwg.org/svg2-draft/painting.html#StrokeMiterlimitProperty + types: [ 'positiveNumber' ] + }, + 'stroke-opacity': { + // https://svgwg.org/svg2-draft/painting.html#StrokeOpacityProperty + types: [ 'opacity' ] + }, + 'stroke-width': { + // https://svgwg.org/svg2-draft/painting.html#StrokeWidthProperty + types: [ + ] + }, + 'table-layout': { + // https://drafts.csswg.org/css-tables/#propdef-table-layout + types: [ + { type: 'discrete', options: [ [ 'auto', 'fixed' ] ] } + ] + }, + 'text-align': { + // https://drafts.csswg.org/css-text-3/#propdef-text-align + types: [ + { type: 'discrete', options: [ [ 'start', 'end' ] ] } + ] + }, + 'text-align-last': { + // https://drafts.csswg.org/css-text-3/#propdef-text-align-last + types: [ + { type: 'discrete', options: [ [ 'start', 'end' ] ] } + ] + }, + 'text-anchor': { + // https://svgwg.org/svg2-draft/text.html#TextAnchorProperty + types: [ + { type: 'discrete', options: [ [ 'middle', 'end' ] ] } + ] + }, + 'text-decoration-color': { + // https://drafts.csswg.org/css-text-decor-3/#propdef-text-decoration-color + types: [ 'color' ] + }, + 'text-decoration-line': { + // https://drafts.csswg.org/css-text-decor-3/#propdef-text-decoration-line + types: [ + { type: 'discrete', options: [ [ 'underline', 'overline' ] ] } + ] + }, + 'text-decoration-style': { + // http://dev.w3.org/csswg/css-text-decor-3/#propdef-text-decoration-style + types: [ + { type: 'discrete', options: [ [ 'solid', 'dotted' ] ] } + ] + }, + 'text-emphasis-color': { + // https://drafts.csswg.org/css-text-decor-3/#propdef-text-emphasis-color + types: [ 'color' ] + }, + 'text-emphasis-position': { + // http://dev.w3.org/csswg/css-text-decor-3/#propdef-text-emphasis-position + types: [ + { type: 'discrete', options: [ [ 'over', 'under left' ] ] } + ] + }, + 'text-emphasis-style': { + // http://dev.w3.org/csswg/css-text-decor-3/#propdef-text-emphasis-style + types: [ + { type: 'discrete', options: [ [ 'circle', 'open dot' ] ] } + ] + }, + 'text-group-align': { + // https://drafts.csswg.org/css-text-4/#propdef-text-group-align + types: [ + { type: 'discrete', options: [ [ 'none', 'center' ] ] } + ] + }, + 'text-indent': { + // https://drafts.csswg.org/css-text-3/#propdef-text-indent + types: [ + ] + }, + 'text-overflow': { + // https://drafts.csswg.org/css-ui/#propdef-text-overflow + types: [ + { type: 'discrete', options: [ [ 'clip', 'ellipsis' ] ] } + ] + }, + 'text-rendering': { + // https://svgwg.org/svg2-draft/painting.html#TextRenderingProperty + types: [ + { type: 'discrete', options: [ [ 'optimizeSpeed', 'optimizeLegibility' ] ] } + ] + }, + 'text-shadow': { + // https://drafts.csswg.org/css-text-decor-3/#propdef-text-shadow + types: [ 'textShadowList' ], + setup: t => { + const element = createElement(t); + element.style.color = 'green'; + return element; + } + }, + 'text-transform': { + // https://drafts.csswg.org/css-text-3/#propdef-text-transform + types: [ + { type: 'discrete', options: [ [ 'capitalize', 'uppercase' ] ] } + ] + }, + 'text-wrap': { + // https://drafts.csswg.org/css-text-4/#propdef-text-wrap + types: [ + { type: 'discrete', options: [ [ 'wrap', 'nowrap' ] ] } + ] + }, + 'touch-action': { + // https://w3c.github.io/pointerevents/#the-touch-action-css-property + types: [ + { type: 'discrete', options: [ [ 'auto', 'none' ] ] } + ] + }, + 'top': { + // https://drafts.csswg.org/css-position/#propdef-top + types: [ + ] + }, + 'transform': { + // https://drafts.csswg.org/css-transforms/#propdef-transform + types: [ 'transformList' ] + }, + 'transform-box': { + // https://drafts.csswg.org/css-transforms/#propdef-transform-box + types: [ + { type: 'discrete', options: [ [ 'fill-box', 'border-box' ] ] } + ] + }, + 'transform-origin': { + // https://drafts.csswg.org/css-transforms/#propdef-transform-origin + types: [ + ] + }, + 'transform-style': { + // https://drafts.csswg.org/css-transforms/#propdef-transform-style + types: [ + { type: 'discrete', options: [ [ 'flat', 'preserve-3d' ] ] } + ] + }, + 'rotate': { + // https://drafts.csswg.org/css-transforms-2/#individual-transforms + types: [ 'rotateList' ] + }, + 'translate': { + // https://drafts.csswg.org/css-transforms-2/#individual-transforms + types: [ 'translateList' ], + setup: t => { + // We need to set a width/height for resolving percentages against. + const element = createElement(t); + element.style.width = '100px'; + element.style.height = '100px'; + return element; + } + }, + 'scale': { + // https://drafts.csswg.org/css-transforms-2/#individual-transforms + types: [ 'scaleList' ] + }, + 'vector-effect': { + // https://svgwg.org/svg2-draft/coords.html#VectorEffectProperty + types: [ + { type: 'discrete', options: [ [ 'none', 'non-scaling-stroke' ] ] }, + ] + }, + 'vertical-align': { + // https://drafts.csswg.org/css21/visudet.html#propdef-vertical-align + types: [ + ] + }, + 'visibility': { + // https://drafts.csswg.org/css2/visufx.html#propdef-visibility + types: [ 'visibility' ] + }, + 'white-space': { + // https://drafts.csswg.org/css-text-4/#propdef-white-space + types: [ + { type: 'discrete', options: [ [ 'pre', 'nowrap' ] ] } + ] + }, + 'width': { + // https://drafts.csswg.org/css21/visudet.html#propdef-width + types: [ + ] + }, + 'word-break': { + // https://drafts.csswg.org/css-text-3/#propdef-word-break + types: [ + { type: 'discrete', options: [ [ 'keep-all', 'break-all' ] ] } + ] + }, + 'word-spacing': { + // https://drafts.csswg.org/css-text-3/#propdef-word-spacing + types: [ 'lengthPercentageOrCalc' ] + }, + 'z-index': { + // https://drafts.csswg.org/css-position/#propdef-z-index + types: [ + ] + }, +}; + +function testAnimationSamples(animation, idlName, testSamples) { + const pseudoType = animation.effect.pseudoElement; + const target = animation.effect.target; + for (const testSample of testSamples) { + animation.currentTime = testSample.time; + assert_equals(getComputedStyle(target, pseudoType)[idlName].toLowerCase(), + testSample.expected, + `The value should be ${testSample.expected}` + + ` at ${testSample.time}ms`); + } +} + +function toOrderedArray(string) { + return string.split(/\s*,\s/).sort(); +} + +// This test is for some list-based CSS properties such as font-variant-settings +// don't specify an order for serializing computed values. +// This test is for such the property. +function testAnimationSamplesWithAnyOrder(animation, idlName, testSamples) { + const type = animation.effect.pseudoElement; + const target = animation.effect.target; + for (const testSample of testSamples) { + animation.currentTime = testSample.time; + + // Convert to array and sort the expected and actual value lists first + // before comparing them. + const computedValues = + toOrderedArray(getComputedStyle(target, type)[idlName]); + const expectedValues = toOrderedArray(testSample.expected); + + assert_array_equals(computedValues, expectedValues, + `The computed values should be ${expectedValues}` + + ` at ${testSample.time}ms`); + } +} + +function RoundMatrix(style) { + var matrixMatch = style.match(/^(matrix(3d)?)\(.+\)$/); + if (!!matrixMatch) { + var matrixType = matrixMatch[1]; + var matrixArgs = style.substr(matrixType.length); + var extractmatrix = function(matrixStr) { + var list = []; + var regex = /[+\-]?[0-9]+[.]?[0-9]*(e[+/-][0-9]+)?/g; + var match = undefined; + do { + match = regex.exec(matrixStr); + if (match) { + list.push(parseFloat(parseFloat(match[0]).toFixed(6))); + } + } while (match); + return list; + } + return matrixType + '(' + extractmatrix(matrixArgs).join(', ') + ')'; + } + return style; +} + +function testAnimationSampleMatrices(animation, idlName, testSamples) { + const target = animation.effect.target; + for (const testSample of testSamples) { + animation.currentTime = testSample.time; + const actual = RoundMatrix(getComputedStyle(target)[idlName]); + const expected = RoundMatrix(createMatrixFromArray(testSample.expected)); + assert_matrix_equals(actual, expected, + `The value should be ${expected} at` + + ` ${testSample.time}ms but got ${actual}`); + } +} + +function testAnimationSampleRotate3d(animation, idlName, testSamples) { + const target = animation.effect.target; + for (const testSample of testSamples) { + animation.currentTime = testSample.time; + const actual = getComputedStyle(target)[idlName]; + const expected = testSample.expected; + assert_rotate3d_equals(actual, expected, + `The value should be ${expected} at` + + ` ${testSample.time}ms but got ${actual}`); + } +} + +function createTestElement(t, setup) { + return setup ? setup(t) : createElement(t); +} + +function isSupported(property) { + const testKeyframe = new TestKeyframe(propertyToIDL(property)); + assert_not_equals(window.KeyframeEffect, undefined, 'window.KeyframeEffect'); + try { + // Since TestKeyframe returns 'undefined' for |property|, + // the KeyframeEffect constructor will throw + // if the string 'undefined' is not a valid value for the property. + new KeyframeEffect(null, testKeyframe); + } catch(e) {} + return testKeyframe.propAccessCount !== 0; +} + +function TestKeyframe(testProp) { + let _propAccessCount = 0; + + Object.defineProperty(this, testProp, { + get: function() { _propAccessCount++; }, + enumerable: true + }); + + Object.defineProperty(this, 'propAccessCount', { + get: function() { return _propAccessCount; } + }); +} + +function propertyToIDL(property) { + // https://drafts.csswg.org/web-animations/#animation-property-name-to-idl-attribute-name + if (property === 'float') { + return 'cssFloat'; + } + return property.replace(/-[a-z]/gi, + function (str) { + return str.substr(1).toUpperCase(); }); +} +function calcFromPercentage(idlName, percentageValue) { + const examElem = document.createElement('div'); + document.body.appendChild(examElem); + examElem.style[idlName] = percentageValue; + + const calcValue = getComputedStyle(examElem)[idlName]; + document.body.removeChild(examElem); + + return calcValue; +} diff --git a/testing/web-platform/tests/web-animations/animation-model/animation-types/property-types.js b/testing/web-platform/tests/web-animations/animation-model/animation-types/property-types.js new file mode 100644 index 0000000000..6f39020b5c --- /dev/null +++ b/testing/web-platform/tests/web-animations/animation-model/animation-types/property-types.js @@ -0,0 +1,2769 @@ +'use strict'; + +const discreteType = { + testInterpolation: (property, setup, options) => { + for (const keyframes of options) { + const [ from, to ] = keyframes; + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate({ [idlName]: [from, to] }, + { duration: 1000, fill: 'both' }); + testAnimationSamples(animation, idlName, + [{ time: 0, expected: from.toLowerCase() }, + { time: 499, expected: from.toLowerCase() }, + { time: 500, expected: to.toLowerCase() }, + { time: 1000, expected: to.toLowerCase() }]); + }, `${property} uses discrete animation when animating between` + + ` "${from}" and "${to}" with linear easing`); + + test(t => { + // Easing: http://cubic-bezier.com/#.68,0,1,.01 + // With this curve, we don't reach the 50% point until about 95% of + // the time has expired. + const idlName = propertyToIDL(property); + const keyframes = {}; + const target = createTestElement(t, setup); + const animation = target.animate( + { [idlName]: [from, to] }, + { + duration: 1000, + fill: 'both', + easing: 'cubic-bezier(0.68,0,1,0.01)', + } + ); + testAnimationSamples(animation, idlName, + [{ time: 0, expected: from.toLowerCase() }, + { time: 940, expected: from.toLowerCase() }, + { time: 960, expected: to.toLowerCase() }]); + }, `${property} uses discrete animation when animating between` + + ` "${from}" and "${to}" with effect easing`); + + test(t => { + // Easing: http://cubic-bezier.com/#.68,0,1,.01 + // With this curve, we don't reach the 50% point until about 95% of + // the time has expired. + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate( + { + [idlName]: [from, to], + easing: 'cubic-bezier(0.68,0,1,0.01)', + }, + { duration: 1000, fill: 'both' } + ); + testAnimationSamples(animation, idlName, + [{ time: 0, expected: from.toLowerCase() }, + { time: 940, expected: from.toLowerCase() }, + { time: 960, expected: to.toLowerCase() }]); + }, `${property} uses discrete animation when animating between` + + ` "${from}" and "${to}" with keyframe easing`); + } + }, + + testAdditionOrAccumulation: (property, setup, options, composite) => { + for (const keyframes of options) { + const [ from, to ] = keyframes; + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.animate({ [idlName]: [from, from] }, 1000); + const animation = target.animate( + { [idlName]: [to, to] }, + { duration: 1000, composite } + ); + testAnimationSamples(animation, idlName, + [{ time: 0, expected: to.toLowerCase() }]); + }, `${property}: "${to}" onto "${from}"`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.animate({ [idlName]: [to, to] }, 1000); + const animation = target.animate( + { [idlName]: [from, from] }, + { duration: 1000, composite } + ); + testAnimationSamples(animation, idlName, + [{ time: 0, expected: from.toLowerCase() }]); + }, `${property}: "${from}" onto "${to}"`); + } + }, + + testAddition: function(property, setup, options) { + this.testAdditionOrAccumulation(property, setup, options, 'add'); + }, + + testAccumulation: function(property, setup, options) { + this.testAdditionOrAccumulation(property, setup, options, 'accumulate'); + }, +}; + +const lengthType = { + testInterpolation: (property, setup) => { + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate({ [idlName]: ['10px', '50px'] }, + { duration: 1000, fill: 'both' }); + testAnimationSamples(animation, idlName, + [{ time: 500, expected: '30px' }]); + }, `${property} supports animating as a length`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate({ [idlName]: ['1rem', '5rem'] }, + { duration: 1000, fill: 'both' }); + testAnimationSamples(animation, idlName, + [{ time: 500, expected: '30px' }]); + }, `${property} supports animating as a length of rem`); + }, + + testAdditionOrAccumulation: (property, setup, composite) => { + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = '10px'; + const animation = target.animate( + { [idlName]: ['10px', '50px'] }, + { duration: 1000, composite } + ); + testAnimationSamples(animation, idlName, [{ time: 0, expected: '20px' }]); + }, `${property}: length`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = '1rem'; + const animation = target.animate( + { [idlName]: ['1rem', '5rem'] }, + { duration: 1000, composite } + ); + testAnimationSamples(animation, idlName, [{ time: 0, expected: '20px' }]); + }, `${property}: length of rem`); + }, + + testAddition: function(property, setup) { + this.testAdditionOrAccumulation(property, setup, 'add'); + }, + + testAccumulation: function(property, setup) { + this.testAdditionOrAccumulation(property, setup, 'accumulate'); + }, +}; + +const lengthPairType = { + testInterpolation: (property, setup) => { + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate( + { [idlName]: ['10px 10px', '50px 50px'] }, + { duration: 1000, fill: 'both' } + ); + testAnimationSamples(animation, idlName, + [{ time: 500, expected: '30px 30px' }]); + }, `${property} supports animating as a length pair`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate( + { [idlName]: ['1rem 1rem', '5rem 5rem'] }, + { duration: 1000, fill: 'both' } + ); + testAnimationSamples(animation, idlName, + [{ time: 500, expected: '30px 30px' }]); + }, `${property} supports animating as a length pair of rem`); + }, + + testAdditionOrAccumulation: (property, setup, composite) => { + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = '10px 10px'; + const animation = target.animate( + { [idlName]: ['10px 10px', '50px 50px'] }, + { duration: 1000, composite } + ); + testAnimationSamples( + animation, + idlName, + [{ time: 0, expected: '20px 20px' }] + ); + }, `${property}: length pair`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = '1rem 1rem'; + const animation = target.animate( + { [idlName]: ['1rem 1rem', '5rem 5rem'] }, + { duration: 1000, composite } + ); + testAnimationSamples( + animation, + idlName, + [{ time: 0, expected: '20px 20px' }] + ); + }, `${property}: length pair of rem`); + }, + + testAddition: function(property, setup) { + this.testAdditionOrAccumulation(property, setup, 'add'); + }, + + testAccumulation: function(property, setup) { + this.testAdditionOrAccumulation(property, setup, 'accumulate'); + }, +}; + +const percentageType = { + testInterpolation: (property, setup) => { + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate({ [idlName]: ['10%', '50%'] }, + { duration: 1000, fill: 'both' }); + testAnimationSamples(animation, idlName, + [{ time: 500, expected: '30%' }]); + }, `${property} supports animating as a percentage`); + }, + + testAdditionOrAccumulation: (property, setup, composite) => { + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = '60%'; + const animation = target.animate( + { [idlName]: ['70%', '100%'] }, + { duration: 1000, composite } + ); + testAnimationSamples(animation, idlName, [{ time: 0, expected: '130%' }]); + }, `${property}: percentage`); + }, + + testAddition: function(property, setup) { + this.testAdditionOrAccumulation(property, setup, 'add'); + }, + + testAccumulation: function(property, setup) { + this.testAdditionOrAccumulation(property, setup, 'accumulate'); + }, +}; + +const integerType = { + testInterpolation: (property, setup) => { + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate({ [idlName]: [-2, 2] }, + { duration: 1000, fill: 'both' }); + testAnimationSamples(animation, idlName, + [{ time: 500, expected: '0' }]); + }, `${property} supports animating as an integer`); + }, + + testAdditionOrAccumulation: (property, setup, composite) => { + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = -1; + const animation = target.animate( + { [idlName]: [-2, 2] }, + { duration: 1000, composite } + ); + testAnimationSamples(animation, idlName, + [{ time: 0, expected: '-3' }]); + }, `${property}: integer`); + }, + + testAddition: function(property, setup) { + this.testAdditionOrAccumulation(property, setup, 'add'); + }, + + testAccumulation: function(property, setup) { + this.testAdditionOrAccumulation(property, setup, 'accumulate'); + }, +}; + +const positiveIntegerType = { + testInterpolation: (property, setup) => { + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate({ [idlName]: [1, 3] }, + { duration: 1000, fill: 'both' }); + testAnimationSamples(animation, idlName, + [ { time: 500, expected: '2' } ]); + }, `${property} supports animating as a positive integer`); + }, + + testAdditionOrAccumulation: (property, setup, composite) => { + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = 1; + const animation = target.animate( + { [idlName]: [2, 5] }, + { duration: 1000, composite } + ); + testAnimationSamples(animation, idlName, + [{ time: 0, expected: '3' }]); + }, `${property}: positive integer`); + }, + + testAddition: function(property, setup) { + this.testAdditionOrAccumulation(property, setup, 'add'); + }, + + testAccumulation: function(property, setup) { + this.testAdditionOrAccumulation(property, setup, 'accumulate'); + }, +}; + +const lengthPercentageOrCalcType = { + testInterpolation: (property, setup) => { + lengthType.testInterpolation(property, setup); + percentageType.testInterpolation(property, setup); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate({ [idlName]: ['10px', '20%'] }, + { duration: 1000, fill: 'both' }); + testAnimationSamples(animation, idlName, + [{ time: 500, expected: 'calc(10% + 5px)' }]); + }, `${property} supports animating as combination units "px" and "%"`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate({ [idlName]: ['10%', '2em'] }, + { duration: 1000, fill: 'both' }); + testAnimationSamples(animation, idlName, + [{ time: 500, expected: 'calc(5% + 10px)' }]); + }, `${property} supports animating as combination units "%" and "em"`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate({ [idlName]: ['1em', '2rem'] }, + { duration: 1000, fill: 'both' }); + testAnimationSamples(animation, idlName, + [{ time: 500, expected: '15px' }]); + }, `${property} supports animating as combination units "em" and "rem"`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate( + { [idlName]: ['10px', 'calc(1em + 20%)'] }, + { duration: 1000, fill: 'both' } + ); + testAnimationSamples(animation, idlName, + [{ time: 500, expected: 'calc(10% + 10px)' }]); + }, `${property} supports animating as combination units "px" and "calc"`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate( + { [idlName]: ['calc(10px + 10%)', 'calc(1em + 1rem + 20%)'] }, + { duration: 1000, fill: 'both' }); + testAnimationSamples(animation, idlName, + [{ time: 500, + expected: 'calc(15% + 15px)' }]); + }, `${property} supports animating as a calc`); + }, + + testAdditionOrAccumulation: (property, setup, composite) => { + lengthType.testAddition(property, setup); + percentageType.testAddition(property, setup); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = '10px'; + const animation = target.animate({ [idlName]: ['10%', '50%'] }, + { duration: 1000, composite }); + testAnimationSamples(animation, idlName, + [{ time: 0, expected: 'calc(10% + 10px)' }]); + }, `${property}: units "%" onto "px"`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = '10%'; + const animation = target.animate({ [idlName]: ['10px', '50px'] }, + { duration: 1000, composite }); + testAnimationSamples(animation, idlName, + [{ time: 0, expected: 'calc(10% + 10px)' }]); + }, `${property}: units "px" onto "%"`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = '10%'; + const animation = target.animate({ [idlName]: ['2rem', '5rem'] }, + { duration: 1000, composite }); + testAnimationSamples(animation, idlName, + [{ time: 0, expected: 'calc(10% + 20px)' }]); + }, `${property}: units "rem" onto "%"`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = '2rem'; + const animation = target.animate({ [idlName]: ['10%', '50%'] }, + { duration: 1000, composite }); + testAnimationSamples(animation, idlName, + [{ time: 0, expected: 'calc(10% + 20px)' }]); + }, `${property}: units "%" onto "rem"`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = '2em'; + const animation = target.animate({ [idlName]: ['2rem', '5rem'] }, + { duration: 1000, composite }); + testAnimationSamples(animation, idlName, [{ time: 0, expected: '40px' }]); + }, `${property}: units "rem" onto "em"`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = '2rem'; + const animation = target.animate({ [idlName]: ['2em', '5em'] }, + { duration: 1000, composite }); + testAnimationSamples(animation, idlName, [{ time: 0, expected: '40px' }]); + }, `${property}: units "em" onto "rem"`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = '10px'; + const animation = target.animate({ [idlName]: ['calc(2em + 20%)', + 'calc(5rem + 50%)'] }, + { duration: 1000, composite }); + testAnimationSamples(animation, idlName, + [{ time: 0, expected: 'calc(20% + 30px)' }]); + }, `${property}: units "calc" onto "px"`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = 'calc(10px + 10%)'; + const animation = target.animate({ [idlName]: ['calc(20px + 20%)', + 'calc(2em + 3rem + 40%)'] }, + { duration: 1000, composite }); + testAnimationSamples(animation, idlName, + [{ time: 0, expected: 'calc(30% + 30px)' }]); + }, `${property}: calc`); + }, + + testAddition: function(property, setup) { + this.testAdditionOrAccumulation(property, setup, 'add'); + }, + + testAccumulation: function(property, setup) { + this.testAdditionOrAccumulation(property, setup, 'accumulate'); + }, +}; + +const positiveNumberType = { + testInterpolation: (property, setup, expectedUnit='') => { + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate({ [idlName]: [1.1, 1.5] }, + { duration: 1000, fill: 'both' }); + testAnimationSamples(animation, idlName, + [{ time: 500, expected: '1.3' + expectedUnit }]); + }, `${property} supports animating as a positive number`); + }, + + testAdditionOrAccumulation: (property, setup, composite) => { + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = 1.1; + const animation = target.animate({ [idlName]: [1.1, 1.5] }, + { duration: 1000, composite }); + testAnimationSamples(animation, idlName, [{ time: 0, expected: '2.2' }]); + }, `${property}: positive number`); + }, + + testAddition: function(property, setup) { + this.testAdditionOrAccumulation(property, setup, 'add'); + }, + + testAccumulation: function(property, setup) { + this.testAdditionOrAccumulation(property, setup, 'accumulate'); + }, +}; + +// Test using float values in the range [0, 1] +const opacityType = { + testInterpolation: (property, setup) => { + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate({ [idlName]: [0.3, 0.8] }, + { duration: 1000, fill: 'both' }); + testAnimationSamples(animation, idlName, + [{ time: 500, expected: '0.55' }]); + }, `${property} supports animating as a [0, 1] number`); + }, + + testAdditionOrAccumulation: (property, setup, composite) => { + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = 0.3; + const animation = target.animate({ [idlName]: [0.3, 0.8] }, + { duration: 1000, composite }); + testAnimationSamples(animation, idlName, [{ time: 0, expected: '0.6' }]); + }, `${property}: [0, 1] number`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = 0.8; + const animation = target.animate({ [idlName]: [0.3, 0.8] }, + { duration: 1000, composite }); + testAnimationSamples(animation, idlName, [{ time: 0, expected: '1' }]); + }, `${property}: [0, 1] number (clamped)`); + }, + + testAddition: function(property, setup) { + this.testAdditionOrAccumulation(property, setup, 'add'); + }, + + testAccumulation: function(property, setup) { + this.testAdditionOrAccumulation(property, setup, 'accumulate'); + }, +}; + +const visibilityType = { + testInterpolation: (property, setup) => { + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate({ [idlName]: ['visible', 'hidden'] }, + { duration: 1000, fill: 'both' }); + testAnimationSamples(animation, idlName, + [{ time: 0, expected: 'visible' }, + { time: 999, expected: 'visible' }, + { time: 1000, expected: 'hidden' }]); + }, `${property} uses visibility animation when animating` + + ' from "visible" to "hidden"'); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate({ [idlName]: ['hidden', 'visible'] }, + { duration: 1000, fill: 'both' }); + testAnimationSamples(animation, idlName, + [{ time: 0, expected: 'hidden' }, + { time: 1, expected: 'visible' }, + { time: 1000, expected: 'visible' }]); + }, `${property} uses visibility animation when animating` + + ' from "hidden" to "visible"'); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate({ [idlName]: ['hidden', 'collapse'] }, + { duration: 1000, fill: 'both' }); + testAnimationSamples(animation, idlName, + [{ time: 0, expected: 'hidden' }, + { time: 499, expected: 'hidden' }, + { time: 500, expected: 'collapse' }, + { time: 1000, expected: 'collapse' }]); + }, `${property} uses visibility animation when animating` + + ' from "hidden" to "collapse"'); + + test(t => { + // Easing: http://cubic-bezier.com/#.68,-.55,.26,1.55 + // With this curve, the value is less than 0 till about 34% + // also more than 1 since about 63% + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = + target.animate({ [idlName]: ['visible', 'hidden'] }, + { duration: 1000, fill: 'both', + easing: 'cubic-bezier(0.68, -0.55, 0.26, 1.55)' }); + testAnimationSamples(animation, idlName, + [{ time: 0, expected: 'visible' }, + { time: 1, expected: 'visible' }, + { time: 330, expected: 'visible' }, + { time: 340, expected: 'visible' }, + { time: 620, expected: 'visible' }, + { time: 630, expected: 'hidden' }, + { time: 1000, expected: 'hidden' }]); + }, `${property} uses visibility animation when animating` + + ' from "visible" to "hidden" with easeInOutBack easing'); + }, + + testAdditionOrAccumulation: (property, setup, composite) => { + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = 'visible'; + const animation = target.animate({ [idlName]: ['visible', 'hidden'] }, + { duration: 1000, fill: 'both', + composite }); + testAnimationSamples(animation, idlName, + [{ time: 0, expected: 'visible' }, + { time: 1000, expected: 'hidden' }]); + }, `${property}: onto "visible"`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = 'hidden'; + const animation = target.animate({ [idlName]: ['hidden', 'visible'] }, + { duration: 1000, fill: 'both', + composite }); + testAnimationSamples(animation, idlName, + [{ time: 0, expected: 'hidden' }, + { time: 1000, expected: 'visible' }]); + }, `${property}: onto "hidden"`); + }, + + testAddition: function(property, setup) { + this.testAdditionOrAccumulation(property, setup, 'add'); + }, + + testAccumulation: function(property, setup) { + this.testAdditionOrAccumulation(property, setup, 'accumulate'); + }, +}; + +const colorType = { + testInterpolation: (property, setup) => { + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate({ [idlName]: ['rgb(255, 0, 0)', + 'rgb(0, 0, 255)'] }, + 1000); + testAnimationSamples(animation, idlName, + [{ time: 500, expected: 'rgb(128, 0, 128)' }]); + }, `${property} supports animating as color of rgb()`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate({ [idlName]: ['#ff0000', '#0000ff'] }, + 1000); + testAnimationSamples(animation, idlName, + [{ time: 500, expected: 'rgb(128, 0, 128)' }]); + }, `${property} supports animating as color of #RGB`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate({ [idlName]: ['hsl(0, 100%, 50%)', + 'hsl(240, 100%, 50%)'] }, + 1000); + testAnimationSamples(animation, idlName, + [{ time: 500, expected: 'rgb(128, 0, 128)' }]); + }, `${property} supports animating as color of hsl()`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate( + { [idlName]: ['#ff000066', '#0000ffcc'] }, + 1000 + ); + // R: 255 * (0.4 * 0.5) / 0.6 = 85 + // B: 255 * (0.8 * 0.5) / 0.6 = 170 + testAnimationSamples(animation, idlName, + [{ time: 500, expected: 'rgba(85, 0, 170, 0.6)' }]); + }, `${property} supports animating as color of #RGBa`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate( + { + [idlName]: ['rgba(255, 0, 0, 0.4)', 'rgba(0, 0, 255, 0.8)'], + }, + 1000 + ); + testAnimationSamples(animation, idlName, // Same as above. + [{ time: 500, expected: 'rgba(85, 0, 170, 0.6)' }]); + }, `${property} supports animating as color of rgba()`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate( + { + [idlName]: ['hsla(0, 100%, 50%, 0.4)', 'hsla(240, 100%, 50%, 0.8)'], + }, + 1000 + ); + testAnimationSamples(animation, idlName, // Same as above. + [{ time: 500, expected: 'rgba(85, 0, 170, 0.6)' }]); + }, `${property} supports animating as color of hsla()`); + }, + + testAdditionOrAccumulation: (property, setup, composite) => { + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = 'rgb(128, 128, 128)'; + const animation = target.animate( + { + [idlName]: ['rgb(255, 0, 0)', 'rgb(0, 0, 255)'], + }, + { duration: 1000, composite } + ); + testAnimationSamples(animation, idlName, + [{ time: 0, expected: 'rgb(255, 128, 128)' }, + // The value at 50% is interpolated + // from 'rgb(128+255, 128, 128)' + // to 'rgb(128, 128, 128+255)'. + { time: 500, expected: 'rgb(255, 128, 255)' }]); + }, `${property} supports animating as color of rgb() with overflowed ` + + ' from and to values'); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = 'rgb(128, 128, 128)'; + const animation = target.animate({ [idlName]: ['#ff0000', '#0000ff'] }, + { duration: 1000, composite }); + testAnimationSamples(animation, idlName, + [{ time: 0, expected: 'rgb(255, 128, 128)' }]); + }, `${property} supports animating as color of #RGB`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = 'rgb(128, 128, 128)'; + const animation = target.animate({ [idlName]: ['hsl(0, 100%, 50%)', + 'hsl(240, 100%, 50%)'] }, + { duration: 1000, composite }); + testAnimationSamples(animation, idlName, + [{ time: 0, expected: 'rgb(255, 128, 128)' }]); + }, `${property} supports animating as color of hsl()`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = 'rgb(128, 128, 128)'; + const animation = target.animate( + { [idlName]: ['#ff000066', '#0000ffcc'] }, + { duration: 1000, composite } + ); + testAnimationSamples(animation, idlName, + [{ time: 0, expected: 'rgb(230, 128, 128)' }]); + }, `${property} supports animating as color of #RGBa`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = 'rgb(128, 128, 128)'; + const animation = target.animate({ [idlName]: ['rgba(255, 0, 0, 0.4)', + 'rgba(0, 0, 255, 0.8)'] }, + { duration: 1000, composite }); + testAnimationSamples(animation, idlName, // Same as above. + [{ time: 0, expected: 'rgb(230, 128, 128)' }]); + }, `${property} supports animating as color of rgba()`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = 'rgb(128, 128, 128)'; + const animation = target.animate( + { + [idlName]: ['hsla(0, 100%, 50%, 0.4)', 'hsla(240, 100%, 50%, 0.8)'], + }, + { duration: 1000, composite } + ); + testAnimationSamples(animation, idlName, // Same as above. + [{ time: 0, expected: 'rgb(230, 128, 128)' }]); + }, `${property} supports animating as color of hsla()`); + }, + + testAddition: function(property, setup) { + this.testAdditionOrAccumulation(property, setup, 'add'); + }, + + testAccumulation: function(property, setup) { + this.testAdditionOrAccumulation(property, setup, 'accumulate'); + }, +}; + +const transformListType = { + testInterpolation: (property, setup) => { + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate( + { + [idlName]: ['translate(200px, -200px)', 'translate(400px, 400px)'], + }, + 1000 + ); + testAnimationSampleMatrices(animation, idlName, + [{ time: 500, expected: [ 1, 0, 0, 1, 300, 100 ] }]); + }, `${property}: translate`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate( + { + [idlName]: ['rotate(45deg)', 'rotate(135deg)'], + }, + 1000 + ); + + testAnimationSampleMatrices(animation, idlName, + [{ time: 500, expected: [ Math.cos(Math.PI / 2), + Math.sin(Math.PI / 2), + -Math.sin(Math.PI / 2), + Math.cos(Math.PI / 2), + 0, 0] }]); + }, `${property}: rotate`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate({ [idlName]: ['scale(3)', 'scale(5)'] }, + 1000); + + testAnimationSampleMatrices(animation, idlName, + [{ time: 500, expected: [ 4, 0, 0, 4, 0, 0 ] }]); + }, `${property}: scale`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate({ [idlName]: ['skew(30deg, 60deg)', + 'skew(60deg, 30deg)'] }, + 1000); + + testAnimationSampleMatrices(animation, idlName, + [{ time: 500, expected: [ 1, Math.tan(Math.PI / 4), + Math.tan(Math.PI / 4), 1, + 0, 0] }]); + }, `${property}: skew`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = + target.animate({ [idlName]: ['translateX(100px) rotate(45deg)', + 'translateX(200px) rotate(135deg)'] }, + 1000); + + testAnimationSampleMatrices(animation, idlName, + [{ time: 500, expected: [ Math.cos(Math.PI / 2), + Math.sin(Math.PI / 2), + -Math.sin(Math.PI / 2), + Math.cos(Math.PI / 2), + 150, 0 ] }]); + }, `${property}: rotate and translate`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = + target.animate({ [idlName]: ['rotate(45deg) translateX(100px)', + 'rotate(135deg) translateX(200px)'] }, + 1000); + + testAnimationSampleMatrices(animation, idlName, + [{ time: 500, expected: [ Math.cos(Math.PI / 2), + Math.sin(Math.PI / 2), + -Math.sin(Math.PI / 2), + Math.cos(Math.PI / 2), + 150 * Math.cos(Math.PI / 2), + 150 * Math.sin(Math.PI / 2) ] }]); + }, `${property}: translate and rotate`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = + target.animate({ [idlName]: ['rotate(0deg)', + 'rotate(1080deg) translateX(100px)'] }, + 1000); + + testAnimationSampleMatrices(animation, idlName, + [{ time: 500, expected: [ -1, 0, 0, -1, -50, 0 ] }]); + }, `${property}: extend shorter list (from)`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = + target.animate({ [idlName]: ['rotate(0deg) translateX(100px)', + 'rotate(1080deg)'] }, + 1000); + + testAnimationSampleMatrices(animation, idlName, + [{ time: 500, expected: [ -1, 0, 0, -1, -50, 0 ] }]); + }, `${property}: extend shorter list (to)`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = // matrix(0, 1, -1, 0, 0, 100) + target.animate({ [idlName]: ['rotate(90deg) translateX(100px)', + // matrix(-1, 0, 0, -1, 200, 0) + 'translateX(200px) rotate(180deg)'] }, + 1000); + + testAnimationSampleMatrices(animation, idlName, + [{ time: 500, expected: [ Math.cos(Math.PI * 3 / 4), + Math.sin(Math.PI * 3 / 4), + -Math.sin(Math.PI * 3 / 4), + Math.cos(Math.PI * 3 / 4), + 100, 50 ] }]); + }, `${property}: mismatch order of translate and rotate`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = // Same matrices as above. + target.animate({ [idlName]: [ 'matrix(0, 1, -1, 0, 0, 100)', + 'matrix(-1, 0, 0, -1, 200, 0)' ] }, + 1000); + + testAnimationSampleMatrices(animation, idlName, + [{ time: 500, expected: [ Math.cos(Math.PI * 3 / 4), + Math.sin(Math.PI * 3 / 4), + -Math.sin(Math.PI * 3 / 4), + Math.cos(Math.PI * 3 / 4), + 100, 50 ] }]); + }, `${property}: matrix`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = + target.animate({ [idlName]: [ 'rotate3d(1, 1, 0, 0deg)', + 'rotate3d(1, 1, 0, 90deg)'] }, + 1000); + + testAnimationSampleMatrices(animation, idlName, + [{ time: 500, expected: rotate3dToMatrix(1, 1, 0, Math.PI / 4) }]); + }, `${property}: rotate3d`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + // To calculate expected matrices easily, generate input matrices from + // rotate3d. + const from = rotate3dToMatrix3d(1, 1, 0, Math.PI / 4); + const to = rotate3dToMatrix3d(1, 1, 0, Math.PI * 3 / 4); + const animation = target.animate({ [idlName]: [ from, to ] }, 1000); + + testAnimationSampleMatrices(animation, idlName, + [{ time: 500, expected: rotate3dToMatrix(1, 1, 0, Math.PI * 2 / 4) }]); + }, `${property}: matrix3d`); + + // This test aims for forcing the two mismatched transforms to be + // decomposed into matrix3d before interpolation. Therefore, we not only + // test the interpolation, but also test the 3D matrix decomposition. + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate( + { + [idlName]: [ + 'scale(0.3)', + // scale(0.5) translateZ(1px) + 'matrix3d(0.5, 0, 0, 0, 0, 0.5, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1)', + ], + }, + 1000 + ); + + testAnimationSampleMatrices(animation, idlName, + [{ time: 500, expected: [ 0.4, 0, 0, 0, + 0, 0.4, 0, 0, + 0, 0, 1, 0, + 0, 0, 0.5, 1] }]); + }, `${property}: mismatched 3D transforms`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = + target.animate({ [idlName]: ['rotateY(60deg)', 'none' ] }, 1000); + + testAnimationSampleMatrices(animation, idlName, + // rotateY(30deg) == rotate3D(0, 1, 0, 30deg) + [{ time: 500, expected: rotate3dToMatrix(0, 1, 0, Math.PI / 6) }]); + }, `${property}: rotateY`); + + // Following tests aim for test the fallback discrete interpolation behavior + // for non-invertible matrices. The non-invertible matrix that we use is the + // singular matrix, matrix(1, 1, 0, 0, 0, 100). + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = + target.animate({ [idlName]: ['matrix(-1, 0, 0, -1, 200, 0)', + 'matrix( 1, 1, 0, 0, 0, 100)'] }, + { duration: 1000, fill: 'both' }); + + testAnimationSampleMatrices(animation, idlName, + [ { time: 0, expected: [ -1, 0, 0, -1, 200, 0 ] }, + { time: 499, expected: [ -1, 0, 0, -1, 200, 0 ] }, + { time: 500, expected: [ 1, 1, 0, 0, 0, 100 ] }, + { time: 1000, expected: [ 1, 1, 0, 0, 0, 100 ] }]); + }, `${property}: non-invertible matrices`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate( + { + [idlName]: [ + // matrix(0, -1, 1, 0, 250, 0) + 'translate(50px) matrix(-1, 0, 0, -1, 200, 0) rotate(90deg)', + // matrix(-1, -1, 0, 0, 100, 100) + 'translate(100px) matrix( 1, 1, 0, 0, 0, 100) rotate(180deg)', + ], + }, + { duration: 1000, fill: 'both' } + ); + + testAnimationSampleMatrices(animation, idlName, + [ { time: 0, expected: [ 0, -1, 1, 0, 250, 0 ] }, + { time: 499, expected: [ 0, -1, 1, 0, 250, 0 ] }, + { time: 500, expected: [ -1, -1, 0, 0, 100, 100 ] }, + { time: 1000, expected: [ -1, -1, 0, 0, 100, 100 ] }]); + }, `${property}: non-invertible matrices in matched transform lists`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate( + { + [idlName]: [ + // matrix(-2, 0, 0, -2, 250, 0) + 'translate(50px) matrix(-1, 0, 0, -1, 200, 0) scale(2)', + // matrix(1, 1, 1, 1, 100, 100) + 'translate(100px) matrix( 1, 1, 0, 0, 0, 100) skew(45deg)', + ], + }, + { duration: 1000, fill: 'both' } + ); + + testAnimationSampleMatrices(animation, idlName, + [ { time: 0, expected: [ -2, 0, 0, -2, 250, 0 ] }, + { time: 499, expected: [ -2, 0, 0, -2, 250, 0 ] }, + { time: 500, expected: [ 1, 1, 1, 1, 100, 100 ] }, + { time: 1000, expected: [ 1, 1, 1, 1, 100, 100 ] }]); + }, `${property}: non-invertible matrices in mismatched transform lists`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate( + { + // perspective(0) is treated as perspective(1px) + [idlName]: ['perspective(0)', 'perspective(10px)'], + }, + 1000 + ); + testAnimationSampleMatrices(animation, idlName, + [{ time: 500, expected: [ 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, -0.55, + 0, 0, 0, 1 ] }]); + }, `${property}: perspective`); + + }, + + testAddition: function(property, setup) { + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = 'translateX(100px)'; + const animation = target.animate({ [idlName]: ['translateX(-200px)', + 'translateX(500px)'] }, + { duration: 1000, fill: 'both', + composite: 'add' }); + testAnimationSampleMatrices(animation, idlName, + [ { time: 0, expected: [ 1, 0, 0, 1, -100, 0 ] }, + { time: 1000, expected: [ 1, 0, 0, 1, 600, 0 ] }]); + }, `${property}: translate`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = 'rotate(45deg)'; + const animation = target.animate({ [idlName]: ['rotate(-90deg)', + 'rotate(90deg)'] }, + { duration: 1000, fill: 'both', + composite: 'add' }); + + testAnimationSampleMatrices(animation, idlName, + [{ time: 0, expected: [ Math.cos(-Math.PI / 4), + Math.sin(-Math.PI / 4), + -Math.sin(-Math.PI / 4), + Math.cos(-Math.PI / 4), + 0, 0] }, + { time: 1000, expected: [ Math.cos(Math.PI * 3 / 4), + Math.sin(Math.PI * 3 / 4), + -Math.sin(Math.PI * 3 / 4), + Math.cos(Math.PI * 3 / 4), + 0, 0] }]); + }, `${property}: rotate`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = 'scale(2)'; + const animation = target.animate({ [idlName]: ['scale(-3)', 'scale(5)'] }, + { duration: 1000, fill: 'both', + composite: 'add' }); + + testAnimationSampleMatrices(animation, idlName, + [{ time: 0, expected: [ -6, 0, 0, -6, 0, 0 ] }, // scale(-3) scale(2) + { time: 1000, expected: [ 10, 0, 0, 10, 0, 0 ] }]); // scale(5) scale(2) + }, `${property}: scale`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + // matrix(1, tan(10deg), tan(10deg), 1) + target.style[idlName] = 'skew(10deg, 10deg)'; + const animation = // matrix(1, tan(20deg), tan(-30deg), 1) + target.animate({ [idlName]: ['skew(-30deg, 20deg)', + // matrix(1, tan(-30deg), tan(20deg), 1) + 'skew(20deg, -30deg)'] }, + { duration: 1000, fill: 'both', composite: 'add' }); + + // matrix at 0%. + // [ 1 tan(10deg) ] [ 1 tan(-30deg) ] + // [ tan(10deg) 1 ] [ tan(20deg) 1 ] = + // + // [ 1 + tan(10deg) * tan(20deg) tan(-30deg) + tan(10deg) ] + // [ tan(10deg) + tan(20deg) tan(10deg) * tan(-30deg) + 1 ] + + // matrix at 100%. + // [ 1 tan(10deg) ] [ 1 tan(20deg) ] + // [ tan(10deg) 1 ] [ tan(-30deg) 1 ] = + // + // [ 1 + tan(10deg) * tan(-30deg) tan(20deg) + tan(10deg) ] + // [ tan(10deg) + tan(-30deg) tan(10deg) * tan(20deg) + 1 ] + + testAnimationSampleMatrices(animation, idlName, + [{ time: 0, expected: [ 1 + Math.tan(Math.PI/18) * Math.tan(Math.PI/9), + Math.tan(Math.PI/18) + Math.tan(Math.PI/9), + Math.tan(-Math.PI/6) + Math.tan(Math.PI/18), + 1 + Math.tan(Math.PI/18) * Math.tan(-Math.PI/6), + 0, 0] }, + { time: 1000, expected: [ 1 + Math.tan(Math.PI/18) * Math.tan(-Math.PI/6), + Math.tan(Math.PI/18) + Math.tan(-Math.PI/6), + Math.tan(Math.PI/9) + Math.tan(Math.PI/18), + 1 + Math.tan(Math.PI/18) * Math.tan(Math.PI/9), + 0, 0] }]); + }, `${property}: skew`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + // matrix(1, 0, 0, 1, 100, 0) + target.style[idlName] = 'translateX(100px)'; + const animation = // matrix(0, 1, -1, 0, 0, 0) + target.animate({ [idlName]: ['rotate(90deg)', + // matrix(-1, 0, 0, -1, 0, 0) + 'rotate(180deg)'] }, + { duration: 1000, fill: 'both', composite: 'add' }); + + testAnimationSampleMatrices(animation, idlName, + [{ time: 0, expected: [ 0, 1, -1, 0, 100, 0 ] }, + { time: 1000, expected: [ -1, 0, 0, -1, 100, 0 ] }]); + }, `${property}: rotate on translate`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + // matrix(0, 1, -1, 0, 0, 0) + target.style[idlName] = 'rotate(90deg)'; + const animation = // matrix(1, 0, 0, 1, 100, 0) + target.animate({ [idlName]: ['translateX(100px)', + // matrix(1, 0, 0, 1, 200, 0) + 'translateX(200px)'] }, + { duration: 1000, fill: 'both', composite: 'add' }); + + testAnimationSampleMatrices(animation, idlName, + [{ time: 0, expected: [ 0, 1, -1, 0, 0, 100 ] }, + { time: 1000, expected: [ 0, 1, -1, 0, 0, 200 ] }]); + }, `${property}: translate on rotate`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = 'rotate(45deg) translateX(100px)'; + const animation = target.animate({ [idlName]: ['rotate(-90deg)', + 'rotate(90deg)'] }, + { duration: 1000, fill: 'both', + composite: 'add' }); + + testAnimationSampleMatrices(animation, idlName, + [{ time: 0, expected: [ Math.cos(-Math.PI / 4), + Math.sin(-Math.PI / 4), + -Math.sin(-Math.PI / 4), + Math.cos(-Math.PI / 4), + 100 * Math.cos(Math.PI / 4), + 100 * Math.sin(Math.PI / 4) ] }, + { time: 1000, expected: [ Math.cos(Math.PI * 3 / 4), + Math.sin(Math.PI * 3 / 4), + -Math.sin(Math.PI * 3 / 4), + Math.cos(Math.PI * 3 / 4), + 100 * Math.cos(Math.PI / 4), + 100 * Math.sin(Math.PI / 4) ] }]); + }, `${property}: rotate on rotate and translate`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = 'matrix(0, 1, -1, 0, 0, 0)'; + const animation = // Same matrices as above. + target.animate({ [idlName]: [ 'matrix(1, 0, 0, 1, 100, 0)', + 'matrix(1, 0, 0, 1, 200, 0)' ] }, + { duration: 1000, fill: 'both', composite: 'add' }); + + testAnimationSampleMatrices(animation, idlName, + [{ time: 0, expected: [ 0, 1, -1, 0, 0, 100 ] }, + { time: 1000, expected: [ 0, 1, -1, 0, 0, 200 ] }]); + }, `${property}: matrix`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = 'rotate3d(1, 1, 0, 45deg)'; + const animation = + target.animate({ [idlName]: [ 'rotate3d(1, 1, 0, -90deg)', + 'rotate3d(1, 1, 0, 90deg)'] }, + { duration: 1000, fill: 'both', composite: 'add' }); + + testAnimationSampleMatrices(animation, idlName, + [{ time: 0, expected: rotate3dToMatrix(1, 1, 0, -Math.PI / 4) }, + { time: 1000, expected: rotate3dToMatrix(1, 1, 0, 3 * Math.PI / 4) }]); + }, `${property}: rotate3d`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + // To calculate expected matrices easily, generate input matrices from + // rotate3d. + target.style[idlName] = rotate3dToMatrix3d(1, 1, 0, Math.PI / 4); + const from = rotate3dToMatrix3d(1, 1, 0, -Math.PI / 2); + const to = rotate3dToMatrix3d(1, 1, 0, Math.PI / 2); + const animation = + target.animate({ [idlName]: [ from, to ] }, + { duration: 1000, fill: 'both', composite: 'add' }); + + testAnimationSampleMatrices(animation, idlName, + [{ time: 0, expected: rotate3dToMatrix(1, 1, 0, -Math.PI / 4) }, + { time: 1000, expected: rotate3dToMatrix(1, 1, 0, 3 * Math.PI / 4) }]); + }, `${property}: matrix3d`); + + // Following tests aim for test the addition behavior for non-invertible + // matrices. Note that the addition for non-invertible matrices should be + // the same, just like addition for invertible matrices. With these tests, + // we can assure that addition never behaves as discrete. The non-invertible + // matrix that we use is the singular matrix, matrix(1, 1, 0, 0, 0, 100). + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = 'translateX(50px)'; + const animation = + target.animate({ [idlName]: ['matrix(-1, 0, 0, -1, 200, 0)', + 'matrix( 1, 1, 0, 0, 0, 100)'] }, + { duration: 1000, fill: 'both', composite: 'add' }); + + testAnimationSampleMatrices(animation, idlName, + [ { time: 0, expected: [ -1, 0, 0, -1, 250, 0 ] }, + { time: 1000, expected: [ 1, 1, 0, 0, 50, 100 ] }]); + }, `${property}: non-invertible matrices`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = 'translateX(50px)'; + const animation = // matrix(0, -1, 1, 0, 200, 0) + target.animate({ [idlName]: ['matrix(-1, 0, 0, -1, 200, 0) rotate(90deg)', + // matrix(-1, -1, 0, 0, 0, 100) + 'matrix( 1, 1, 0, 0, 0, 100) rotate(180deg)'] }, + { duration: 1000, fill: 'both', composite: 'add' }); + + testAnimationSampleMatrices(animation, idlName, + [ { time: 0, expected: [ 0, -1, 1, 0, 250, 0 ] }, + { time: 1000, expected: [ -1, -1, 0, 0, 50, 100 ] }]); + }, `${property}: non-invertible matrices in matched transform lists`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = 'translateX(50px)'; + const animation = // matrix(-2, 0, 0, -2, 200, 0) + target.animate({ [idlName]: ['matrix(-1, 0, 0, -1, 200, 0) scale(2)', + // matrix(1, 1, 1, 1, 0, 100) + 'matrix( 1, 1, 0, 0, 0, 100) skew(45deg)'] }, + { duration: 1000, fill: 'both', composite: 'add' }); + + testAnimationSampleMatrices(animation, idlName, + [ { time: 0, expected: [ -2, 0, 0, -2, 250, 0 ] }, + { time: 1000, expected: [ 1, 1, 1, 1, 50, 100 ] }]); + }, `${property}: non-invertible matrices in mismatched transform lists`); + }, + + testAccumulation: function(property, setup) { + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = 'translateX(100px)'; + const animation = target.animate({ [idlName]: ['translateX(-200px)', + 'translateX(500px)'] }, + { duration: 1000, fill: 'both', + composite: 'accumulate' }); + testAnimationSampleMatrices(animation, idlName, + [ { time: 0, expected: [ 1, 0, 0, 1, -100, 0 ] }, + { time: 1000, expected: [ 1, 0, 0, 1, 600, 0 ] }]); + }, `${property}: translate`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = 'rotate(45deg)'; + const animation = target.animate({ [idlName]: ['rotate(-90deg)', + 'rotate(90deg)'] }, + { duration: 1000, fill: 'both', + composite: 'accumulate' }); + + testAnimationSampleMatrices(animation, idlName, + [{ time: 0, expected: [ Math.cos(-Math.PI / 4), + Math.sin(-Math.PI / 4), + -Math.sin(-Math.PI / 4), + Math.cos(-Math.PI / 4), + 0, 0] }, + { time: 1000, expected: [ Math.cos(Math.PI * 3 / 4), + Math.sin(Math.PI * 3 / 4), + -Math.sin(Math.PI * 3 / 4), + Math.cos(Math.PI * 3 / 4), + 0, 0] }]); + }, `${property}: rotate`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = 'scale(2)'; + const animation = target.animate({ [idlName]: ['scale(-3)', 'scale(5)'] }, + { duration: 1000, fill: 'both', + composite: 'accumulate' }); + + testAnimationSampleMatrices(animation, idlName, + // scale((2 - 1) + (-3 - 1) + 1) + [{ time: 0, expected: [ -2, 0, 0, -2, 0, 0 ] }, + // scale((2 - 1) + (5 - 1) + 1) + { time: 1000, expected: [ 6, 0, 0, 6, 0, 0 ] }]); + }, `${property}: scale`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + // matrix(1, tan(10deg), tan(10deg), 1) + target.style[idlName] = 'skew(10deg, 10deg)'; + const animation = // matrix(1, tan(20deg), tan(-30deg), 1) + target.animate({ [idlName]: ['skew(-30deg, 20deg)', + // matrix(1, tan(-30deg), tan(20deg), 1) + 'skew(20deg, -30deg)'] }, + { duration: 1000, fill: 'both', composite: 'accumulate' }); + + testAnimationSampleMatrices(animation, idlName, + [{ time: 0, expected: [ 1, Math.tan(Math.PI/6), + Math.tan(-Math.PI/9), 1, + 0, 0] }, + { time: 1000, expected: [ 1, Math.tan(-Math.PI/9), + Math.tan(Math.PI/6), 1, + 0, 0] }]); + }, `${property}: skew`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + // matrix(1, 0, 0, 1, 100, 0) + target.style[idlName] = 'translateX(100px)'; + const animation = // matrix(0, 1, -1, 0, 0, 0) + target.animate({ [idlName]: ['rotate(90deg)', + // matrix(-1, 0, 0, -1, 0, 0) + 'rotate(180deg)'] }, + { duration: 1000, fill: 'both', composite: 'accumulate' }); + + testAnimationSampleMatrices(animation, idlName, + [{ time: 0, expected: [ 0, 1, -1, 0, 100, 0 ] }, + { time: 1000, expected: [ -1, 0, 0, -1, 100, 0 ] }]); + }, `${property}: rotate on translate`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + // matrix(0, 1, -1, 0, 0, 0) + target.style[idlName] = 'rotate(90deg)'; + const animation = // matrix(1, 0, 0, 1, 100, 0) + target.animate({ [idlName]: ['translateX(100px)', + // matrix(1, 0, 0, 1, 200, 0) + 'translateX(200px)'] }, + { duration: 1000, fill: 'both', composite: 'accumulate' }); + + testAnimationSampleMatrices(animation, idlName, + [{ time: 0, expected: [ 0, 1, -1, 0, 100, 0 ] }, + { time: 1000, expected: [ 0, 1, -1, 0, 200, 0 ] }]); + }, `${property}: translate on rotate`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = 'rotate(45deg)'; + const animation = + target.animate({ [idlName]: ['rotate(45deg) translateX(0px)', + 'rotate(45deg) translateX(100px)'] }, + { duration: 1000, fill: 'both', composite: 'accumulate' }); + + testAnimationSampleMatrices(animation, idlName, + [{ time: 0, expected: [ Math.cos(Math.PI / 2), + Math.sin(Math.PI / 2), + -Math.sin(Math.PI / 2), + Math.cos(Math.PI / 2), + 0 * Math.cos(Math.PI / 2), + 0 * Math.sin(Math.PI / 2) ] }, + { time: 1000, expected: [ Math.cos(Math.PI / 2), + Math.sin(Math.PI / 2), + -Math.sin(Math.PI / 2), + Math.cos(Math.PI / 2), + 100 * Math.cos(Math.PI / 2), + 100 * Math.sin(Math.PI / 2) ] }]); + }, `${property}: rotate and translate on rotate`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = 'rotate(45deg) translateX(100px)'; + const animation = + target.animate({ [idlName]: ['rotate(45deg)', 'rotate(45deg)'] }, + { duration: 1000, fill: 'both', composite: 'accumulate' }); + + testAnimationSampleMatrices(animation, idlName, + [{ time: 0, expected: [ Math.cos(Math.PI / 2), + Math.sin(Math.PI / 2), + -Math.sin(Math.PI / 2), + Math.cos(Math.PI / 2), + 100 * Math.cos(Math.PI / 2), + 100 * Math.sin(Math.PI / 2) ] }, + { time: 1000, expected: [ Math.cos(Math.PI / 2), + Math.sin(Math.PI / 2), + -Math.sin(Math.PI / 2), + Math.cos(Math.PI / 2), + 100 * Math.cos(Math.PI / 2), + 100 * Math.sin(Math.PI / 2) ] }]); + }, `${property}: rotate on rotate and translate`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = 'matrix(0, 1, -1, 0, 0, 0)'; + const animation = // Same matrices as above. + target.animate({ [idlName]: [ 'matrix(1, 0, 0, 1, 100, 0)', + 'matrix(1, 0, 0, 1, 200, 0)' ] }, + { duration: 1000, fill: 'both', composite: 'accumulate' }); + + testAnimationSampleMatrices(animation, idlName, + [{ time: 0, expected: [ 0, 1, -1, 0, 100, 0 ] }, + { time: 1000, expected: [ 0, 1, -1, 0, 200, 0 ] }]); + }, `${property}: matrix`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = 'rotate3d(1, 1, 0, 45deg)'; + const animation = + target.animate({ [idlName]: [ 'rotate3d(1, 1, 0, -90deg)', + 'rotate3d(1, 1, 0, 90deg)'] }, + { duration: 1000, fill: 'both', composite: 'accumulate' }); + + testAnimationSampleMatrices(animation, idlName, + [{ time: 0, expected: rotate3dToMatrix(1, 1, 0, -Math.PI / 4) }, + { time: 1000, expected: rotate3dToMatrix(1, 1, 0, 3 * Math.PI / 4) }]); + }, `${property}: rotate3d`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + // To calculate expected matrices easily, generate input matrices from + // rotate3d. + target.style[idlName] = rotate3dToMatrix3d(1, 1, 0, Math.PI / 4); + const from = rotate3dToMatrix3d(1, 1, 0, -Math.PI / 2); + const to = rotate3dToMatrix3d(1, 1, 0, Math.PI / 2); + const animation = + target.animate({ [idlName]: [ from, to ] }, + { duration: 1000, fill: 'both', composite: 'accumulate' }); + + testAnimationSampleMatrices(animation, idlName, + [{ time: 0, expected: rotate3dToMatrix(1, 1, 0, -Math.PI / 4) }, + { time: 1000, expected: rotate3dToMatrix(1, 1, 0, 3 * Math.PI / 4) }]); + }, `${property}: matrix3d`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const matrixArray = [ 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 1, 1 ]; + + target.style[idlName] = createMatrixFromArray(matrixArray); + const animation = + target.animate({ [idlName]: [ 'none', 'none' ] }, + { duration: 1000, fill: 'both', composite: 'accumulate' }); + + testAnimationSampleMatrices(animation, idlName, + [{ time: 0, expected: matrixArray }, + { time: 1000, expected: matrixArray }]); + }, `${property}: none`); + + // Following tests aim for test the fallback discrete accumulation behavior + // for non-invertible matrices. The non-invertible matrix that we use is the + // singular matrix, matrix(1, 1, 0, 0, 0, 100). + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.animate({ [idlName]: ['matrix(-1, 0, 0, -1, 200, 0)', + 'matrix(-1, 0, 0, -1, 200, 0)'] }, 1000); + const animation = target.animate( + { + [idlName]: [ + 'matrix( 1, 1, 0, 0, 0, 100)', + 'matrix( 1, 1, 0, 0, 0, 100)', + ], + }, + { duration: 1000, composite: 'accumulate' } + ); + testAnimationSampleMatrices(animation, idlName, [ + { time: 0, expected: [1, 1, 0, 0, 0, 100] }, + ]); + }, `${property}: non-invertible matrices (non-invertible onto invertible)`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.animate({ [idlName]: ['matrix( 1, 1, 0, 0, 0, 100)', + 'matrix( 1, 1, 0, 0, 0, 100)'] }, 1000); + const animation = target.animate( + { + [idlName]: [ + 'matrix(-1, 0, 0, -1, 200, 0)', + 'matrix(-1, 0, 0, -1, 200, 0)', + ], + }, + { duration: 1000, composite: 'accumulate' } + ); + testAnimationSampleMatrices(animation, idlName, [ + { time: 0, expected: [-1, 0, 0, -1, 200, 0] }, + ]); + }, `${property}: non-invertible matrices (invertible onto non-invertible)`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + // matrix(0, -1, 1, 0, 250, 0) + target.animate( + { + [idlName]: [ + 'translate(50px) matrix(-1, 0, 0, -1, 200, 0) rotate(90deg)', + 'translate(50px) matrix(-1, 0, 0, -1, 200, 0) rotate(90deg)', + ], + }, + 1000 + ); + // matrix(-1, -1, 0, 0, 100, 100) + const animation = target.animate( + { + [idlName]: [ + 'translate(100px) matrix( 1, 1, 0, 0, 0, 100) rotate(180deg)', + 'translate(100px) matrix( 1, 1, 0, 0, 0, 100) rotate(180deg)', + ], + }, + { duration: 1000, composite: 'accumulate' } + ); + testAnimationSampleMatrices(animation, idlName, [ + { time: 0, expected: [-1, -1, 0, 0, 100, 100] }, + ]); + }, `${property}: non-invertible matrices in matched transform lists (non-invertible onto invertible)`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + // matrix(-1, -1, 0, 0, 100, 100) + target.animate( + { + [idlName]: [ + 'translate(100px) matrix(1, 1, 0, 0, 0, 100) rotate(180deg)', + 'translate(100px) matrix(1, 1, 0, 0, 0, 100) rotate(180deg)', + ], + }, + 1000 + ); + // matrix(0, -1, 1, 0, 250, 0) + const animation = target.animate( + { + [idlName]: [ + 'translate(50px) matrix(-1, 0, 0, -1, 200, 0) rotate(90deg)', + 'translate(50px) matrix(-1, 0, 0, -1, 200, 0) rotate(90deg)', + ], + }, + { duration: 1000, composite: 'accumulate' } + ); + testAnimationSampleMatrices(animation, idlName, [ + { time: 0, expected: [0, -1, 1, 0, 250, 0] }, + ]); + }, `${property}: non-invertible matrices in matched transform lists (invertible onto non-invertible)`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + // matrix(-2, 0, 0, -2, 250, 0) + target.animate( + { + [idlName]: [ + 'translate(50px) matrix(-1, 0, 0, -1, 200, 0) scale(2)', + 'translate(50px) matrix(-1, 0, 0, -1, 200, 0) scale(2)', + ], + }, + 1000 + ); + // matrix(1, 1, 1, 1, 100, 100) + const animation = target.animate( + { + [idlName]: [ + 'translate(100px) matrix(1, 1, 0, 0, 0, 100) skew(45deg)', + 'translate(100px) matrix(1, 1, 0, 0, 0, 100) skew(45deg)', + ], + }, + { duration: 1000, composite: 'accumulate' } + ); + testAnimationSampleMatrices(animation, idlName, [ + { time: 0, expected: [1, 1, 1, 1, 100, 100] }, + ]); + }, `${property}: non-invertible matrices in mismatched transform lists` + + ' (non-invertible onto invertible)'); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + // matrix(1, 1, 1, 1, 100, 100) + target.animate( + { + [idlName]: [ + 'translate(100px) matrix(1, 1, 0, 0, 0, 100) skew(45deg)', + 'translate(100px) matrix(1, 1, 0, 0, 0, 100) skew(45deg)', + ], + }, + 1000 + ); + // matrix(-2, 0, 0, -2, 250, 0) + const animation = target.animate( + { + [idlName]: [ + 'translate(50px) matrix(-1, 0, 0, -1, 200, 0) scale(2)', + 'translate(50px) matrix(-1, 0, 0, -1, 200, 0) scale(2)', + ], + }, + { duration: 1000, composite: 'accumulate' } + ); + testAnimationSampleMatrices(animation, idlName, [ + { time: 0, expected: [-2, 0, 0, -2, 250, 0] }, + ]); + }, `${property}: non-invertible matrices in mismatched transform lists` + + ' (invertible onto non-invertible)'); + }, +}; + +const rotateListType = { + testInterpolation: (property, setup) => { + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate( + { + [idlName]: ['45deg', '135deg'], + }, + 1000 + ); + + testAnimationSamples(animation, idlName, + [{ time: 500, expected: '90deg' }]); + }, `${property} without rotation axes`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = + target.animate({ [idlName]: [ '0 1 0 0deg', + '0 1 0 90deg'] }, + 1000); + + testAnimationSamples(animation, idlName, + [{ time: 500, expected: 'y 45deg' }]); + }, `${property} with rotation axes`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + + const animation = + target.animate({ [idlName]: [ '0 1 0 0deg', + '0 1 0 720deg'] }, + 1000); + + testAnimationSamples(animation, idlName, + [{ time: 250, expected: 'y 180deg' }]); + }, `${property} with rotation axes and range over 360 degrees`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = + target.animate({ [idlName]: [ '0 1 0 0deg', + '0 1 1 90deg'] }, + 1000); + + testAnimationSampleRotate3d(animation, idlName, + [{ time: 500, expected: '0 0.707107 0.707107 45deg' }]); + }, `${property} with different rotation axes`); + }, + testAddition: function(property, setup) { + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = '45deg'; + const animation = target.animate({ [idlName]: ['-90deg', '90deg'] }, + { duration: 1000, fill: 'both', + composite: 'add' }); + + testAnimationSamples(animation, idlName, + [{ time: 0, expected: '-45deg' }, + { time: 1000, expected: '135deg' }]); + }, `${property} without rotation axes`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + // Rotation specified in transform property should not affect the computed + // value of |property|. + target.style.transform = 'rotate(20deg)'; + target.style[idlName] = 'y -45deg'; + const animation = + target.animate({ [idlName]: ['0 1 0 90deg', + '0 1 0 180deg'] }, + { duration: 1000, fill: 'both', composite: 'add' }); + + testAnimationSamples(animation, idlName, + [{ time: 0, expected: 'y 45deg' }, + { time: 1000, expected: 'y 135deg' }]); + }, `${property} with underlying transform`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = + target.animate({ [idlName]: [ '0 1 0 0deg', + '1 1 1 270deg'] }, + { duration: 1000, fill: 'both', composite: 'add' }); + + testAnimationSampleRotate3d(animation, idlName, + [{ time: 500, expected: '0.57735 0.57735 0.57735 135deg' }]); + }, `${property} with different rotation axes`); + }, + testAccumulation: function(property, setup) { + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = '45deg'; + const animation = target.animate({ [idlName]: ['-90deg', '90deg'] }, + { duration: 1000, fill: 'both', + composite: 'accumulate' }); + + testAnimationSamples(animation, idlName, + [{ time: 0, expected: '-45deg' }, + { time: 1000, expected: '135deg' }]); + }, `${property} without rotation axes`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style.transform = 'translateX(100px)'; + target.style[idlName] = '1 0 0 -45deg'; + const animation = + target.animate({ [idlName]: ['1 0 0 90deg', + '1 0 0 180deg'] }, + { duration: 1000, fill: 'both', composite: 'accumulate' }); + + testAnimationSamples(animation, idlName, + [{ time: 0, expected: 'x 45deg' }, + { time: 1000, expected: 'x 135deg' }]); + }, `${property} with underlying transform`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = + target.animate({ [idlName]: [ '0 1 0 0deg', + '1 0 1 180deg'] }, + { duration: 1000, fill: 'both', composite: 'accumulate' }); + + testAnimationSampleRotate3d(animation, idlName, + [{ time: 500, expected: '0.707107 0 0.707107 90deg' }]); + }, `${property} with different rotation axes`); + }, +}; + +const translateListType = { + testInterpolation: (property, setup) => { + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate( + { + [idlName]: ['200px', '400px'], + }, + 1000 + ); + testAnimationSamples(animation, idlName, + [{ time: 500, expected: '300px' }]); + }, `${property} with two unspecified values`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate( + { + [idlName]: ['200px -200px', '400px 400px'], + }, + 1000 + ); + testAnimationSamples(animation, idlName, + [{ time: 500, expected: '300px 100px' }]); + }, `${property} with one unspecified value`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate( + { + [idlName]: ['200px -200px 600px', '400px 400px -200px'], + }, + 1000 + ); + testAnimationSamples(animation, idlName, + [{ time: 500, expected: '300px 100px 200px' }]); + }, `${property} with all three values specified`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate( + { + [idlName]: ['0% -101px 600px', '400px 50% -200px'], + }, + 1000 + ); + testAnimationSamples(animation, idlName, + [{ time: 500, expected: 'calc(0% + 200px) calc(25% - 50.5px) 200px' }]); + }, `${property} with combination of percentages and lengths`); + }, + testAddition: function(property, setup) { + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = '100px'; + const animation = target.animate({ [idlName]: ['-200px', '500px'] }, + { duration: 1000, fill: 'both', + composite: 'add' }); + testAnimationSamples(animation, idlName, + [ { time: 0, expected: '-100px' }, + { time: 1000, expected: '600px' }]); + + }, `${property}`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.transform = 'translateY(100px)'; + target.style[idlName] = '100px'; + const animation = target.animate({ [idlName]: ['-200px', '500px'] }, + { duration: 1000, fill: 'both', + composite: 'add' }); + testAnimationSamples(animation, idlName, + [ { time: 0, expected: '-100px' }, + { time: 1000, expected: '600px' }]); + + }, `${property} with underlying transform`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = '50%'; + const animation = target.animate({ [idlName]: ['-200px', '500px'] }, + { duration: 1000, fill: 'both', + composite: 'add' }); + testAnimationSamples(animation, idlName, + [ { time: 0, expected: 'calc(50% - 200px)' }, + { time: 1000, expected: 'calc(50% + 500px)' }]); + + }, `${property} with underlying percentage value`); + }, + testAccumulation: function(property, setup) { + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = '100px'; + const animation = target.animate({ [idlName]: ['-200px', '500px'] }, + { duration: 1000, fill: 'both', + composite: 'accumulate' }); + testAnimationSamples(animation, idlName, + [ { time: 0, expected: '-100px' }, + { time: 1000, expected: '600px' }]); + }, `${property}`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.transform = 'translateY(100px)'; + target.style[idlName] = '100px'; + const animation = target.animate({ [idlName]: ['-200px', '500px'] }, + { duration: 1000, fill: 'both', + composite: 'accumulate' }); + testAnimationSamples(animation, idlName, + [ { time: 0, expected: '-100px' }, + { time: 1000, expected: '600px' }]); + }, `${property} with transform`); + }, +}; + +const scaleListType = { + testInterpolation: (property, setup) => { + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate({ [idlName]: ['3', '5'] }, + 1000); + + testAnimationSamples(animation, idlName, + [{ time: 500, expected: '4' }]); + }, `${property} with two unspecified values`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate({ [idlName]: ['3 3', '5 5'] }, + 1000); + + testAnimationSamples(animation, idlName, + [{ time: 500, expected: '4' }]); + }, `${property} with one unspecified value`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate({ [idlName]: ['3 3 3', '5 5 5'] }, + 1000); + + testAnimationSamples(animation, idlName, + [{ time: 500, expected: '4 4 4' }]); + }, `${property}`); + }, + testAddition: function(property, setup) { + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = '2'; + const animation = target.animate({ [idlName]: ['-3', '5'] }, + { duration: 1000, fill: 'both', + composite: 'add' }); + + testAnimationSamples(animation, idlName, + [{ time: 0, expected: '-6' }, + { time: 1000, expected: '10' }]); + }, `${property} with two unspecified values`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = '2 2'; + const animation = target.animate({ [idlName]: ['-3 -3', '5 5'] }, + { duration: 1000, fill: 'both', + composite: 'add' }); + + testAnimationSamples(animation, idlName, + [{ time: 0, expected: '-6' }, + { time: 1000, expected: '10' }]); + }, `${property} with one unspecified value`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = '2 2 2'; + const animation = target.animate({ [idlName]: ['-1 -2 -3', '4 5 6'] }, + { duration: 1000, fill: 'both', + composite: 'add' }); + + testAnimationSamples(animation, idlName, + [{ time: 0, expected: '-2 -4 -6' }, + { time: 1000, expected: '8 10 12' }]); + }, `${property}`); + }, + testAccumulation: function(property, setup) { + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = '2'; + const animation = target.animate({ [idlName]: ['-3', '5'] }, + { duration: 1000, fill: 'both', + composite: 'accumulate' }); + + testAnimationSamples(animation, idlName, + [{ time: 0, expected: '-2' }, + { time: 1000, expected: '6' }]); + }, `${property} with two unspecified values`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = '2 2'; + const animation = target.animate({ [idlName]: ['-3 -3', '5 5'] }, + { duration: 1000, fill: 'both', + composite: 'accumulate' }); + + testAnimationSamples(animation, idlName, + [{ time: 0, expected: '-2' }, + { time: 1000, expected: '6' }]); + }, `${property} with one unspecified value`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = '2 2 2'; + const animation = target.animate({ [idlName]: ['-1 -2 -3', '4 5 6'] }, + { duration: 1000, fill: 'both', + composite: 'accumulate' }); + + testAnimationSamples(animation, idlName, + [{ time: 0, expected: '0 -1 -2' }, + { time: 1000, expected: '5 6 7' }]); + }, `${property}`); + }, +}; + +const filterListType = { + testInterpolation: (property, setup) => { + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate({ [idlName]: + ['blur(10px)', 'blur(50px)'] }, + 1000); + + testAnimationSamples(animation, idlName, + [{ time: 500, expected: 'blur(30px)' }]); + }, `${property}: blur function`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate({ [idlName]: ['hue-rotate(0deg)', + 'hue-rotate(100deg)'] }, + 1000); + + testAnimationSamples(animation, idlName, + [{ time: 500, expected: 'hue-rotate(50deg)' }]); + }, `${property}: hue-rotate function with same unit(deg)`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate({ [idlName]: ['hue-rotate(10deg)', + 'hue-rotate(100rad)'] }, + 1000); + + // 10deg = 0.1745rad. + testAnimationSamples(animation, idlName, + [{ time: 500, expected: 'hue-rotate(2869.79deg)' }]); + }, `${property}: hue-rotate function with different unit(deg -> rad)`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate( + { [idlName]: + ['drop-shadow(10px 10px 10px rgba(255, 0, 0, 0.4))', + 'drop-shadow(50px 50px 50px rgba(0, 0, 255, 0.8))'] }, + 1000); + + testAnimationSamples( + animation, idlName, + [{ time: 500, + expected: 'drop-shadow(rgba(85, 0, 170, 0.6) 30px 30px 30px)' }]); + }, `${property}: drop-shadow function`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate( + { [idlName]: + ['brightness(0.1) contrast(0.1) grayscale(0.1) invert(0.1) ' + + 'opacity(0.1) saturate(0.1) sepia(0.1)', + 'brightness(0.5) contrast(0.5) grayscale(0.5) invert(0.5) ' + + 'opacity(0.5) saturate(0.5) sepia(0.5)'] }, + 1000); + + testAnimationSamples(animation, idlName, + [{ time: 500, + expected: 'brightness(0.3) contrast(0.3) grayscale(0.3) ' + + 'invert(0.3) opacity(0.3) saturate(0.3) sepia(0.3)' }]); + }, `${property}: percentage or numeric-specifiable functions` + + ' (number value)'); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate( + { [idlName]: + ['brightness(10%) contrast(10%) grayscale(10%) invert(10%) ' + + 'opacity(10%) saturate(10%) sepia(10%)', + 'brightness(50%) contrast(50%) grayscale(50%) invert(50%) ' + + 'opacity(50%) saturate(50%) sepia(50%)'] }, + 1000); + + testAnimationSamples(animation, idlName, + [{ time: 500, + expected: 'brightness(0.3) contrast(0.3) grayscale(0.3) ' + + 'invert(0.3) opacity(0.3) saturate(0.3) sepia(0.3)' }]); + }, `${property}: percentage or numeric-specifiable functions` + + ' (percentage value)'); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate( + { [idlName]: + // To make missing filter-function-lists, specified the grayscale. + ['grayscale(0)', + 'grayscale(1) brightness(0) contrast(0) opacity(0) saturate(0)' ]}, + 1000); + + testAnimationSamples(animation, idlName, + [{ time: 500, + expected: 'grayscale(0.5) brightness(0.5) contrast(0.5) ' + + 'opacity(0.5) saturate(0.5)' }]); + }, `${property}: interpolate different length of filter-function-list` + + ' with function which lacuna value is 1'); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate( + { [idlName]: + // To make missing filter-function-lists, specified the opacity. + ['opacity(1)', + 'opacity(0) grayscale(1) invert(1) sepia(1) blur(10px)'] }, + 1000); + + testAnimationSamples(animation, idlName, + [{ time: 500, + expected: + 'opacity(0.5) grayscale(0.5) invert(0.5) sepia(0.5) blur(5px)' }]); + }, `${property}: interpolate different length of filter-function-list` + + ' with function which lacuna value is 0'); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate( + { [idlName]: + ['blur(0px)', + 'blur(10px) drop-shadow(10px 10px 10px rgba(0, 0, 255, 0.8))'] }, + 1000); + + testAnimationSamples(animation, idlName, + [{ time: 500, + // Per the spec: The initial value for interpolation is all length values + // set to 0 and the used color set to transparent. + expected: 'blur(5px) drop-shadow(rgba(0, 0, 255, 0.4) 5px 5px 5px)' }]); + }, `${property}: interpolate different length of filter-function-list` + + ' with drop-shadow function'); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate({ [idlName]: ['none', 'blur(10px)'] }, + 1000); + + testAnimationSamples(animation, idlName, + [{ time: 500, expected: 'blur(5px)' }]); + }, `${property}: interpolate from none`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate( + { [idlName]: + ['blur(0px) url(\"#f1\")', + 'blur(10px) url(\"#f2\")']}, + 1000); + testAnimationSamples(animation, idlName, + [{ time: 499, expected: 'blur(0px) url(\"#f1\")' }, + { time: 500, expected: 'blur(10px) url(\"#f2\")' }]); + }, `${property}: url function (interpoalte as discrete)`); + }, + + testAddition: function(property, setup) { + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = 'blur(10px)'; + const animation = target.animate({ [idlName]: ['blur(20px)', + 'blur(50px)'] }, + { duration: 1000, composite: 'add' }); + testAnimationSamples(animation, idlName, + [ { time: 0, expected: 'blur(10px) blur(20px)' }]); + }, `${property}: blur on blur`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = 'blur(10px)'; + const animation = target.animate({ [idlName]: ['brightness(80%)', + 'brightness(40%)'] }, + { duration: 1000, composite: 'add' }); + testAnimationSamples(animation, idlName, + [ { time: 0, expected: 'blur(10px) brightness(0.8)' }]); + }, `${property}: different filter functions`); + }, + + testAccumulation: function(property, setup) { + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = 'blur(10px) brightness(0.3)'; + const animation = target.animate( + { + [idlName]: [ + 'blur(20px) brightness(0.1)', + 'blur(20px) brightness(0.1)', + ], + }, + { duration: 1000, composite: 'accumulate' } + ); + // brightness(0.1) onto brightness(0.3) means + // brightness((0.1 - 1.0) + (0.3 - 1.0) + 1.0). The result of this formula + // is brightness(-0.6) that means brightness(0.0). + testAnimationSamples(animation, idlName, + [ { time: 0, expected: 'blur(30px) brightness(0)' }]); + }, `${property}: same ordered filter functions`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = 'blur(10px) brightness(1.3)'; + const animation = target.animate( + { + [idlName]: [ + 'brightness(1.2) blur(20px)', + 'brightness(1.2) blur(20px)', + ], + }, + { duration: 1000, composite: 'accumulate' } + ); + // Mismatched ordered functions can't be accumulated. + testAnimationSamples(animation, idlName, + [ { time: 0, expected: 'brightness(1.2) blur(20px)' }]); + }, `${property}: mismatched ordered filter functions`); + }, +}; + +const textShadowListType = { + testInterpolation: (property, setup) => { + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = + target.animate({ [idlName]: [ 'none', + 'rgb(100, 100, 100) 10px 10px 10px'] }, + { duration: 1000, fill: 'both' }); + testAnimationSamples(animation, idlName, + // Premultiplied + [{ time: 500, expected: 'rgba(100, 100, 100, 0.5) 5px 5px 5px' }]); + }, `${property}: from none to other`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = + target.animate({ [idlName]: [ 'rgb(100, 100, 100) 10px 10px 10px', + 'none' ] }, + { duration: 1000, fill: 'both' }); + testAnimationSamples(animation, idlName, + // Premultiplied + [{ time: 500, expected: 'rgba(100, 100, 100, 0.5) 5px 5px 5px' }]); + }, `${property}: from other to none`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = + target.animate({ [idlName]: [ 'rgb(0, 0, 0) 0px 0px 0px', + 'rgb(100, 100, 100) 10px 10px 10px'] }, + { duration: 1000, fill: 'both' }); + testAnimationSamples(animation, idlName, + [{ time: 500, expected: 'rgb(50, 50, 50) 5px 5px 5px' }]); + }, `${property}: single shadow`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = + target.animate({ [idlName]: [ 'rgb(0, 0, 0) 0px 0px 0px, ' + + 'rgb(200, 200, 200) 20px 20px 20px', + 'rgb(100, 100, 100) 10px 10px 10px, ' + + 'rgb(100, 100, 100) 10px 10px 10px'] }, + { duration: 1000, fill: 'both' }); + testAnimationSamples(animation, idlName, + [{ time: 500, expected: 'rgb(50, 50, 50) 5px 5px 5px, ' + + 'rgb(150, 150, 150) 15px 15px 15px' }]); + }, `${property}: shadow list`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = + target.animate({ [idlName]: [ 'rgb(200, 200, 200) 20px 20px 20px', + 'rgb(100, 100, 100) 10px 10px 10px, ' + + 'rgb(100, 100, 100) 10px 10px 10px'] }, + { duration: 1000, fill: 'both' }); + testAnimationSamples(animation, idlName, + [{ time: 500, expected: 'rgb(150, 150, 150) 15px 15px 15px, ' + + 'rgba(100, 100, 100, 0.5) 5px 5px 5px' }]); + }, `${property}: mismatched list length (from longer to shorter)`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = + target.animate({ [idlName]: [ 'rgb(100, 100, 100) 10px 10px 10px, ' + + 'rgb(100, 100, 100) 10px 10px 10px', + 'rgb(200, 200, 200) 20px 20px 20px'] }, + { duration: 1000, fill: 'both' }); + testAnimationSamples(animation, idlName, + [{ time: 500, expected: 'rgb(150, 150, 150) 15px 15px 15px, ' + + 'rgba(100, 100, 100, 0.5) 5px 5px 5px' }]); + }, `${property}: mismatched list length (from shorter to longer)`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style.color = 'rgb(0, 255, 0)'; + const animation = + target.animate({ [idlName]: [ 'currentcolor 0px 0px 0px', + 'currentcolor 10px 10px 10px'] }, + { duration: 1000, fill: 'both' }); + testAnimationSamples(animation, idlName, + [{ time: 500, expected: 'rgb(0, 255, 0) 5px 5px 5px' }]); + }, `${property}: with currentcolor`); + }, + + testAddition: function(property, setup) { + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = 'rgb(0, 0, 0) 0px 0px 0px'; + const animation = + target.animate({ [idlName]: [ 'rgb(120, 120, 120) 10px 10px 10px', + 'rgb(120, 120, 120) 10px 10px 10px'] }, + { duration: 1000, composite: 'add' }); + testAnimationSamples(animation, idlName, + [ { time: 0, expected: 'rgb(0, 0, 0) 0px 0px 0px, ' + + 'rgb(120, 120, 120) 10px 10px 10px' }]); + }, `${property}: shadow`); + }, + + testAccumulation: function(property, setup) { + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = 'rgb(120, 120, 120) 10px 10px 10px'; + const animation = + target.animate({ [idlName]: [ 'rgb(120, 120, 120) 10px 10px 10px', + 'rgb(120, 120, 120) 10px 10px 10px'] }, + { duration: 1000, composite: 'accumulate' }); + testAnimationSamples(animation, idlName, + [ { time: 0, expected: 'rgb(240, 240, 240) 20px 20px 20px' }]); + }, `${property}: shadow`); + }, +}; + + +const boxShadowListType = { + testInterpolation: (property, setup) => { + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = + target.animate({ [idlName]: [ 'none', + 'rgb(100, 100, 100) 10px 10px 10px 0px'] }, + { duration: 1000, fill: 'both' }); + testAnimationSamples(animation, idlName, + // Premultiplied + [{ time: 500, expected: 'rgba(100, 100, 100, 0.5) 5px 5px 5px 0px' }]); + }, `${property}: from none to other`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = + target.animate({ [idlName]: [ 'rgb(100, 100, 100) 10px 10px 10px 0px', + 'none' ] }, + { duration: 1000, fill: 'both' }); + testAnimationSamples(animation, idlName, + // Premultiplied + [{ time: 500, expected: 'rgba(100, 100, 100, 0.5) 5px 5px 5px 0px' }]); + }, `${property}: from other to none`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = + target.animate({ [idlName]: [ 'rgb(0, 0, 0) 0px 0px 0px 0px', + 'rgb(100, 100, 100) 10px 10px 10px 0px'] }, + { duration: 1000, fill: 'both' }); + testAnimationSamples(animation, idlName, + [{ time: 500, expected: 'rgb(50, 50, 50) 5px 5px 5px 0px' }]); + }, `${property}: single shadow`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = + target.animate({ [idlName]: [ 'rgb(0, 0, 0) 0px 0px 0px 0px, ' + + 'rgb(200, 200, 200) 20px 20px 20px 20px', + 'rgb(100, 100, 100) 10px 10px 10px 0px, ' + + 'rgb(100, 100, 100) 10px 10px 10px 0px'] }, + { duration: 1000, fill: 'both' }); + testAnimationSamples(animation, idlName, + [{ time: 500, expected: 'rgb(50, 50, 50) 5px 5px 5px 0px, ' + + 'rgb(150, 150, 150) 15px 15px 15px 10px' }]); + }, `${property}: shadow list`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = + target.animate({ [idlName]: [ 'rgb(200, 200, 200) 20px 20px 20px 20px', + 'rgb(100, 100, 100) 10px 10px 10px 0px, ' + + 'rgb(100, 100, 100) 10px 10px 10px 0px'] }, + { duration: 1000, fill: 'both' }); + testAnimationSamples(animation, idlName, + [{ time: 500, expected: 'rgb(150, 150, 150) 15px 15px 15px 10px, ' + + 'rgba(100, 100, 100, 0.5) 5px 5px 5px 0px' }]); + }, `${property}: mismatched list length (from shorter to longer)`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = + target.animate({ [idlName]: [ 'rgb(100, 100, 100) 10px 10px 10px 0px, ' + + 'rgb(100, 100, 100) 10px 10px 10px 0px', + 'rgb(200, 200, 200) 20px 20px 20px 20px']}, + { duration: 1000, fill: 'both' }); + testAnimationSamples(animation, idlName, + [{ time: 500, expected: 'rgb(150, 150, 150) 15px 15px 15px 10px, ' + + 'rgba(100, 100, 100, 0.5) 5px 5px 5px 0px' }]); + }, `${property}: mismatched list length (from longer to shorter)`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style.color = 'rgb(0, 255, 0)'; + const animation = + target.animate({ [idlName]: [ 'currentcolor 0px 0px 0px 0px', + 'currentcolor 10px 10px 10px 10px'] }, + { duration: 1000, fill: 'both' }); + testAnimationSamples(animation, idlName, + [{ time: 500, expected: 'rgb(0, 255, 0) 5px 5px 5px 5px' }]); + }, `${property}: with currentcolor`); + }, + + testAddition: function(property, setup) { + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = 'rgb(0, 0, 0) 0px 0px 0px 0px'; + const animation = + target.animate({ [idlName]: [ 'rgb(120, 120, 120) 10px 10px 10px 0px', + 'rgb(120, 120, 120) 10px 10px 10px 0px'] }, + { duration: 1000, composite: 'add' }); + testAnimationSamples(animation, idlName, + [ { time: 0, expected: 'rgb(0, 0, 0) 0px 0px 0px 0px, ' + + 'rgb(120, 120, 120) 10px 10px 10px 0px' }]); + }, `${property}: shadow`); + }, + + testAccumulation: function(property, setup) { + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = 'rgb(120, 120, 120) 10px 10px 10px 10px'; + const animation = + target.animate({ [idlName]: [ 'rgb(120, 120, 120) 10px 10px 10px 10px', + 'rgb(120, 120, 120) 10px 10px 10px 10px'] }, + { duration: 1000, composite: 'accumulate' }); + testAnimationSamples(animation, idlName, + [ { time: 0, expected: 'rgb(240, 240, 240) 20px 20px 20px 20px' }]); + }, `${property}: shadow`); + }, +}; + +const positionType = { + testInterpolation: (property, setup) => { + lengthPairType.testInterpolation(property, setup); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate({ [idlName]: ['10% 10%', '50% 50%'] }, + { duration: 1000, fill: 'both' }); + testAnimationSamples( + animation, idlName, + [{ time: 500, expected: calcFromPercentage(idlName, '30% 30%') }]); + }, `${property} supports animating as a position of percent`); + }, + + testAdditionOrAccumulation: (property, setup, composite) => { + lengthPairType.testAddition(property, setup); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = '60% 60%'; + const animation = target.animate({ [idlName]: ['70% 70%', '100% 100%'] }, + { duration: 1000, composite }); + testAnimationSamples( + animation, idlName, + [{ time: 0, expected: calcFromPercentage(idlName, '130% 130%') }]); + }, `${property}: position of percentage`); + }, + + testAddition: function(property, setup) { + this.testAdditionOrAccumulation(property, setup, 'add'); + }, + + testAccumulation: function(property, setup) { + this.testAdditionOrAccumulation(property, setup, 'accumulate'); + }, +}; + +const rectType = { + testInterpolation: (property, setup) => { + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate({ [idlName]: + ['rect(10px, 10px, 10px, 10px)', + 'rect(50px, 50px, 50px, 50px)'] }, + { duration: 1000, fill: 'both' }); + testAnimationSamples( + animation, idlName, + [{ time: 500, expected: 'rect(30px, 30px, 30px, 30px)' }]); + }, `${property} supports animating as a rect`); + }, + + testAdditionOrAccumulation: (property, setup, composite) => { + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = 'rect(100px, 100px, 100px, 100px)'; + const animation = target.animate({ [idlName]: + ['rect(10px, 10px, 10px, 10px)', + 'rect(10px, 10px, 10px, 10px)'] }, + { duration: 1000, composite }); + testAnimationSamples( + animation, idlName, + [{ time: 0, expected: 'rect(110px, 110px, 110px, 110px)' }]); + }, `${property}: rect`); + }, + + testAddition: function(property, setup) { + this.testAdditionOrAccumulation(property, setup, 'add'); + }, + + testAccumulation: function(property, setup) { + this.testAdditionOrAccumulation(property, setup, 'accumulate'); + }, +} + +// stroke-dasharray: none | [ <length> | <percentage> | <number> ]* +const dasharrayType = { + testInterpolation: (property, setup) => { + percentageType.testInterpolation(property, setup); + positiveNumberType.testInterpolation(property, setup, 'px'); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate({ [idlName]: + ['8, 16, 4', + '4, 8, 12, 16'] }, + { duration: 1000, fill: 'both' }); + testAnimationSamples( + animation, idlName, + [{ time: 500, expected: '6px, 12px, 8px, 12px, 10px, 6px, 10px, 16px, 4px, 8px, 14px, 10px' }]); + }, `${property} supports animating as a dasharray (mismatched length)`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = target.animate({ [idlName]: + ['2, 50%, 6, 10', + '6, 30%, 2, 2'] }, + { duration: 1000, fill: 'both' }); + testAnimationSamples( + animation, idlName, + [{ time: 500, expected: '4px, 40%, 4px, 6px' }]); + }, `${property} supports animating as a dasharray (mixed lengths and percentages)`); + + }, + + // Note that stroke-dasharray is neither additive nor cumulative, so we should + // write this additive test case that animating value replaces underlying + // values. + // See https://www.w3.org/TR/SVG2/painting.html#StrokeDashing. + testAdditionOrAccumulation: (property, setup, composite) => { + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = '6, 30%, 2px'; + const animation = target.animate({ [idlName]: + ['1, 2, 3, 4, 5', + '6, 7, 8, 9, 10'] }, + { duration: 1000, composite }); + testAnimationSamples( + animation, idlName, + [{ time: 0, expected: '1px, 2px, 3px, 4px, 5px' }]); + }, `${property}: dasharray`); + }, + + testAddition: function(property, setup) { + this.testAdditionOrAccumulation(property, setup, 'add'); + }, + + testAccumulation: function(property, setup) { + this.testAdditionOrAccumulation(property, setup, 'accumulate'); + }, +} + +const fontVariationSettingsType = { + testInterpolation: (property, setup) => { + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = + target.animate({ [idlName]: ['"wght" 1.1', '"wght" 1.5'] }, + { duration: 1000, fill: 'both' }); + testAnimationSamples(animation, idlName, + [{ time: 0, expected: '"wght" 1.1' }, + { time: 250, expected: '"wght" 1.2' }, + { time: 750, expected: '"wght" 1.4' } ]); + }, `${property} supports animation as float`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = + target.animate({ [idlName]: ['"wdth" 1, "wght" 1.1', + '"wght" 1.5, "wdth" 5'] }, + { duration: 1000, fill: 'both' }); + testAnimationSamplesWithAnyOrder( + animation, idlName, + [{ time: 0, expected: '"wdth" 1, "wght" 1.1' }, + { time: 250, expected: '"wdth" 2, "wght" 1.2' }, + { time: 750, expected: '"wdth" 4, "wght" 1.4' } ]); + }, `${property} supports animation as float with multiple tags`); + + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + const animation = + target.animate({ [idlName]: ['"wdth" 1, "wght" 1.1', + '"wght" 10, "wdth" 5, "wght" 1.5'] }, + { duration: 1000, fill: 'both' }); + testAnimationSamplesWithAnyOrder( + animation, idlName, + [{ time: 250, expected: '"wdth" 2, "wght" 1.2' }, + { time: 750, expected: '"wdth" 4, "wght" 1.4' } ]); + }, `${property} supports animation as float with multiple duplicate tags`); + }, + + testAdditionOrAccumulation: (property, setup, composite) => { + test(t => { + const idlName = propertyToIDL(property); + const target = createTestElement(t, setup); + target.style[idlName] = '"wght" 1'; + const animation = + target.animate({ [idlName]: ['"wght" 1.1', '"wght" 1.5'] }, + { duration: 1000, composite }); + testAnimationSamples(animation, idlName, + [{ time: 250, expected: '"wght" 2.2' }, + { time: 750, expected: '"wght" 2.4' } ]); + }, `${property} with composite type ${composite}`); + }, + + testAddition: function(property, setup) { + this.testAdditionOrAccumulation(property, setup, 'add'); + }, + + testAccumulation: function(property, setup) { + this.testAdditionOrAccumulation(property, setup, 'accumulate'); + }, +} + +const types = { + color: colorType, + discrete: discreteType, + filterList: filterListType, + integer: integerType, + positiveInteger: positiveIntegerType, + length: lengthType, + percentage: percentageType, + lengthPercentageOrCalc: lengthPercentageOrCalcType, + lengthPair: lengthPairType, + positiveNumber: positiveNumberType, + opacity: opacityType, + transformList: transformListType, + rotateList: rotateListType, + translateList: translateListType, + scaleList: scaleListType, + visibility: visibilityType, + boxShadowList: boxShadowListType, + textShadowList: textShadowListType, + rect: rectType, + position: positionType, + dasharray: dasharrayType, + fontVariationSettings: fontVariationSettingsType, +}; diff --git a/testing/web-platform/tests/web-animations/animation-model/animation-types/property-utils.js b/testing/web-platform/tests/web-animations/animation-model/animation-types/property-utils.js new file mode 100644 index 0000000000..d3a7b12a61 --- /dev/null +++ b/testing/web-platform/tests/web-animations/animation-model/animation-types/property-utils.js @@ -0,0 +1,38 @@ +'use strict'; + +function runAnimationTypeTest(gCSSProperties, testType) { + for (const property in gCSSProperties) { + if (!isSupported(property)) { + continue; + } + + const setupFunction = gCSSProperties[property].setup; + for (const animationType of gCSSProperties[property].types) { + let typeObject; + let animationTypeString; + if (typeof animationType === 'string') { + typeObject = types[animationType]; + animationTypeString = animationType; + } else if (typeof animationType === 'object' && + animationType.type && typeof animationType.type === 'string') { + typeObject = types[animationType.type]; + animationTypeString = animationType.type; + } + + // First, test that the animation type object has 'testAccumulation', or + // 'testAddition', or 'testInterpolation'. + // We use test() function here so that we can continue the remainder tests + // even if this test fails. + test(t => { + assert_own_property(typeObject, testType, animationTypeString + + ` should have ${testType} property`); + assert_equals(typeof typeObject[testType], 'function', + `${testType} method should be a function`); + }, `${property} (type: ${animationTypeString}) has ${testType} function`); + + if (typeObject[testType] && typeof typeObject[testType] === 'function') { + typeObject[testType](property, setupFunction, animationType.options); + } + } + } +} diff --git a/testing/web-platform/tests/web-animations/animation-model/animation-types/visibility.html b/testing/web-platform/tests/web-animations/animation-model/animation-types/visibility.html new file mode 100644 index 0000000000..f5a60b4e2c --- /dev/null +++ b/testing/web-platform/tests/web-animations/animation-model/animation-types/visibility.html @@ -0,0 +1,57 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Animation type for the 'visibility' property</title> +<!-- FIXME: The following spec link should be updated once this definition has + been moved to CSS Values & Units. --> +<link rel="help" href="https://drafts.csswg.org/css-transitions/#animtype-visibility"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<body> +<div id="log"></div> +<div id="target"></div> +<script> +'use strict'; + +test(t => { + const div = createDiv(t); + const anim = div.animate({ visibility: ['hidden','visible'] }, + { duration: 100 * MS_PER_SEC, fill: 'both' }); + + anim.currentTime = 0; + assert_equals(getComputedStyle(div).visibility, 'hidden', + 'Visibility when progress = 0'); + + anim.currentTime = 10 * MS_PER_SEC + 1; + assert_equals(getComputedStyle(div).visibility, 'visible', + 'Visibility when progress > 0 due to linear easing'); + + anim.finish(); + assert_equals(getComputedStyle(div).visibility, 'visible', + 'Visibility when progress = 1'); + +}, 'Visibility clamping behavior'); + +test(t => { + const div = createDiv(t); + const anim = div.animate({ visibility: ['hidden', 'visible'] }, + { duration: 100 * MS_PER_SEC, fill: 'both', + easing: 'cubic-bezier(0.25, -0.6, 0, 0.5)' }); + + anim.currentTime = 0; + assert_equals(getComputedStyle(div).visibility, 'hidden', + 'Visibility when progress = 0'); + + // Timing function is below zero. So we expected visibility is hidden. + anim.currentTime = 10 * MS_PER_SEC + 1; + assert_equals(getComputedStyle(div).visibility, 'hidden', + 'Visibility when progress < 0 due to cubic-bezier easing'); + + anim.currentTime = 60 * MS_PER_SEC; + assert_equals(getComputedStyle(div).visibility, 'visible', + 'Visibility when progress > 0 due to cubic-bezier easing'); + +}, 'Visibility clamping behavior with an easing that has a negative component'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/animation-model/combining-effects/applying-interpolated-transform.html b/testing/web-platform/tests/web-animations/animation-model/combining-effects/applying-interpolated-transform.html new file mode 100644 index 0000000000..c101b5f0cf --- /dev/null +++ b/testing/web-platform/tests/web-animations/animation-model/combining-effects/applying-interpolated-transform.html @@ -0,0 +1,87 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title> apply interpolated transform on multiple keyframes</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#keyframes-section"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<body> +<div id="log"></div> +<div id="target"></div> +<script> +'use strict'; +// This test tests the correctness if animation behavior under +// box-size-denpendent and non-box-size-dependent transformation. +test(t => { + var div_1 = createDiv(t); + div_1.style.width = "100px"; + // Non-pairwise compatible transforms that are effectively no-ops with a + // matrix fallback. Both rotate(360deg) and scale(1) are identity matrix, + // making it easy to compute. + const keyframe1 = [ + {"transform":"translateX( 200px ) rotate( 360deg )"}, + {"transform":"translateX( 100% ) scale( 1 )"}, + ]; + const keyframe2 = [ + {"transform":"translateX( 200px ) rotate( 360deg )"}, + {"transform":"translateX( 100% ) scale( 1 )"}, + {"transform":"none"}, + {} + ]; + + const animation1 = div_1.animate(keyframe1, { + "duration":3000, + "easing":"linear", + }); + const animation2 = div_1.animate(keyframe2, { + "duration":3000, + "easing":"linear", + }); + // new animation on the second div, using px value instead of % as a reference + + var div_2 = createDiv(t); + div_2.style.width = "100px"; + const keyframe3 = [ + {"transform":"translateX( 200px ) rotate( 360deg )"}, + {"transform":"translateX( 100px ) scale( 1 )"}, + ]; + const keyframe4 = [ + {"transform":"translateX( 200px ) rotate( 360deg )"}, + {"transform":"translateX( 100px ) scale( 1 )"}, + {"transform":"none"}, + {} + ]; + + const animation3 = div_2.animate(keyframe1, { + "duration":3000, + "easing":"linear", + }); + const animation4 = div_2.animate(keyframe2, { + "duration":3000, + "easing":"linear", + "composite": 'replace', + }); + animation1.pause(); + animation2.pause(); + animation3.pause(); + animation4.pause(); + var i; + for (i = 0; i <= 30; i++) { + animation2.currentTime = 100 * i; + animation4.currentTime = 100 * i; + var box_size_dependent_transform = getComputedStyle(div_1)['transform']; + var reference_transform = getComputedStyle(div_2)['transform'] + var progress = i / 30; + // The second animation replaces the first animation. As the rotate and + // scale perations are effectively no-ops when the matrix fallback is + // applied. The expected behavior is to go from x-postion 200px to 0 in the + // first 2000ms and go back to x-position 200px in the last 1000ms. + var expected_transform = 'matrix(1, 0, 0, 1, $1, 0)' + .replace('$1', Math.max(200 - 300 * progress, 0) + + Math.max(0, -400 + 600 * progress)); + assert_matrix_equals(box_size_dependent_transform, reference_transform); + assert_matrix_equals(reference_transform, expected_transform); + } +}) +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/animation-model/combining-effects/applying-the-composited-result.html b/testing/web-platform/tests/web-animations/animation-model/combining-effects/applying-the-composited-result.html new file mode 100644 index 0000000000..336115e577 --- /dev/null +++ b/testing/web-platform/tests/web-animations/animation-model/combining-effects/applying-the-composited-result.html @@ -0,0 +1,29 @@ +<!doctype html> +<meta charset=utf-8> +<title>Applying the composited result</title> +<link rel="help" + href="https://drafts.csswg.org/web-animations-1/#applying-the-composited-result"> +<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); + div.style.marginLeft = '10px'; + const animation = div.animate( + { marginLeft: ['100px', '200px'] }, + 100 * MS_PER_SEC + ); + await animation.ready; + + animation.finish(); + + const marginLeft = parseFloat(getComputedStyle(div).marginLeft); + assert_equals(marginLeft, 10, 'The computed style should be reset'); +}, 'Finishing an animation that does not fill forwards causes its animation' + + ' style to be cleared'); + +</script> diff --git a/testing/web-platform/tests/web-animations/animation-model/combining-effects/effect-composition.html b/testing/web-platform/tests/web-animations/animation-model/combining-effects/effect-composition.html new file mode 100644 index 0000000000..70a8cdd92c --- /dev/null +++ b/testing/web-platform/tests/web-animations/animation-model/combining-effects/effect-composition.html @@ -0,0 +1,154 @@ +<!doctype html> +<meta charset=utf-8> +<title>Effect composition</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#effect-composition"> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src="../../testcommon.js"></script> +<div id="log"></div> +<script> +'use strict'; + +for (const composite of ['accumulate', 'add']) { + test(t => { + const div = createDiv(t); + div.style.marginLeft = '10px'; + const anim = + div.animate({ marginLeft: ['0px', '10px'], composite }, 100); + + anim.currentTime = 50; + assert_equals(getComputedStyle(div).marginLeft, '15px', + 'Animated margin-left style at 50%'); + }, `${composite} onto the base value`); + + test(t => { + const div = createDiv(t); + div.style.marginLeft = '10px'; + const anim = + div.animate([{ marginLeft: '20px', offset: 0.25 }, { marginLeft: '30px', offset: 0.75 }], + { duration: 100, composite }); + + anim.currentTime = 50; + assert_equals(getComputedStyle(div).marginLeft, '35px', + 'Animated margin-left style at 50%'); + }, `${composite} onto the base value when the interval does not include the 0 or 1 keyframe`); + + test(t => { + const div = createDiv(t); + const anims = []; + anims.push(div.animate({ marginLeft: ['10px', '20px'], + composite: 'replace' }, + 100)); + anims.push(div.animate({ marginLeft: ['0px', '10px'], + composite }, + 100)); + + for (const anim of anims) { + anim.currentTime = 50; + } + + assert_equals(getComputedStyle(div).marginLeft, '20px', + 'Animated style at 50%'); + }, `${composite} onto an underlying animation value`); + + test(t => { + const div = createDiv(t); + const anims = []; + anims.push(div.animate({ transform: 'translateX(100px)' }, { duration: 100, composite: 'replace' })); + anims.push(div.animate({ transform: 'translateY(100px)' }, { duration: 100, composite })); + + for (const anim of anims) { + anim.currentTime = 50; + } + + assert_equals(getComputedStyle(div).transform, 'matrix(1, 0, 0, 1, 50, 50)', + 'Animated style at 50%'); + }, `${composite} onto an underlying animation value with implicit from values`); + + test(t => { + const div = createDiv(t); + const anims = []; + anims.push(div.animate([{ offset: 1, transform: 'translateX(100px)' }], { duration: 100, composite: 'replace' })); + anims.push(div.animate([{ offset: 1, transform: 'translateY(100px)' }], { duration: 100, composite })); + + for (const anim of anims) { + anim.currentTime = 50; + } + + assert_equals(getComputedStyle(div).transform, 'matrix(1, 0, 0, 1, 50, 50)', + 'Animated style at 50%'); + }, `${composite} onto an underlying animation value with implicit to values`); + + test(t => { + const div = createDiv(t); + div.style.marginLeft = '10px'; + const anim = + div.animate([{ marginLeft: '10px', composite }, + { marginLeft: '30px', composite: 'replace' }], + 100); + + anim.currentTime = 50; + assert_equals(getComputedStyle(div).marginLeft, '25px', + 'Animated style at 50%'); + }, `Composite when mixing ${composite} and replace`); + + test(t => { + const div = createDiv(t); + div.style.marginLeft = '10px'; + const anim = + div.animate([{ marginLeft: '10px', composite: 'replace' }, + { marginLeft: '20px' }], + { duration: 100 , composite }); + + anim.currentTime = 50; + assert_equals(getComputedStyle(div).marginLeft, '20px', + 'Animated style at 50%'); + }, `${composite} specified on a keyframe overrides the composite mode of` + + ' the effect'); + + test(t => { + const div = createDiv(t); + div.style.marginLeft = '10px'; + const anim = + div.animate([{ marginLeft: '10px', composite: 'replace' }, + { marginLeft: '20px' }], + 100); + + anim.effect.composite = composite; + anim.currentTime = 50; // (10 + (10 + 20)) * 0.5 + assert_equals(getComputedStyle(div).marginLeft, '20px', + 'Animated style at 50%'); + }, 'unspecified composite mode on a keyframe is overriden by setting' + + ` ${composite} of the effect`); +} + +test(t => { + const div = createDiv(t); + const anims = []; + anims.push(div.animate({ marginLeft: ['10px', '20px'], + composite: 'replace' }, + 100)); + anims.push(div.animate({ marginLeft: ['0px', '10px'], + composite: 'add' }, + 100)); + // This should fully replace the previous effects. + anims.push(div.animate({ marginLeft: ['20px', '30px'], + composite: 'replace' }, + 100)); + anims.push(div.animate({ marginLeft: ['30px', '40px'], + composite: 'add' }, + 100)); + + for (const anim of anims) { + anim.currentTime = 50; + } + + // The result of applying the above effect stack is: + // underlying = 0.5 * 20 + 0.5 * 30 = 25 + // result = 0.5 * (underlying + 30px) + 0.5 * (underlying + 40px) + // = 60 + assert_equals(getComputedStyle(div).marginLeft, '60px', + 'Animated style at 50%'); +}, 'Composite replace fully replaces the underlying animation value'); + +</script> diff --git a/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/computed-keyframes-shorthands.html b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/computed-keyframes-shorthands.html new file mode 100644 index 0000000000..ff62a23bce --- /dev/null +++ b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/computed-keyframes-shorthands.html @@ -0,0 +1,30 @@ +<!doctype html> +<meta charset=utf-8> +<title>Calculating computed keyframes: Shorthand properties</title> +<link rel="help" href="https://drafts.csswg.org/web-animations-1/#calculating-computed-keyframes"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<body> +<div id="log"></div> +<div id="target"></div> +<script> +'use strict'; + +test(t => { + const div = createDiv(t); + div.style.opacity = '0'; + + const animation = div.animate({ all: 'initial' }, 100 * MS_PER_SEC); + animation.currentTime = 50 * MS_PER_SEC; + + assert_approx_equals( + parseFloat(getComputedStyle(div).opacity), + 0.5, + 0.0001, + 'Should be half way through an opacity animation' + ); +}, 'It should be possible to animate the all shorthand'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-in-removed-iframe-crash.html b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-in-removed-iframe-crash.html new file mode 100644 index 0000000000..209ede786f --- /dev/null +++ b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-in-removed-iframe-crash.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <title>Querying keyframes for an effect in a removed iframe should not crash + </title> +<link rel="help" href="https://www.w3.org/TR/web-animations-1/#dom-keyframeeffect-getkeyframes"> +</head> +<body> + <div id="target"></div> + <iframe id="iframe"></iframe> +</body> +<script type="text/javascript"> + const target = document.getElementById('target'); + const iframe = document.getElementById('iframe'); + const keyframes = [{ background: 'green' }, { background: 'blue' }, ]; + const effect = new iframe.contentWindow.KeyframeEffect(target, keyframes); + iframe.parentNode.removeChild(iframe); + const result = effect.getKeyframes(); +</script> +</html> diff --git a/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-on-marquee-parent-crash.html b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-on-marquee-parent-crash.html new file mode 100644 index 0000000000..2948d6b66e --- /dev/null +++ b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-on-marquee-parent-crash.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<title>Animating marquee's parent inside display:none</title> +<link rel="help" href="https://drafts.csswg.org/web-animations-1/"> +<link rel="help" href="https://crbug.com/1290016"> +<div id=container></div> +<script> + let outer = document.createElement('div'); + outer.style.display = 'none'; + let inner = document.createElement('div'); + let marquee = document.createElement('marquee'); + + let effect = new KeyframeEffect(inner, [{'width': '1px'}], 1); + let anim = new Animation(effect, null); + anim.pause(); + + document.body.append(outer); + outer.append(inner); + inner.append(marquee); + + // Don't crash. +</script> diff --git a/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-context-filling.html b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-context-filling.html new file mode 100644 index 0000000000..fcb7f13126 --- /dev/null +++ b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-context-filling.html @@ -0,0 +1,377 @@ +<!doctype html> +<meta charset=utf-8> +<title>The effect value of a keyframe effect: Forwards-filling animations whose + values depend on their context (target element)</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#calculating-computed-keyframes"> +<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 div = createDiv(t); + div.style.fontSize = '10px'; + const animation = div.animate( + [{ marginLeft: '10em' }, { marginLeft: '20em' }], + { duration: 1000, fill: 'forwards' } + ); + animation.finish(); + assert_equals( + getComputedStyle(div).marginLeft, + '200px', + 'Effect value before updating font-size' + ); + + div.style.fontSize = '20px'; + + assert_equals( + getComputedStyle(div).marginLeft, + '400px', + 'Effect value after updating font-size' + ); +}, 'Filling effect values reflect changes to font-size on element'); + +test(t => { + const parentDiv = createDiv(t); + const div = createDiv(t); + parentDiv.appendChild(div); + parentDiv.style.fontSize = '10px'; + + const animation = div.animate( + [{ marginLeft: '10em' }, { marginLeft: '20em' }], + { duration: 1000, fill: 'forwards' } + ); + animation.finish(); + assert_equals( + getComputedStyle(div).marginLeft, + '200px', + 'Effect value before updating font-size on parent element' + ); + + parentDiv.style.fontSize = '20px'; + + assert_equals( + getComputedStyle(div).marginLeft, + '400px', + 'Effect value after updating font-size on parent element' + ); +}, 'Filling effect values reflect changes to font-size on parent element'); + +test(t => { + const div = createDiv(t); + div.style.setProperty('--target', '100px'); + const animation = div.animate( + [{ marginLeft: '0px' }, { marginLeft: 'var(--target)' }], + { duration: 1000, fill: 'forwards' } + ); + animation.finish(); + assert_equals( + getComputedStyle(div).marginLeft, + '100px', + 'Effect value before updating variable' + ); + + div.style.setProperty('--target', '200px'); + + assert_equals( + getComputedStyle(div).marginLeft, + '200px', + 'Effect value after updating variable' + ); +}, 'Filling effect values reflect changes to variables on element'); + +test(t => { + const parentDiv = createDiv(t); + const div = createDiv(t); + parentDiv.appendChild(div); + + parentDiv.style.setProperty('--target', '10em'); + parentDiv.style.fontSize = '10px'; + + const animation = div.animate( + [{ marginLeft: '0px' }, { marginLeft: 'calc(var(--target) * 2)' }], + { duration: 1000, fill: 'forwards' } + ); + animation.finish(); + assert_equals( + getComputedStyle(div).marginLeft, + '200px', + 'Effect value before updating variable' + ); + + parentDiv.style.setProperty('--target', '20em'); + + assert_equals( + getComputedStyle(div).marginLeft, + '400px', + 'Effect value after updating variable' + ); +}, 'Filling effect values reflect changes to variables on parent element'); + +test(t => { + const div = createDiv(t); + const animation = div.animate( + [{ marginLeft: '100px' }, { marginLeft: '200px' }], + { duration: 1000, fill: 'forwards' } + ); + animation.finish(); + assert_equals( + getComputedStyle(div).marginLeft, + '200px', + 'Effect value before updating the animation' + ); + + animation.effect.setKeyframes([ + { marginLeft: '100px' }, + { marginLeft: '300px' }, + ]); + + assert_equals( + getComputedStyle(div).marginLeft, + '300px', + 'Effect value after updating the animation' + ); +}, 'Filling effect values reflect changes to the the animation\'s keyframes'); + +test(t => { + const div = createDiv(t); + div.style.marginLeft = '100px'; + const animation = div.animate( + [{ marginLeft: '100px' }, { marginLeft: '200px' }], + { duration: 1000, fill: 'forwards' } + ); + animation.finish(); + assert_equals( + getComputedStyle(div).marginLeft, + '200px', + 'Effect value before updating the animation' + ); + + animation.effect.composite = 'add'; + + assert_equals( + getComputedStyle(div).marginLeft, + '300px', + 'Effect value after updating the composite mode' + ); +}, 'Filling effect values reflect changes to the the animation\'s composite mode'); + +test(t => { + const div = createDiv(t); + const animation = div.animate( + [{ marginLeft: '0px' }, { marginLeft: '100px' }], + { duration: 1000, iterations: 2, fill: 'forwards' } + ); + animation.finish(); + assert_equals( + getComputedStyle(div).marginLeft, + '100px', + 'Effect value before updating the animation' + ); + + animation.effect.iterationComposite = 'accumulate'; + + assert_equals( + getComputedStyle(div).marginLeft, + '200px', + 'Effect value after updating the iteration composite mode' + ); +}, 'Filling effect values reflect changes to the the animation\'s iteration composite mode'); + +test(t => { + const div = createDiv(t); + div.style.marginLeft = '100px'; + const animation = div.animate( + [{ marginLeft: '100px' }, { marginLeft: '200px' }], + { duration: 1000, fill: 'forwards', composite: 'add' } + ); + animation.finish(); + assert_equals( + getComputedStyle(div).marginLeft, + '300px', + 'Effect value before updating underlying value' + ); + + div.style.marginLeft = '200px'; + + assert_equals( + getComputedStyle(div).marginLeft, + '400px', + 'Effect value after updating underlying value' + ); +}, 'Filling effect values reflect changes to the base value when using' + + ' additive animation'); + +test(t => { + const div = createDiv(t); + div.style.marginLeft = '100px'; + const animation = div.animate( + [{ marginLeft: '100px' }, { marginLeft: '200px', composite: 'add' }], + { duration: 1000, fill: 'forwards' } + ); + animation.finish(); + assert_equals( + getComputedStyle(div).marginLeft, + '300px', + 'Effect value before updating underlying value' + ); + + div.style.marginLeft = '200px'; + + assert_equals( + getComputedStyle(div).marginLeft, + '400px', + 'Effect value after updating underlying value' + ); +}, 'Filling effect values reflect changes to the base value when using' + + ' additive animation on a single keyframe'); + +test(t => { + const div = createDiv(t); + div.style.marginLeft = '0px'; + const animation = div.animate([{ marginLeft: '100px', offset: 0 }], { + duration: 1000, + fill: 'forwards', + }); + animation.finish(); + assert_equals( + getComputedStyle(div).marginLeft, + '0px', + 'Effect value before updating underlying value' + ); + + div.style.marginLeft = '200px'; + + assert_equals( + getComputedStyle(div).marginLeft, + '200px', + 'Effect value after updating underlying value' + ); +}, 'Filling effect values reflect changes to the base value when using' + + ' the fill value is an implicit keyframe'); + +test(t => { + const parentDiv = createDiv(t); + const div = createDiv(t); + parentDiv.appendChild(div); + parentDiv.style.fontSize = '10px'; + div.style.marginLeft = '10em'; + // Computed underlying margin-left is 100px + + const animation = div.animate( + [{ marginLeft: '100px' }, { marginLeft: '200px' }], + { duration: 1000, fill: 'forwards', composite: 'add' } + ); + animation.finish(); + assert_equals( + getComputedStyle(div).marginLeft, + '300px', + 'Effect value before updating font-size on parent' + ); + + parentDiv.style.fontSize = '20px'; + // Computed underlying margin-left is now 200px + + assert_equals( + getComputedStyle(div).marginLeft, + '400px', + 'Effect value after updating font-size on parent' + ); +}, 'Filling effect values reflect changes to the base value via a' + + ' parent element'); + +test(t => { + const div = createDiv(t); + const animationA = div.animate( + [{ marginLeft: '0px' }, { marginLeft: '100px' }], + { duration: 2000, fill: 'forwards', easing: 'step-end' } + ); + const animationB = div.animate( + [{ marginLeft: '100px' }, { marginLeft: '200px' }], + { duration: 1000, fill: 'forwards', composite: 'add' } + ); + animationB.finish(); + assert_equals( + getComputedStyle(div).marginLeft, + '200px', + 'Effect value before updating underyling animation' + ); + + // Go to end of the underlying animation so that it jumps to 100px + animationA.finish(); + + assert_equals( + getComputedStyle(div).marginLeft, + '300px', + 'Effect value after updating underlying animation' + ); +}, 'Filling effect values reflect changes to underlying animations'); + +test(t => { + const parentDiv = createDiv(t); + const div = createDiv(t); + parentDiv.appendChild(div); + + parentDiv.style.fontSize = '10px'; + + const animationA = div.animate( + [{ marginLeft: '0px' }, { marginLeft: '10em' }], + { duration: 2000, fill: 'forwards', easing: 'step-start' } + ); + const animationB = div.animate( + [{ marginLeft: '100px' }, { marginLeft: '200px' }], + { duration: 1000, fill: 'forwards', composite: 'add' } + ); + animationB.finish(); + assert_equals( + getComputedStyle(div).marginLeft, + '300px', + 'Effect value before updating parent font-size' + ); + + parentDiv.style.fontSize = '20px'; + // At this point the underlying animation's output should jump to 200px. + + assert_equals( + getComputedStyle(div).marginLeft, + '400px', + 'Effect value after updating parent font-size' + ); +}, 'Filling effect values reflect changes to underlying animations via a' + + ' a parent element'); + +test(t => { + const div = createDiv(t); + const animationA = div.animate( + [{ marginLeft: '0px' }, { marginLeft: '0px' }], + { duration: 2000, fill: 'forwards' } + ); + const animationB = div.animate( + [{ marginLeft: '100px' }, { marginLeft: '200px' }], + { duration: 1000, fill: 'forwards', composite: 'add' } + ); + animationB.finish(); + assert_equals( + getComputedStyle(div).marginLeft, + '200px', + 'Effect value before updating underyling animation' + ); + + animationA.effect.setKeyframes([ + { marginLeft: '100px' }, + { marginLeft: '100px' }, + ]); + + assert_equals( + getComputedStyle(div).marginLeft, + '300px', + 'Effect value after updating underlying animation' + ); +}, 'Filling effect values reflect changes to underlying animations made by' + + ' directly changing the keyframes'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-context.html b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-context.html new file mode 100644 index 0000000000..3730a02098 --- /dev/null +++ b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-context.html @@ -0,0 +1,105 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>The effect value of a keyframe effect: Property values that depend on + their context (target element)</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#calculating-computed-keyframes"> +<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 div = createDiv(t); + div.style.fontSize = '10px'; + const animation = div.animate([ { marginLeft: '10em' }, + { marginLeft: '20em' } ], 1000); + animation.currentTime = 500; + assert_equals(getComputedStyle(div).marginLeft, '150px', + 'Effect value before updating font-size'); + div.style.fontSize = '20px'; + assert_equals(getComputedStyle(div).marginLeft, '300px', + 'Effect value after updating font-size'); +}, 'Effect values reflect changes to font-size on element'); + +test(t => { + const parentDiv = createDiv(t); + const div = createDiv(t); + parentDiv.appendChild(div); + parentDiv.style.fontSize = '10px'; + + const animation = div.animate([ { marginLeft: '10em' }, + { marginLeft: '20em' } ], 1000); + animation.currentTime = 500; + assert_equals(getComputedStyle(div).marginLeft, '150px', + 'Effect value before updating font-size on parent element'); + parentDiv.style.fontSize = '20px'; + assert_equals(getComputedStyle(div).marginLeft, '300px', + 'Effect value after updating font-size on parent element'); +}, 'Effect values reflect changes to font-size on parent element'); + +promise_test(t => { + const parentDiv = createDiv(t); + const div = createDiv(t); + parentDiv.appendChild(div); + parentDiv.style.fontSize = '10px'; + const animation = div.animate([ { marginLeft: '10em' }, + { marginLeft: '20em' } ], 1000); + + animation.pause(); + animation.currentTime = 500; + parentDiv.style.fontSize = '20px'; + + return animation.ready.then(() => { + assert_equals(getComputedStyle(div).marginLeft, '300px', + 'Effect value after updating font-size on parent element'); + }); +}, 'Effect values reflect changes to font-size when computed style is not' + + ' immediately flushed'); + +promise_test(t => { + const divWith10pxFontSize = createDiv(t); + divWith10pxFontSize.style.fontSize = '10px'; + const divWith20pxFontSize = createDiv(t); + divWith20pxFontSize.style.fontSize = '20px'; + + const div = createDiv(t); + div.remove(); // Detach + const animation = div.animate([ { marginLeft: '10em' }, + { marginLeft: '20em' } ], 1000); + animation.pause(); + + return animation.ready.then(() => { + animation.currentTime = 500; + + divWith10pxFontSize.appendChild(div); + assert_equals(getComputedStyle(div).marginLeft, '150px', + 'Effect value after attaching to font-size:10px parent'); + divWith20pxFontSize.appendChild(div); + assert_equals(getComputedStyle(div).marginLeft, '300px', + 'Effect value after attaching to font-size:20px parent'); + }); +}, 'Effect values reflect changes to font-size from reparenting'); + +test(t => { + const divA = createDiv(t); + divA.style.fontSize = '10px'; + + const divB = createDiv(t); + divB.style.fontSize = '20px'; + + const animation = divA.animate([ { marginLeft: '10em' }, + { marginLeft: '20em' } ], 1000); + animation.currentTime = 500; + assert_equals(getComputedStyle(divA).marginLeft, '150px', + 'Effect value before updating target element'); + + animation.effect.target = divB; + assert_equals(getComputedStyle(divB).marginLeft, '300px', + 'Effect value after updating target element'); +}, 'Effect values reflect changes to target element'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-interval-distance.html b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-interval-distance.html new file mode 100644 index 0000000000..6bf5d8cd46 --- /dev/null +++ b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-interval-distance.html @@ -0,0 +1,36 @@ +<!doctype html> +<meta charset=utf-8> +<title>The effect value of a keyframe effect: Calculating the interval + distance between keyframes</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#the-effect-value-of-a-keyframe-animation-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'; + +test(t => { + // In Firefox there was a floating precision bug in the calculation of the + // progress at the end of the 0.2<->1.0 interval. This test exercises that + // calculation in case other UAs suffer from the same problem. + const target = createDiv(t); + const anim = target.animate( + [ + { opacity: 0 }, + { offset: 0.2, opacity: 1, easing: 'step-end' }, + { opacity: 0 }, + ], + { + duration: 1000, + fill: 'forwards', + } + ); + + anim.currentTime = 1000; + assert_equals(getComputedStyle(target).opacity, '0'); +}, 'Interval distance is calculated correctly (precision test)'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-iteration-composite-operation.html b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-iteration-composite-operation.html new file mode 100644 index 0000000000..abfda86f4d --- /dev/null +++ b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-iteration-composite-operation.html @@ -0,0 +1,824 @@ +<!doctype html> +<meta charset=utf-8> +<title>The effect value of a keyframe effect: Applying the iteration composite + operation</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#the-effect-value-of-a-keyframe-animation-effect"> +<link rel="help" href="https://drafts.csswg.org/web-animations/#effect-accumulation-section"> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src="../../testcommon.js"></script> +<div id="log"></div> +<script> +'use strict'; + +test(t => { + const div = createDiv(t); + const anim = + div.animate({ alignContent: ['flex-start', 'flex-end'] }, + { duration: 100 * MS_PER_SEC, + easing: 'linear', + iterations: 10, + iterationComposite: 'accumulate' }); + + anim.currentTime = anim.effect.getComputedTiming().duration / 2; + assert_equals(getComputedStyle(div).alignContent, 'flex-end', + 'Animated align-content style at 50s of the first iteration'); + anim.currentTime = anim.effect.getComputedTiming().duration * 2; + assert_equals(getComputedStyle(div).alignContent, 'flex-start', + 'Animated align-content style at 0s of the third iteration'); + anim.currentTime += anim.effect.getComputedTiming().duration / 2; + assert_equals(getComputedStyle(div).alignContent, 'flex-end', + 'Animated align-content style at 50s of the third iteration'); +}, 'iteration composition of discrete type animation (align-content)'); + +test(t => { + const div = createDiv(t); + const anim = + div.animate({ marginLeft: ['0px', '10px'] }, + { duration: 100 * MS_PER_SEC, + easing: 'linear', + iterations: 10, + iterationComposite: 'accumulate' }); + anim.pause(); + + anim.currentTime = anim.effect.getComputedTiming().duration / 2; + assert_equals(getComputedStyle(div).marginLeft, '5px', + 'Animated margin-left style at 50s of the first iteration'); + anim.currentTime = anim.effect.getComputedTiming().duration * 2; + assert_equals(getComputedStyle(div).marginLeft, '20px', + 'Animated margin-left style at 0s of the third iteration'); + anim.currentTime += anim.effect.getComputedTiming().duration / 2; + assert_equals(getComputedStyle(div).marginLeft, '25px', + 'Animated margin-left style at 50s of the third iteration'); +}, 'iteration composition of <length> type animation'); + +test(t => { + const parent = createDiv(t); + parent.style.width = '100px'; + const div = createDiv(t); + parent.appendChild(div); + + const anim = + div.animate({ width: ['0%', '50%'] }, + { duration: 100 * MS_PER_SEC, + easing: 'linear', + iterations: 10, + iterationComposite: 'accumulate' }); + anim.pause(); + + anim.currentTime = anim.effect.getComputedTiming().duration / 2; + assert_equals(getComputedStyle(div).width, '25px', + 'Animated width style at 50s of the first iteration'); + anim.currentTime = anim.effect.getComputedTiming().duration * 2; + assert_equals(getComputedStyle(div).width, '100px', + 'Animated width style at 0s of the third iteration'); + anim.currentTime += anim.effect.getComputedTiming().duration / 2; + assert_equals(getComputedStyle(div).width, '125px', + 'Animated width style at 50s of the third iteration'); +}, 'iteration composition of <percentage> type animation'); + +test(t => { + const div = createDiv(t); + const anim = + div.animate({ color: ['rgb(0, 0, 0)', 'rgb(120, 120, 120)'] }, + { duration: 100 * MS_PER_SEC, + easing: 'linear', + iterations: 10, + iterationComposite: 'accumulate' }); + anim.pause(); + + anim.currentTime = anim.effect.getComputedTiming().duration / 2; + assert_equals(getComputedStyle(div).color, 'rgb(60, 60, 60)', + 'Animated color style at 50s of the first iteration'); + anim.currentTime = anim.effect.getComputedTiming().duration * 2; + assert_equals(getComputedStyle(div).color, 'rgb(240, 240, 240)', + 'Animated color style at 0s of the third iteration'); + anim.currentTime += anim.effect.getComputedTiming().duration / 2; + assert_equals(getComputedStyle(div).color, 'rgb(255, 255, 255)', + 'Animated color style at 50s of the third iteration'); +}, 'iteration composition of <color> type animation'); + +test(t => { + const div = createDiv(t); + const anim = + div.animate({ color: ['rgb(0, 120, 0)', 'rgb(60, 60, 60)'] }, + { duration: 100 * MS_PER_SEC, + easing: 'linear', + iterations: 10, + iterationComposite: 'accumulate' }); + anim.pause(); + + anim.currentTime = anim.effect.getComputedTiming().duration / 2; + assert_equals(getComputedStyle(div).color, 'rgb(30, 90, 30)', + 'Animated color style at 50s of the first iteration'); + anim.currentTime = anim.effect.getComputedTiming().duration * 2; + assert_equals(getComputedStyle(div).color, 'rgb(120, 240, 120)', + 'Animated color style at 0s of the third iteration'); + anim.currentTime += anim.effect.getComputedTiming().duration / 2; + // The green color is (240 + 180) / 2 = 210 + assert_equals(getComputedStyle(div).color, 'rgb(150, 210, 150)', + 'Animated color style at 50s of the third iteration'); +}, 'iteration composition of <color> type animation that green component is ' + + 'decreasing'); + + test(t => { + const div = createDiv(t); + const anim = + div.animate({ flexGrow: [0, 10] }, + { duration: 100 * MS_PER_SEC, + easing: 'linear', + iterations: 10, + iterationComposite: 'accumulate' }); + anim.pause(); + + anim.currentTime = anim.effect.getComputedTiming().duration / 2; + assert_equals(getComputedStyle(div).flexGrow, '5', + 'Animated flex-grow style at 50s of the first iteration'); + anim.currentTime = anim.effect.getComputedTiming().duration * 2; + assert_equals(getComputedStyle(div).flexGrow, '20', + 'Animated flex-grow style at 0s of the third iteration'); + anim.currentTime += anim.effect.getComputedTiming().duration / 2; + assert_equals(getComputedStyle(div).flexGrow, '25', + 'Animated flex-grow style at 50s of the third iteration'); +}, 'iteration composition of <number> type animation'); + +test(t => { + const div = createDiv(t); + div.style.position = 'absolute'; + const anim = + div.animate({ clip: ['rect(0px, 0px, 0px, 0px)', + 'rect(10px, 10px, 10px, 10px)'] }, + { duration: 100 * MS_PER_SEC, + easing: 'linear', + iterations: 10, + iterationComposite: 'accumulate' }); + anim.pause(); + + anim.currentTime = anim.effect.getComputedTiming().duration / 2; + assert_equals(getComputedStyle(div).clip, 'rect(5px, 5px, 5px, 5px)', + 'Animated clip style at 50s of the first iteration'); + anim.currentTime = anim.effect.getComputedTiming().duration * 2; + assert_equals(getComputedStyle(div).clip, 'rect(20px, 20px, 20px, 20px)', + 'Animated clip style at 0s of the third iteration'); + anim.currentTime += anim.effect.getComputedTiming().duration / 2; + assert_equals(getComputedStyle(div).clip, 'rect(25px, 25px, 25px, 25px)', + 'Animated clip style at 50s of the third iteration'); +}, 'iteration composition of <shape> type animation'); + +test(t => { + const div = createDiv(t); + const anim = + div.animate({ width: ['calc(0vw + 0px)', 'calc(0vw + 10px)'] }, + { duration: 100 * MS_PER_SEC, + easing: 'linear', + iterations: 10, + iterationComposite: 'accumulate' }); + anim.pause(); + + anim.currentTime = anim.effect.getComputedTiming().duration / 2; + assert_equals(getComputedStyle(div).width, '5px', + 'Animated calc width style at 50s of the first iteration'); + anim.currentTime = anim.effect.getComputedTiming().duration * 2; + assert_equals(getComputedStyle(div).width, '20px', + 'Animated calc width style at 0s of the third iteration'); + anim.currentTime += anim.effect.getComputedTiming().duration / 2; + assert_equals(getComputedStyle(div).width, '25px', + 'Animated calc width style at 50s of the third iteration'); +}, 'iteration composition of <calc()> value animation'); + +test(t => { + const parent = createDiv(t); + parent.style.width = '100px'; + const div = createDiv(t); + parent.appendChild(div); + + const anim = + div.animate({ width: ['calc(0% + 0px)', 'calc(10% + 10px)'] }, + { duration: 100 * MS_PER_SEC, + easing: 'linear', + iterations: 10, + iterationComposite: 'accumulate' }); + anim.pause(); + + anim.currentTime = anim.effect.getComputedTiming().duration / 2; + assert_equals(getComputedStyle(div).width, '10px', + // 100px * 5% + 5px + 'Animated calc width style at 50s of the first iteration'); + anim.currentTime = anim.effect.getComputedTiming().duration * 2; + assert_equals(getComputedStyle(div).width, + '40px', // 100px * (10% + 10%) + (10px + 10px) + 'Animated calc width style at 0s of the third iteration'); + anim.currentTime += anim.effect.getComputedTiming().duration / 2; + assert_equals(getComputedStyle(div).width, + '50px', // (40px + 60px) / 2 + 'Animated calc width style at 50s of the third iteration'); +}, 'iteration composition of <calc()> value animation that the values can\'t' + + 'be reduced'); + +test(t => { + const div = createDiv(t); + const anim = + div.animate({ opacity: [0, 0.4] }, + { duration: 100 * MS_PER_SEC, + easing: 'linear', + iterations: 10, + iterationComposite: 'accumulate' }); + anim.pause(); + + anim.currentTime = anim.effect.getComputedTiming().duration / 2; + assert_equals(getComputedStyle(div).opacity, '0.2', + 'Animated opacity style at 50s of the first iteration'); + anim.currentTime = anim.effect.getComputedTiming().duration * 2; + assert_equals(getComputedStyle(div).opacity, '0.8', + 'Animated opacity style at 0s of the third iteration'); + anim.currentTime += anim.effect.getComputedTiming().duration / 2; + assert_equals(getComputedStyle(div).opacity, '1', // (0.8 + 1.2) * 0.5 + 'Animated opacity style at 50s of the third iteration'); +}, 'iteration composition of opacity animation'); + +test(t => { + const div = createDiv(t); + const anim = + div.animate({ boxShadow: ['rgb(0, 0, 0) 0px 0px 0px 0px', + 'rgb(120, 120, 120) 10px 10px 10px 0px'] }, + { duration: 100 * MS_PER_SEC, + easing: 'linear', + iterations: 10, + iterationComposite: 'accumulate' }); + anim.pause(); + + anim.currentTime = anim.effect.getComputedTiming().duration / 2; + assert_equals(getComputedStyle(div).boxShadow, + 'rgb(60, 60, 60) 5px 5px 5px 0px', + 'Animated box-shadow style at 50s of the first iteration'); + anim.currentTime = anim.effect.getComputedTiming().duration * 2; + assert_equals(getComputedStyle(div).boxShadow, + 'rgb(240, 240, 240) 20px 20px 20px 0px', + 'Animated box-shadow style at 0s of the third iteration'); + anim.currentTime += anim.effect.getComputedTiming().duration / 2; + assert_equals(getComputedStyle(div).boxShadow, + 'rgb(255, 255, 255) 25px 25px 25px 0px', + 'Animated box-shadow style at 50s of the third iteration'); +}, 'iteration composition of box-shadow animation'); + +test(t => { + const div = createDiv(t); + const anim = + div.animate({ filter: ['blur(0px)', 'blur(10px)'] }, + { duration: 100 * MS_PER_SEC, + easing: 'linear', + iterations: 10, + iterationComposite: 'accumulate' }); + anim.pause(); + + anim.currentTime = anim.effect.getComputedTiming().duration / 2; + assert_equals(getComputedStyle(div).filter, 'blur(5px)', + 'Animated filter blur style at 50s of the first iteration'); + anim.currentTime = anim.effect.getComputedTiming().duration * 2; + assert_equals(getComputedStyle(div).filter, 'blur(20px)', + 'Animated filter blur style at 0s of the third iteration'); + anim.currentTime += anim.effect.getComputedTiming().duration / 2; + assert_equals(getComputedStyle(div).filter, 'blur(25px)', + 'Animated filter blur style at 50s of the third iteration'); +}, 'iteration composition of filter blur animation'); + +test(t => { + const div = createDiv(t); + const anim = + div.animate({ filter: ['brightness(1)', + 'brightness(180%)'] }, + { duration: 100 * MS_PER_SEC, + easing: 'linear', + iterations: 10, + iterationComposite: 'accumulate' }); + anim.pause(); + + anim.currentTime = anim.effect.getComputedTiming().duration / 2; + assert_equals(getComputedStyle(div).filter, + 'brightness(1.4)', + 'Animated filter brightness style at 50s of the first iteration'); + anim.currentTime = anim.effect.getComputedTiming().duration * 2; + assert_equals(getComputedStyle(div).filter, + 'brightness(2.6)', // brightness(1) + brightness(0.8) + brightness(0.8) + 'Animated filter brightness style at 0s of the third iteration'); + anim.currentTime += anim.effect.getComputedTiming().duration / 2; + assert_equals(getComputedStyle(div).filter, + 'brightness(3)', // (brightness(2.6) + brightness(3.4)) * 0.5 + 'Animated filter brightness style at 50s of the third iteration'); +}, 'iteration composition of filter brightness for different unit animation'); + +test(t => { + const div = createDiv(t); + const anim = + div.animate({ filter: ['brightness(0)', + 'brightness(1)'] }, + { duration: 100 * MS_PER_SEC, + easing: 'linear', + iterations: 10, + iterationComposite: 'accumulate' }); + anim.pause(); + + anim.currentTime = anim.effect.getComputedTiming().duration / 2; + assert_equals(getComputedStyle(div).filter, + 'brightness(0.5)', + 'Animated filter brightness style at 50s of the first iteration'); + anim.currentTime = anim.effect.getComputedTiming().duration * 2; + assert_equals(getComputedStyle(div).filter, + 'brightness(0)', // brightness(1) is an identity element, not accumulated. + 'Animated filter brightness style at 0s of the third iteration'); + anim.currentTime += anim.effect.getComputedTiming().duration / 2; + assert_equals(getComputedStyle(div).filter, + 'brightness(0.5)', // brightness(1) is an identity element, not accumulated. + 'Animated filter brightness style at 50s of the third iteration'); +}, 'iteration composition of filter brightness animation'); + +test(t => { + const div = createDiv(t); + const anim = + div.animate({ filter: ['drop-shadow(rgb(0, 0, 0) 0px 0px 0px)', + 'drop-shadow(rgb(120, 120, 120) 10px 10px 10px)'] }, + { duration: 100 * MS_PER_SEC, + easing: 'linear', + iterations: 10, + iterationComposite: 'accumulate' }); + anim.pause(); + + anim.currentTime = anim.effect.getComputedTiming().duration / 2; + assert_equals(getComputedStyle(div).filter, + 'drop-shadow(rgb(60, 60, 60) 5px 5px 5px)', + 'Animated filter drop-shadow style at 50s of the first iteration'); + anim.currentTime = anim.effect.getComputedTiming().duration * 2; + assert_equals(getComputedStyle(div).filter, + 'drop-shadow(rgb(240, 240, 240) 20px 20px 20px)', + 'Animated filter drop-shadow style at 0s of the third iteration'); + anim.currentTime += anim.effect.getComputedTiming().duration / 2; + assert_equals(getComputedStyle(div).filter, + 'drop-shadow(rgb(255, 255, 255) 25px 25px 25px)', + 'Animated filter drop-shadow style at 50s of the third iteration'); +}, 'iteration composition of filter drop-shadow animation'); + +test(t => { + const div = createDiv(t); + const anim = + div.animate({ filter: ['brightness(1) contrast(1)', + 'brightness(2) contrast(2)'] }, + { duration: 100 * MS_PER_SEC, + easing: 'linear', + iterations: 10, + iterationComposite: 'accumulate' }); + anim.pause(); + + anim.currentTime = anim.effect.getComputedTiming().duration / 2; + assert_equals(getComputedStyle(div).filter, + 'brightness(1.5) contrast(1.5)', + 'Animated filter list at 50s of the first iteration'); + anim.currentTime = anim.effect.getComputedTiming().duration * 2; + assert_equals(getComputedStyle(div).filter, + 'brightness(3) contrast(3)', + 'Animated filter list at 0s of the third iteration'); + anim.currentTime += anim.effect.getComputedTiming().duration / 2; + assert_equals(getComputedStyle(div).filter, + 'brightness(3.5) contrast(3.5)', + 'Animated filter list at 50s of the third iteration'); +}, 'iteration composition of same filter list animation'); + +test(t => { + const div = createDiv(t); + const anim = + div.animate({ filter: ['brightness(1) contrast(1)', + 'contrast(2) brightness(2)'] }, + { duration: 100 * MS_PER_SEC, + easing: 'linear', + iterations: 10, + iterationComposite: 'accumulate' }); + anim.pause(); + + anim.currentTime = anim.effect.getComputedTiming().duration / 2; + assert_equals(getComputedStyle(div).filter, + 'contrast(2) brightness(2)', // discrete + 'Animated filter list at 50s of the first iteration'); + anim.currentTime = anim.effect.getComputedTiming().duration * 2; + assert_equals(getComputedStyle(div).filter, + // We can't accumulate 'contrast(2) brightness(2)' onto + // the first list 'brightness(1) contrast(1)' because of + // mismatch of the order. + 'brightness(1) contrast(1)', + 'Animated filter list at 0s of the third iteration'); + anim.currentTime += anim.effect.getComputedTiming().duration / 2; + assert_equals(getComputedStyle(div).filter, + // We *can* accumulate 'contrast(2) brightness(2)' onto + // the same list 'contrast(2) brightness(2)' here. + 'contrast(4) brightness(4)', // discrete + 'Animated filter list at 50s of the third iteration'); +}, 'iteration composition of discrete filter list because of mismatch ' + + 'of the order'); + + test(t => { + const div = createDiv(t); + const anim = + div.animate({ filter: ['sepia(0)', + 'sepia(1) contrast(2)'] }, + { duration: 100 * MS_PER_SEC, + easing: 'linear', + iterations: 10, + iterationComposite: 'accumulate' }); + anim.pause(); + + anim.currentTime = anim.effect.getComputedTiming().duration / 2; + assert_equals(getComputedStyle(div).filter, + 'sepia(0.5) contrast(1.5)', + 'Animated filter list at 50s of the first iteration'); + anim.currentTime = anim.effect.getComputedTiming().duration * 2; + assert_equals(getComputedStyle(div).filter, + 'sepia(1) contrast(3)', + 'Animated filter list at 0s of the third iteration'); + anim.currentTime += anim.effect.getComputedTiming().duration / 2; + assert_equals(getComputedStyle(div).filter, + 'sepia(1) contrast(3.5)', + 'Animated filter list at 50s of the third iteration'); +}, 'iteration composition of different length filter list animation'); + +test(t => { + const div = createDiv(t); + const anim = + div.animate({ transform: ['rotate(0deg)', 'rotate(180deg)'] }, + { duration: 100 * MS_PER_SEC, + easing: 'linear', + iterations: 10, + iterationComposite: 'accumulate' }); + anim.pause(); + + anim.currentTime = anim.effect.getComputedTiming().duration / 2; + assert_matrix_equals(getComputedStyle(div).transform, + 'matrix(0, 1, -1, 0, 0, 0)', // rotate(90deg) + 'Animated transform(rotate) style at 50s of the first iteration'); + anim.currentTime = anim.effect.getComputedTiming().duration * 2; + assert_matrix_equals(getComputedStyle(div).transform, + 'matrix(1, 0, 0, 1, 0, 0)', // rotate(360deg) + 'Animated transform(rotate) style at 0s of the third iteration'); + anim.currentTime += anim.effect.getComputedTiming().duration / 2; + assert_matrix_equals(getComputedStyle(div).transform, + 'matrix(0, 1, -1, 0, 0, 0)', // rotate(450deg) + 'Animated transform(rotate) style at 50s of the third iteration'); +}, 'iteration composition of transform(rotate) animation'); + +test(t => { + const div = createDiv(t); + const anim = + div.animate({ transform: ['scale(0)', 'scale(1)'] }, + { duration: 100 * MS_PER_SEC, + easing: 'linear', + iterations: 10, + iterationComposite: 'accumulate' }); + anim.pause(); + + anim.currentTime = anim.effect.getComputedTiming().duration / 2; + assert_matrix_equals(getComputedStyle(div).transform, + 'matrix(0.5, 0, 0, 0.5, 0, 0)', // scale(0.5) + 'Animated transform(scale) style at 50s of the first iteration'); + anim.currentTime = anim.effect.getComputedTiming().duration * 2; + assert_matrix_equals(getComputedStyle(div).transform, + 'matrix(0, 0, 0, 0, 0, 0)', // scale(0); scale(1) is an identity element, + // not accumulated. + 'Animated transform(scale) style at 0s of the third iteration'); + anim.currentTime += anim.effect.getComputedTiming().duration / 2; + assert_matrix_equals(getComputedStyle(div).transform, + 'matrix(0.5, 0, 0, 0.5, 0, 0)', // scale(0.5); scale(1) an identity + // element, not accumulated. + 'Animated transform(scale) style at 50s of the third iteration'); +}, 'iteration composition of transform: [ scale(0), scale(1) ] animation'); + +test(t => { + const div = createDiv(t); + const anim = + div.animate({ transform: ['scale(1)', 'scale(2)'] }, + { duration: 100 * MS_PER_SEC, + easing: 'linear', + iterations: 10, + iterationComposite: 'accumulate' }); + anim.pause(); + + anim.currentTime = anim.effect.getComputedTiming().duration / 2; + assert_matrix_equals(getComputedStyle(div).transform, + 'matrix(1.5, 0, 0, 1.5, 0, 0)', // scale(1.5) + 'Animated transform(scale) style at 50s of the first iteration'); + anim.currentTime = anim.effect.getComputedTiming().duration * 2; + assert_matrix_equals(getComputedStyle(div).transform, + 'matrix(3, 0, 0, 3, 0, 0)', // scale(1 + (2 -1) + (2 -1)) + 'Animated transform(scale) style at 0s of the third iteration'); + anim.currentTime += anim.effect.getComputedTiming().duration / 2; + assert_matrix_equals(getComputedStyle(div).transform, + 'matrix(3.5, 0, 0, 3.5, 0, 0)', // (scale(3) + scale(4)) * 0.5 + 'Animated transform(scale) style at 50s of the third iteration'); +}, 'iteration composition of transform: [ scale(1), scale(2) ] animation'); + +test(t => { + const div = createDiv(t); + const anim = + div.animate({ transform: ['scale(0)', 'scale(2)'] }, + { duration: 100 * MS_PER_SEC, + easing: 'linear', + iterations: 10, + iterationComposite: 'accumulate' }); + anim.pause(); + + anim.currentTime = anim.effect.getComputedTiming().duration / 2; + assert_matrix_equals(getComputedStyle(div).transform, + 'matrix(1, 0, 0, 1, 0, 0)', // scale(1) + 'Animated transform(scale) style at 50s of the first iteration'); + anim.currentTime = anim.effect.getComputedTiming().duration * 2; + assert_matrix_equals(getComputedStyle(div).transform, + 'matrix(2, 0, 0, 2, 0, 0)', // (scale(0) + scale(2-1)*2) + 'Animated transform(scale) style at 0s of the third iteration'); + anim.currentTime += anim.effect.getComputedTiming().duration / 2; + assert_matrix_equals(getComputedStyle(div).transform, + 'matrix(3, 0, 0, 3, 0, 0)', // (scale(2) + scale(4)) * 0.5 + 'Animated transform(scale) style at 50s of the third iteration'); +}, 'iteration composition of transform: scale(2) animation'); + +test(t => { + const div = createDiv(t); + const anim = + div.animate({ transform: ['rotate(0deg) translateX(0px)', + 'rotate(180deg) translateX(10px)'] }, + { duration: 100 * MS_PER_SEC, + easing: 'linear', + iterations: 10, + iterationComposite: 'accumulate' }); + anim.pause(); + + anim.currentTime = anim.effect.getComputedTiming().duration / 2; + assert_matrix_equals(getComputedStyle(div).transform, + 'matrix(0, 1, -1, 0, 0, 5)', // rotate(90deg) translateX(5px) + 'Animated transform list at 50s of the first iteration'); + anim.currentTime = anim.effect.getComputedTiming().duration * 2; + assert_matrix_equals(getComputedStyle(div).transform, + 'matrix(1, 0, 0, 1, 20, 0)', // rotate(360deg) translateX(20px) + 'Animated transform list at 0s of the third iteration'); + anim.currentTime += anim.effect.getComputedTiming().duration / 2; + assert_matrix_equals(getComputedStyle(div).transform, + 'matrix(0, 1, -1, 0, 0, 25)', // rotate(450deg) translateX(25px) + 'Animated transform list at 50s of the third iteration'); +}, 'iteration composition of transform list animation'); + +test(t => { + const div = createDiv(t); + const anim = + div.animate({ transform: ['matrix(2, 0, 0, 2, 0, 0)', + 'matrix(3, 0, 0, 3, 30, 0)'] }, + { duration: 100 * MS_PER_SEC, + easing: 'linear', + iterations: 10, + iterationComposite: 'accumulate' }); + anim.pause(); + + anim.currentTime = anim.effect.getComputedTiming().duration / 2; + assert_matrix_equals(getComputedStyle(div).transform, + 'matrix(2.5, 0, 0, 2.5, 15, 0)', + 'Animated transform of matrix function at 50s of the first iteration'); + anim.currentTime = anim.effect.getComputedTiming().duration * 2; + assert_matrix_equals(getComputedStyle(div).transform, + // scale(2) + (scale(3-1)*2) + translateX(30px)*2 + 'matrix(6, 0, 0, 6, 60, 0)', + 'Animated transform of matrix function at 0s of the third iteration'); + anim.currentTime += anim.effect.getComputedTiming().duration / 2; + assert_matrix_equals(getComputedStyle(div).transform, + // from: matrix(6, 0, 0, 6, 60, 0) + // to: matrix(7, 0, 0, 7, 90, 0) + // = scale(3) + (scale(3-1)*2) + translateX(30px)*3 + 'matrix(6.5, 0, 0, 6.5, 75, 0)', + 'Animated transform of matrix function at 50s of the third iteration'); +}, 'iteration composition of transform of matrix function'); + +test(t => { + const div = createDiv(t); + const anim = + div.animate({ transform: ['translateX(0px) scale(2)', + 'scale(3) translateX(10px)'] }, + { duration: 100 * MS_PER_SEC, + easing: 'linear', + iterations: 10, + iterationComposite: 'accumulate' }); + anim.pause(); + + anim.currentTime = anim.effect.getComputedTiming().duration / 2; + assert_matrix_equals(getComputedStyle(div).transform, + // Interpolate between matrix(2, 0, 0, 2, 0, 0) = translateX(0px) scale(2) + // and matrix(3, 0, 0, 3, 30, 0) = scale(3) translateX(10px) + 'matrix(2.5, 0, 0, 2.5, 15, 0)', + 'Animated transform list at 50s of the first iteration'); + anim.currentTime = anim.effect.getComputedTiming().duration * 2; + assert_matrix_equals(getComputedStyle(div).transform, + // 'from' and 'to' value are mismatched, so accumulate + // matrix(2, 0, 0, 2, 0, 0) onto matrix(3, 0, 0, 3, 30, 0) * 2 + // = scale(2) + (scale(3-1)*2) + translateX(30px)*2 + 'matrix(6, 0, 0, 6, 60, 0)', + 'Animated transform list at 0s of the third iteration'); + anim.currentTime += anim.effect.getComputedTiming().duration / 2; + assert_matrix_equals(getComputedStyle(div).transform, + // Interpolate between matrix(6, 0, 0, 6, 60, 0) + // and matrix(7, 0, 0, 7, 210, 0) = scale(7) translate(30px) + 'matrix(6.5, 0, 0, 6.5, 135, 0)', + 'Animated transform list at 50s of the third iteration'); +}, 'iteration composition of transform list animation whose order is' + + ' mismatched'); + +test(t => { + const div = createDiv(t); + // Even if each transform list does not have functions which exist in + // other pair of the list, we don't fill any missing functions at all. + const anim = + div.animate({ transform: ['translateX(0px)', + 'scale(2) translateX(10px)'] }, + { duration: 100 * MS_PER_SEC, + easing: 'linear', + iterations: 10, + iterationComposite: 'accumulate' }); + anim.pause(); + + anim.currentTime = anim.effect.getComputedTiming().duration / 2; + assert_matrix_equals(getComputedStyle(div).transform, + // Interpolate between matrix(1, 0, 0, 1, 0, 0) = translateX(0px) + // and matrix(2, 0, 0, 2, 20, 0) = scale(2) translateX(10px) + 'matrix(1.5, 0, 0, 1.5, 10, 0)', + 'Animated transform list at 50s of the first iteration'); + anim.currentTime = anim.effect.getComputedTiming().duration * 2; + assert_matrix_equals(getComputedStyle(div).transform, + // 'from' and 'to' value are mismatched, so accumulate + // matrix(1, 0, 0, 1, 0, 0) onto matrix(2, 0, 0, 2, 20, 0) * 2 + // = scale(1) + (scale(2-1)*2) + translateX(20px)*2 + 'matrix(3, 0, 0, 3, 40, 0)', + 'Animated transform list at 0s of the third iteration'); + anim.currentTime += anim.effect.getComputedTiming().duration / 2; + assert_matrix_equals(getComputedStyle(div).transform, + // Interpolate between matrix(3, 0, 0, 3, 40, 0) + // and matrix(4, 0, 0, 4, 120, 0) = + // scale(2 + (2-1)*2) translate(10px * 3) + 'matrix(3.5, 0, 0, 3.5, 80, 0)', + 'Animated transform list at 50s of the third iteration'); +}, 'iteration composition of transform list animation whose order is' + + ' mismatched because of missing functions'); + + test(t => { + const div = createDiv(t); + const anim = + div.animate({ transform: ['none', + 'translateX(10px)'] }, + { duration: 100 * MS_PER_SEC, + easing: 'linear', + iterations: 10, + iterationComposite: 'accumulate' }); + anim.pause(); + + anim.currentTime = anim.effect.getComputedTiming().duration / 2; + assert_matrix_equals(getComputedStyle(div).transform, + // translateX(none) -> translateX(10px) @ 50% + 'matrix(1, 0, 0, 1, 5, 0)', + 'Animated transform list at 50s of the first iteration'); + anim.currentTime = anim.effect.getComputedTiming().duration * 2; + assert_matrix_equals(getComputedStyle(div).transform, + // translateX(10px * 2 + none) -> translateX(10px * 2 + 10px) @ 0% + 'matrix(1, 0, 0, 1, 20, 0)', + 'Animated transform list at 0s of the third iteration'); + anim.currentTime += anim.effect.getComputedTiming().duration / 2; + assert_matrix_equals(getComputedStyle(div).transform, + // translateX(10px * 2 + none) -> translateX(10px * 2 + 10px) @ 50% + 'matrix(1, 0, 0, 1, 25, 0)', + 'Animated transform list at 50s of the third iteration'); +}, 'iteration composition of transform from none to translate'); + +test(t => { + const div = createDiv(t); + const anim = + div.animate({ transform: ['matrix3d(1, 0, 0, 0, ' + + '0, 1, 0, 0, ' + + '0, 0, 1, 0, ' + + '0, 0, 30, 1)', + 'matrix3d(1, 0, 0, 0, ' + + '0, 1, 0, 0, ' + + '0, 0, 1, 0, ' + + '0, 0, 50, 1)'] }, + { duration: 100 * MS_PER_SEC, + easing: 'linear', + iterations: 10, + iterationComposite: 'accumulate' }); + anim.pause(); + + anim.currentTime = anim.effect.getComputedTiming().duration / 2; + assert_matrix_equals(getComputedStyle(div).transform, + 'matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 40, 1)', + 'Animated transform of matrix3d function at 50s of the first iteration'); + anim.currentTime = anim.effect.getComputedTiming().duration * 2; + assert_matrix_equals(getComputedStyle(div).transform, + // translateZ(30px) + (translateZ(50px)*2) + 'matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 130, 1)', + 'Animated transform of matrix3d function at 0s of the third iteration'); + anim.currentTime += anim.effect.getComputedTiming().duration / 2; + assert_matrix_equals(getComputedStyle(div).transform, + // from: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 130, 1) + // to: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 150, 1) + 'matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 140, 1)', + 'Animated transform of matrix3d function at 50s of the third iteration'); +}, 'iteration composition of transform of matrix3d function'); + +test(t => { + const div = createDiv(t); + const anim = + div.animate({ transform: ['rotate3d(1, 1, 0, 0deg)', + 'rotate3d(1, 1, 0, 90deg)'] }, + { duration: 100 * MS_PER_SEC, + easing: 'linear', + iterations: 10, + iterationComposite: 'accumulate' }); + anim.pause(); + + anim.currentTime = 0; + assert_matrix_equals(getComputedStyle(div).transform, + 'matrix(1, 0, 0, 1, 0, 0)', // Actually not rotated at all. + 'Animated transform of rotate3d function at 50s of the first iteration'); + anim.currentTime = anim.effect.getComputedTiming().duration * 2; + assert_matrix_equals(getComputedStyle(div).transform, + rotate3dToMatrix3d(1, 1, 0, Math.PI), // 180deg + 'Animated transform of rotate3d function at 0s of the third iteration'); + anim.currentTime += anim.effect.getComputedTiming().duration / 2; + assert_matrix_equals(getComputedStyle(div).transform, + rotate3dToMatrix3d(1, 1, 0, 225 * Math.PI / 180), //((270 + 180) * 0.5)deg + 'Animated transform of rotate3d function at 50s of the third iteration'); +}, 'iteration composition of transform of rotate3d function'); + +test(t => { + const div = createDiv(t); + const anim = + div.animate({ marginLeft: ['10px', '20px'] }, + { duration: 100 * MS_PER_SEC, + easing: 'linear', + iterations: 10, + iterationComposite: 'accumulate' }); + anim.pause(); + + anim.currentTime = anim.effect.getComputedTiming().duration / 2; + assert_equals(getComputedStyle(div).marginLeft, '15px', + 'Animated margin-left style at 50s of the first iteration'); + anim.currentTime = anim.effect.getComputedTiming().duration * 2; + assert_equals(getComputedStyle(div).marginLeft, '50px', // 10px + 20px + 20px + 'Animated margin-left style at 0s of the third iteration'); + anim.currentTime += anim.effect.getComputedTiming().duration / 2; + assert_equals(getComputedStyle(div).marginLeft, '55px', // (50px + 60px) * 0.5 + 'Animated margin-left style at 50s of the third iteration'); +}, 'iteration composition starts with non-zero value animation'); + +test(t => { + const div = createDiv(t); + const anim = + div.animate({ marginLeft: ['10px', '-10px'] }, + { duration: 100 * MS_PER_SEC, + easing: 'linear', + iterations: 10, + iterationComposite: 'accumulate' }); + anim.pause(); + + anim.currentTime = anim.effect.getComputedTiming().duration / 2; + assert_equals(getComputedStyle(div).marginLeft, + '0px', + 'Animated margin-left style at 50s of the first iteration'); + anim.currentTime = anim.effect.getComputedTiming().duration * 2; + assert_equals(getComputedStyle(div).marginLeft, + '-10px', // 10px + -10px + -10px + 'Animated margin-left style at 0s of the third iteration'); + anim.currentTime += anim.effect.getComputedTiming().duration / 2; + assert_equals(getComputedStyle(div).marginLeft, + '-20px', // (-10px + -30px) * 0.5 + 'Animated margin-left style at 50s of the third iteration'); +}, 'iteration composition with negative final value animation'); + +test(t => { + const div = createDiv(t); + const anim = div.animate({ marginLeft: ['0px', '10px'] }, + { duration: 100 * MS_PER_SEC, + easing: 'linear', + iterations: 10, + iterationComposite: 'accumulate' }); + anim.pause(); + + anim.currentTime = + anim.effect.getComputedTiming().duration * 2 + + anim.effect.getComputedTiming().duration / 2; + assert_equals(getComputedStyle(div).marginLeft, '25px', + 'Animated style at 50s of the third iteration'); + + // double its duration. + anim.effect.updateTiming({ + duration: anim.effect.getComputedTiming().duration * 2 + }); + assert_equals(getComputedStyle(div).marginLeft, '12.5px', + 'Animated style at 25s of the first iteration'); + + // half of original. + anim.effect.updateTiming({ + duration: anim.effect.getComputedTiming().duration / 4 + }); + assert_equals(getComputedStyle(div).marginLeft, '50px', + 'Animated style at 50s of the fourth iteration'); +}, 'duration changes with an iteration composition operation of accumulate'); + +</script> diff --git a/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-overlapping-keyframes.html b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-overlapping-keyframes.html new file mode 100644 index 0000000000..a2a0683921 --- /dev/null +++ b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-overlapping-keyframes.html @@ -0,0 +1,77 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>The effect value of a keyframe effect: Overlapping keyframes</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#the-effect-value-of-a-keyframe-animation-effect"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<body> +<div id="log"></div> +<div id="target"></div> +<script> +'use strict'; + +function assert_opacity_value(opacity, expected, description) { + return assert_approx_equals(parseFloat(opacity), expected, 0.0001, description); +} + +test(t => { + const div = createDiv(t); + const anim = div.animate([ { offset: 0, opacity: 0 }, + { offset: 0, opacity: 0.1 }, + { offset: 0, opacity: 0.2 }, + { offset: 1, opacity: 0.8 }, + { offset: 1, opacity: 0.9 }, + { offset: 1, opacity: 1 } ], + { duration: 1000, + easing: 'cubic-bezier(0.5, -0.5, 0.5, 1.5)' }); + assert_opacity_value(getComputedStyle(div).opacity, 0.2, + 'When progress is zero the last keyframe with offset 0 should' + + ' be used'); + // http://cubic-bezier.com/#.5,-0.5,.5,1.5 + // At t=0.15, the progress should be negative + anim.currentTime = 150; + assert_equals(getComputedStyle(div).opacity, '0', + 'When progress is negative, the first keyframe with a 0 offset' + + ' should be used'); + // At t=0.71, the progress should be just less than 1 + anim.currentTime = 710; + assert_approx_equals(parseFloat(getComputedStyle(div).opacity), 0.8, 0.01, + 'When progress is just less than 1, the first keyframe with an' + + ' offset of 1 should be used as the interval endpoint'); + // At t=0.85, the progress should be > 1 + anim.currentTime = 850; + assert_equals(getComputedStyle(div).opacity, '1', + 'When progress is greater than 1.0, the last keyframe with a 1' + + ' offset should be used'); + anim.finish(); + assert_equals(getComputedStyle(div).opacity, '1', + 'When progress is equal to 1.0, the last keyframe with a 1' + + ' offset should be used'); +}, 'Overlapping keyframes at 0 and 1 use the appropriate value when the' + + ' progress is outside the range [0, 1]'); + +test(t => { + const div = createDiv(t); + const anim = div.animate([ { offset: 0, opacity: 0 }, + { offset: 0.5, opacity: 0.3 }, + { offset: 0.5, opacity: 0.5 }, + { offset: 0.5, opacity: 0.7 }, + { offset: 1, opacity: 1 } ], 1000); + anim.currentTime = 250; + assert_opacity_value(getComputedStyle(div).opacity, 0.15, + 'Before the overlap point, the first keyframe from the' + + ' overlap point should be used as interval endpoint'); + anim.currentTime = 500; + assert_opacity_value(getComputedStyle(div).opacity, 0.7, + 'At the overlap point, the last keyframe from the' + + ' overlap point should be used as interval startpoint'); + anim.currentTime = 750; + assert_opacity_value(getComputedStyle(div).opacity, 0.85, + 'After the overlap point, the last keyframe from the' + + ' overlap point should be used as interval startpoint'); +}, 'Overlapping keyframes between 0 and 1 use the appropriate value on each' + + ' side of the overlap point'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-replaced-animations.html b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-replaced-animations.html new file mode 100644 index 0000000000..d40a01fdd2 --- /dev/null +++ b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-replaced-animations.html @@ -0,0 +1,161 @@ +<!doctype html> +<meta charset=utf-8> +<title>The effect value of a keyframe effect: Overlapping keyframes</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#the-effect-value-of-a-keyframe-animation-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'; + +function assert_opacity_value(opacity, expected, description) { + return assert_approx_equals( + parseFloat(opacity), + expected, + 0.0001, + description + ); +} + +promise_test(async t => { + const div = createDiv(t); + div.style.opacity = '0.1'; + + const animA = div.animate( + { opacity: 0.2 }, + { duration: 1, fill: 'forwards' } + ); + const animB = div.animate( + { opacity: 0.3 }, + { duration: 1, fill: 'forwards' } + ); + await animA.finished; + + // Sanity check + assert_equals(animA.replaceState, 'removed'); + assert_equals(animB.replaceState, 'active'); + + // animA is now removed so if we cancel animB, we should go back to the + // underlying value + animB.cancel(); + assert_opacity_value( + getComputedStyle(div).opacity, + 0.1, + 'Opacity should be the un-animated value' + ); +}, 'Removed animations do not contribute to animated style'); + +promise_test(async t => { + const div = createDiv(t); + div.style.opacity = '0.1'; + + const animA = div.animate( + { opacity: 0.2 }, + { duration: 1, fill: 'forwards' } + ); + const animB = div.animate( + { opacity: 0.3, composite: 'add' }, + { duration: 1, fill: 'forwards' } + ); + await animA.finished; + + // Sanity check + assert_equals(animA.replaceState, 'removed'); + assert_equals(animB.replaceState, 'active'); + + // animA has been removed so the final result should be 0.1 + 0.3 = 0.4. + // (If animA were not removed it would be 0.2 + 0.3 = 0.5.) + assert_opacity_value( + getComputedStyle(div).opacity, + 0.4, + 'Opacity value should not include the removed animation' + ); +}, 'Removed animations do not contribute to the effect stack'); + +promise_test(async t => { + const div = createDiv(t); + div.style.opacity = '0.1'; + + const animA = div.animate( + { opacity: 0.2 }, + { duration: 1, fill: 'forwards' } + ); + const animB = div.animate( + { opacity: 0.3 }, + { duration: 1, fill: 'forwards' } + ); + + await animA.finished; + + animA.persist(); + + animB.cancel(); + assert_opacity_value( + getComputedStyle(div).opacity, + 0.2, + "Opacity should be the persisted animation's value" + ); +}, 'Persisted animations contribute to animated style'); + +promise_test(async t => { + const div = createDiv(t); + div.style.opacity = '0.1'; + + const animA = div.animate( + { opacity: 0.2 }, + { duration: 1, fill: 'forwards' } + ); + const animB = div.animate( + { opacity: 0.3, composite: 'add' }, + { duration: 1, fill: 'forwards' } + ); + + await animA.finished; + + assert_opacity_value( + getComputedStyle(div).opacity, + 0.4, + 'Opacity value should NOT include the contribution of the removed animation' + ); + + animA.persist(); + + assert_opacity_value( + getComputedStyle(div).opacity, + 0.5, + 'Opacity value should include the contribution of the persisted animation' + ); +}, 'Persisted animations contribute to the effect stack'); + +promise_test(async t => { + const div = createDiv(t); + div.style.opacity = '0.1'; + + const animA = div.animate( + { opacity: 0.2 }, + { duration: 1, fill: 'forwards' } + ); + + // Persist the animation before it finishes (and before it would otherwise be + // removed). + animA.persist(); + + const animB = div.animate( + { opacity: 0.3, composite: 'add' }, + { duration: 1, fill: 'forwards' } + ); + + await animA.finished; + + assert_opacity_value( + getComputedStyle(div).opacity, + 0.5, + 'Opacity value should include the contribution of the persisted animation' + ); +}, 'Animations persisted before they would be removed contribute to the' + + ' effect stack'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-transformed-distance.html b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-transformed-distance.html new file mode 100644 index 0000000000..a33d6d4f24 --- /dev/null +++ b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-transformed-distance.html @@ -0,0 +1,84 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>The effect value of a keyframe effect: Calculating the transformed + distance between keyframes</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#the-effect-value-of-a-keyframe-animation-effect"> +<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> +<script> +'use strict'; + +// Test that applying easing to keyframes is applied as expected + +for (const params of gEasingTests) { + test(t => { + const target = createDiv(t); + const anim = target.animate([ { width: '0px' }, + // We put the easing on the second keyframe + // so we can test that it is only applied + // to the specified keyframe. + { width: '100px', easing: params.easing }, + { width: '200px' } ], + { duration: 2000, + fill: 'forwards' }); + + for (const sampleTime of [0, 999, 1000, 1100, 1500, 2000]) { + anim.currentTime = sampleTime; + + const portion = (sampleTime - 1000) / 1000; + const expectedWidth = sampleTime < 1000 + ? sampleTime / 10 // first segment is linear + : 100 + params.easingFunction(portion) * 100; + assert_approx_equals(parseFloat(getComputedStyle(target).width), + expectedWidth, + 0.02, + 'The width should be approximately ' + + `${expectedWidth} at ${sampleTime}ms`); + } + }, `A ${params.desc} on a keyframe affects the resulting style`); +} + +// Test that a linear-equivalent cubic-bezier easing applied to a keyframe does +// not alter (including clamping) the result. + +for (const params of gEasingTests) { + const linearEquivalentEasings = [ 'cubic-bezier(0, 0, 0, 0)', + 'cubic-bezier(1, 1, 1, 1)' ]; + test(t => { + for (const linearEquivalentEasing of linearEquivalentEasings) { + const timing = { duration: 1000, + fill: 'forwards', + easing: params.easing }; + + const linearTarget = createDiv(t); + const linearAnim = linearTarget.animate([ { width: '0px' }, + { width: '100px' } ], + timing); + + const equivalentTarget = createDiv(t); + const equivalentAnim = + equivalentTarget.animate([ { width: '0px', + easing: linearEquivalentEasing }, + { width: '100px' } ], + timing); + + for (const sampleTime of [0, 250, 500, 750, 1000]) { + linearAnim.currentTime = sampleTime; + equivalentAnim.currentTime = sampleTime; + + assert_equals(getComputedStyle(linearTarget).width, + getComputedStyle(equivalentTarget).width, + `The 'width' of the animated elements should be equal ` + + `at ${sampleTime}ms`); + } + } + }, 'Linear-equivalent cubic-bezier keyframe easing applied to an effect ' + + `with a ${params.desc} does not alter the result`); +} + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/transform-and-opacity-on-inline-001-ref.html b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/transform-and-opacity-on-inline-001-ref.html new file mode 100644 index 0000000000..1e7f250c48 --- /dev/null +++ b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/transform-and-opacity-on-inline-001-ref.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html> +<title>CSS Test (Animations): Element.animate() animating both transform and opacity on an inline</title> +<link rel="author" title="L. David Baron" href="https://dbaron.org/"> +<link rel="author" title="Google" href="http://www.google.com/"> + +<style> +#target { + opacity: 0.4; + will-change: opacity; +} +</style> + +<body><span id="target">x</span></body> diff --git a/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/transform-and-opacity-on-inline-001.html b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/transform-and-opacity-on-inline-001.html new file mode 100644 index 0000000000..f76b53cd4c --- /dev/null +++ b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/transform-and-opacity-on-inline-001.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<html> +<title>CSS Test (Animations): Element.animate() animating both transform and opacity on an inline</title> +<link rel="author" title="L. David Baron" href="https://dbaron.org/"> +<link rel="author" title="Google" href="http://www.google.com/"> +<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=1316688"> +<link rel="match" href="transform-and-opacity-on-inline-001-ref.html"> +<meta name="assert" content="This should not crash, and should render as opacity 0.5."> + +<script> +// The transform animation should be ignored; the opacity animation should work. +window.onload = function() { + document.getElementById("target").animate( + [ + { + "transform": "translateX(0px)", + "opacity": "0.8", + }, + { + "transform": "translateX(300px)", + "opacity": "0.0", + } + ], + { duration:1000000, delay: -500000, easing: "steps(3, jump-both)" }); +} +</script> +<body><span id="target">x</span></body> diff --git a/testing/web-platform/tests/web-animations/crashtests/get-computed-timing-crash.html b/testing/web-platform/tests/web-animations/crashtests/get-computed-timing-crash.html new file mode 100644 index 0000000000..b666eea91f --- /dev/null +++ b/testing/web-platform/tests/web-animations/crashtests/get-computed-timing-crash.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<html> +<title>GetComputedTiming on an animation without an execution context, +timeline or playback rate</title> +<link rel="author" title="Google" href="http://www.google.com/"> +<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=1318012"> +<meta name="assert" content="This should not crash."> +<body> + <div id="target"></div> + <iframe id="iframe"></iframe> +</body> +<script type="text/javascript"> + const keyframeEffect = new KeyframeEffect(target, { opacity: [1, 1] }); + const anim = new iframe.contentWindow.Animation(keyframeEffect, null); + anim.play(); + anim.playbackRate = 0; + document.body.removeChild(iframe); + anim.effect.getComputedTiming(); +</script> +</html> diff --git a/testing/web-platform/tests/web-animations/crashtests/infinite-active-duration.html b/testing/web-platform/tests/web-animations/crashtests/infinite-active-duration.html new file mode 100644 index 0000000000..f92cd13942 --- /dev/null +++ b/testing/web-platform/tests/web-animations/crashtests/infinite-active-duration.html @@ -0,0 +1,68 @@ +<!DOCTYPE html> +<title>Various test cases producing infinite active duration</title> +<link rel="help" href="https://drafts.csswg.org/css-animations-1/#animation-iteration-count" /> +<script> + let effect = new KeyframeEffect(null, + { opacity: [0, 1] }, + { duration: 1, delay: -17592186044416, iterations: Infinity }); + effect.getComputedTiming(); + + // Infinity delay + Infinity active duration + effect = new KeyframeEffect(null, + { opacity: [0, 1] }, + { duration: 1, delay: Number.MAX_VALUE, iterations: Infinity }); + effect.getComputedTiming(); + + // Infinity end delay + Infinity active duration + effect = new KeyframeEffect(null, + { opacity: [0, 1] }, + { duration: 1, endDelay: Number.MAX_VALUE, iterations: Infinity }); + effect.getComputedTiming(); + + // Infinity delay + Infinity active duration + Infinity end delay + effect = new KeyframeEffect(null, + { opacity: [0, 1] }, + { duration: 1, + delay: Number.MAX_VALUE, endDelay: Number.MAX_VALUE, + iterations: Infinity }); + effect.getComputedTiming(); + + // -Infinity delay + Infinity active duration + effect = new KeyframeEffect(null, + { opacity: [0, 1] }, + { duration: 1, delay: -Number.MAX_VALUE, iterations: Infinity }); + effect.getComputedTiming(); + + // -Infinity end delay + Infinity active duration + effect = new KeyframeEffect(null, + { opacity: [0, 1] }, + { duration: 1, endDelay: -Number.MAX_VALUE, iterations: Infinity }); + effect.getComputedTiming(); + + // -Infinity delay + Infinity active duration + -Infinity end delay + effect = new KeyframeEffect(null, + { opacity: [0, 1] }, + { duration: 1, + delay: -Number.MAX_VALUE, endDelay: -Number.MAX_VALUE, + iterations: Infinity }); + effect.getComputedTiming(); + + // -Infinity delay + finite active duration + effect = new KeyframeEffect(null, + { opacity: [0, 1] }, + { duration: 1, delay: -Number.MAX_VALUE, iterations: 1 }); + effect.getComputedTiming(); + + // -Infinity end delay + finite active duration + effect = new KeyframeEffect(null, + { opacity: [0, 1] }, + { duration: 1, endDelay: -Number.MAX_VALUE, iterations: 1 }); + effect.getComputedTiming(); + + // very large iterations + effect = new KeyframeEffect(null, + { opacity: [0, 1] }, + { duration: 1, delay: 281474976710655, iterations: 18014398509481984 }); + effect.getComputedTiming(); + +</script> diff --git a/testing/web-platform/tests/web-animations/crashtests/partially-overlapping-animations-one-not-current-001.html b/testing/web-platform/tests/web-animations/crashtests/partially-overlapping-animations-one-not-current-001.html new file mode 100644 index 0000000000..b943514f42 --- /dev/null +++ b/testing/web-platform/tests/web-animations/crashtests/partially-overlapping-animations-one-not-current-001.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html> +<title>CSS Test (Animations): Reparenting an element with a web animation on the compositor</title> +<link rel="author" title="L. David Baron" href="https://dbaron.org/"> +<link rel="author" title="Google" href="http://www.google.com/"> +<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=1319304"> +<meta name="assert" content="This should not crash."> + +<script> +window.onload = function(){ + let div = document.querySelector("div"); + let a1 = div.animate([{"transform": "translateX(10px)", "opacity": "0.4"}], { duration: 1000 }); + a1.reverse(); + let a2 = div.animate([{"transform": "translateY(10px)"}], { duration: 1000 }); +} +</script> +<div>X</div> diff --git a/testing/web-platform/tests/web-animations/crashtests/reparent-animating-element-001.html b/testing/web-platform/tests/web-animations/crashtests/reparent-animating-element-001.html new file mode 100644 index 0000000000..49ee9c433c --- /dev/null +++ b/testing/web-platform/tests/web-animations/crashtests/reparent-animating-element-001.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<html class="test-wait"> +<title>CSS Test (Animations): Reparenting an element with a web animation on the compositor</title> +<link rel="author" title="L. David Baron" href="https://dbaron.org/"> +<link rel="author" title="Google" href="http://www.google.com/"> +<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=1301838"> +<meta name="assert" content="This should not crash."> + +<style> +#animate { + width: 100px; + height: 100px; + background: blue; +} +</style> +<div id="animate"></div> +<div id="newparent"></div> +<script> + +document.getElementById("animate").animate( + [ + { transform: "rotate(0deg)" }, + { transform: "rotate(360deg)" } + ], + { + duration: 5000, + iterations: Infinity + } +); + +requestAnimationFrame(function() { + requestAnimationFrame(function() { + document.getElementById("newparent").appendChild(document.getElementById("animate")); + requestAnimationFrame(function() { + document.documentElement.classList.remove("test-wait"); + }); + }); +}); + +</script> diff --git a/testing/web-platform/tests/web-animations/crashtests/reparent-animating-element-002.html b/testing/web-platform/tests/web-animations/crashtests/reparent-animating-element-002.html new file mode 100644 index 0000000000..0d3549fc33 --- /dev/null +++ b/testing/web-platform/tests/web-animations/crashtests/reparent-animating-element-002.html @@ -0,0 +1,44 @@ +<!DOCTYPE html> +<html class="test-wait"> +<title>CSS Test (Animations): Reparenting an element with a web animation on the compositor</title> +<link rel="author" title="L. David Baron" href="https://dbaron.org/"> +<link rel="author" title="Google" href="http://www.google.com/"> +<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=1305487"> +<meta name="assert" content="This should not crash."> +<!-- + +The Chromium implementation of <marquee> essentially uses web animations +underneath. However, I was unable to make a testcase for this crash +that uses web animations directly. Despite that, it still seems worth +adding this testcase here in WPT. + +--> + +<style> +#animate { + width: 100px; + height: 100px; +} +#newparent { + display: none; +} +</style> +<marquee id="animate">X</marquee> +<div id="newparent"></div> +<script> + +let a = document.getElementById("animate"); + +requestAnimationFrame(function() { + // use setTimeout because the crash doesn't happen if we do this inside + // a requestAnimationFrame callback + setTimeout(function() { + a.remove(); + document.getElementById("newparent").appendChild(a); + requestAnimationFrame(function() { + document.documentElement.classList.remove("test-wait"); + }); + }, 0); +}); + +</script> diff --git a/testing/web-platform/tests/web-animations/idlharness.window.js b/testing/web-platform/tests/web-animations/idlharness.window.js new file mode 100644 index 0000000000..aaf6f58e7b --- /dev/null +++ b/testing/web-platform/tests/web-animations/idlharness.window.js @@ -0,0 +1,21 @@ +// META: script=/resources/WebIDLParser.js +// META: script=/resources/idlharness.js +// META: timeout=long + +'use strict'; + +idl_test( + ['web-animations', 'web-animations-2'], + ['dom', 'html', 'scroll-animations'], + idl_array => { + idl_array.add_objects({ + Animation: ['new Animation()'], + AnimationPlaybackEvent: ['new AnimationPlaybackEvent("cancel")'], + Document: ['document'], + DocumentTimeline: ['document.timeline'], + KeyframeEffect: ['new KeyframeEffect(null, null)'], + ShadowRoot: ['shadowRoot'], + }); + self.shadowRoot = document.createElement("div").attachShadow({mode: "open"}); + } +); diff --git a/testing/web-platform/tests/web-animations/interfaces/Animatable/animate-no-browsing-context.html b/testing/web-platform/tests/web-animations/interfaces/Animatable/animate-no-browsing-context.html new file mode 100644 index 0000000000..61a7502a98 --- /dev/null +++ b/testing/web-platform/tests/web-animations/interfaces/Animatable/animate-no-browsing-context.html @@ -0,0 +1,107 @@ +<!doctype html> +<meta charset=utf-8> +<title>Animatable.animate in combination with elements in documents + without a browsing context</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#dom-animatable-animate"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<div id="log"></div> +<script> +'use strict'; + +// +// The following tests relate to animations on elements in documents without +// a browsing context. This is NOT the same as documents that are not bound to +// a document tree. +// + +function getXHRDoc(t) { + return new Promise(resolve => { + const xhr = new XMLHttpRequest(); + xhr.open('GET', '../../resources/xhr-doc.py'); + xhr.responseType = 'document'; + xhr.onload = t.step_func(() => { + assert_equals(xhr.readyState, xhr.DONE, + 'Request should complete successfully'); + assert_equals(xhr.status, 200, + 'Response should be OK'); + resolve(xhr.responseXML); + }); + xhr.send(); + }); +} + +promise_test(t => { + return getXHRDoc(t).then(xhrdoc => { + const div = xhrdoc.getElementById('test'); + const anim = div.animate(null); + assert_class_string(anim.timeline, 'DocumentTimeline', + 'Animation should have a timeline'); + assert_equals(anim.timeline, xhrdoc.timeline, + 'Animation timeline should be the default document timeline' + + ' of the XHR doc'); + assert_not_equals(anim.timeline, document.timeline, + 'Animation timeline should NOT be the same timeline as' + + ' the default document timeline for the current' + + ' document'); + + }); +}, 'Element.animate() creates an animation with the correct timeline' + + ' when called on an element in a document without a browsing context'); + +// +// The following tests are cross-cutting tests that are not specific to the +// Animatable.animate() interface. Instead, they draw on assertions from +// various parts of the spec. These assertions are tested in other tests +// but are repeated here to confirm that user agents are not doing anything +// different in the particular case of document without a browsing context. +// + +promise_test(t => { + return getXHRDoc(t).then(xhrdoc => { + const div = xhrdoc.getElementById('test'); + const anim = div.animate(null); + // Since a document from XHR will not be active by itself, its document + // timeline will also be inactive. + assert_equals(anim.timeline.currentTime, null, + 'Document timeline time should be null'); + }); +}, 'The timeline associated with an animation trigger on an element in' + + ' a document without a browsing context is inactive'); + +promise_test(t => { + let anim; + return getXHRDoc(t).then(xhrdoc => { + const div = xhrdoc.getElementById('test'); + anim = div.animate(null); + anim.timeline = document.timeline; + assert_true(anim.pending, 'The animation should be initially pending'); + return waitForAnimationFrames(2); + }).then(() => { + // Because the element is in a document without a browsing context, it will + // not be rendered and hence the user agent will never deem it ready to + // animate. + assert_true(anim.pending, + 'The animation should still be pending after replacing' + + ' the document timeline'); + }); +}, 'Replacing the timeline of an animation targetting an element in a' + + ' document without a browsing context leaves it in the pending state'); + +promise_test(t => { + let anim; + return getXHRDoc(t).then(xhrdoc => { + const div = xhrdoc.getElementById('test'); + anim = div.animate({ opacity: [ 0, 1 ] }, 1000); + anim.timeline = document.timeline; + document.body.appendChild(div); + assert_equals(getComputedStyle(div).opacity, '0', + 'Style should be updated'); + }); +}, 'Replacing the timeline of an animation targetting an element in a' + + ' document without a browsing context and then adopting that element' + + ' causes it to start updating style'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/interfaces/Animatable/animate.html b/testing/web-platform/tests/web-animations/interfaces/Animatable/animate.html new file mode 100644 index 0000000000..dad633ba9a --- /dev/null +++ b/testing/web-platform/tests/web-animations/interfaces/Animatable/animate.html @@ -0,0 +1,346 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Animatable.animate</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#dom-animatable-animate"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<script src="../../resources/easing-tests.js"></script> +<script src="../../resources/keyframe-utils.js"></script> +<script src="../../resources/keyframe-tests.js"></script> +<script src="../../resources/timing-utils.js"></script> +<script src="../../resources/timing-tests.js"></script> +<style> +.pseudo::before {content: '';} +.pseudo::after {content: '';} +.pseudo::marker {content: '';} +</style> +<body> +<div id="log"></div> +<iframe width="10" height="10" id="iframe"></iframe> +<script> +'use strict'; + +// Tests on Element + +test(t => { + const anim = createDiv(t).animate(null); + assert_class_string(anim, 'Animation', 'Returned object is an Animation'); +}, 'Element.animate() creates an Animation object'); + +test(t => { + const iframe = window.frames[0]; + const div = createDiv(t, iframe.document); + const anim = Element.prototype.animate.call(div, null); + assert_equals(Object.getPrototypeOf(anim), iframe.Animation.prototype, + 'The prototype of the created Animation is that defined on' + + ' the relevant global for the target element'); + assert_not_equals(Object.getPrototypeOf(anim), Animation.prototype, + 'The prototype of the created Animation is NOT that of' + + ' the current global'); +}, 'Element.animate() creates an Animation object in the relevant realm of' + + ' the target element'); + +test(t => { + const div = createDiv(t); + const anim = Element.prototype.animate.call(div, null); + assert_class_string(anim.effect, 'KeyframeEffect', + 'Returned Animation has a KeyframeEffect'); +}, 'Element.animate() creates an Animation object with a KeyframeEffect'); + +test(t => { + const iframe = window.frames[0]; + const div = createDiv(t, iframe.document); + const anim = Element.prototype.animate.call(div, null); + assert_equals(Object.getPrototypeOf(anim.effect), + iframe.KeyframeEffect.prototype, + 'The prototype of the created KeyframeEffect is that defined on' + + ' the relevant global for the target element'); + assert_not_equals(Object.getPrototypeOf(anim.effect), + KeyframeEffect.prototype, + 'The prototype of the created KeyframeEffect is NOT that of' + + ' the current global'); +}, 'Element.animate() creates an Animation object with a KeyframeEffect' + + ' that is created in the relevant realm of the target element'); + +for (const subtest of gEmptyKeyframeListTests) { + test(t => { + const anim = createDiv(t).animate(subtest, 2000); + assert_not_equals(anim, null); + }, 'Element.animate() accepts empty keyframe lists ' + + `(input: ${JSON.stringify(subtest)})`); +} + +for (const subtest of gKeyframesTests) { + test(t => { + const anim = createDiv(t).animate(subtest.input, 2000); + assert_frame_lists_equal(anim.effect.getKeyframes(), subtest.output); + }, `Element.animate() accepts ${subtest.desc}`); +} + +for (const subtest of gInvalidKeyframesTests) { + test(t => { + const div = createDiv(t); + assert_throws_js(TypeError, () => { + div.animate(subtest.input, 2000); + }); + }, `Element.animate() does not accept ${subtest.desc}`); +} + +test(t => { + const anim = createDiv(t).animate(null, 2000); + assert_equals(anim.effect.getTiming().duration, 2000); + assert_default_timing_except(anim.effect, ['duration']); +}, 'Element.animate() accepts a double as an options argument'); + +test(t => { + const anim = createDiv(t).animate(null, + { duration: Infinity, fill: 'forwards' }); + assert_equals(anim.effect.getTiming().duration, Infinity); + assert_equals(anim.effect.getTiming().fill, 'forwards'); + assert_default_timing_except(anim.effect, ['duration', 'fill']); +}, 'Element.animate() accepts a KeyframeAnimationOptions argument'); + +test(t => { + const anim = createDiv(t).animate(null); + assert_default_timing_except(anim.effect, []); +}, 'Element.animate() accepts an absent options argument'); + +for (const invalid of gBadDelayValues) { + test(t => { + assert_throws_js(TypeError, () => { + createDiv(t).animate(null, { delay: invalid }); + }); + }, `Element.animate() does not accept invalid delay value: ${invalid}`); +} + +test(t => { + const anim = createDiv(t).animate(null, { duration: 'auto' }); + assert_equals(anim.effect.getTiming().duration, 'auto', 'set duration \'auto\''); + assert_equals(anim.effect.getComputedTiming().duration, 0, + 'getComputedTiming() after set duration \'auto\''); +}, 'Element.animate() accepts a duration of \'auto\' using a dictionary' + + ' object'); + +for (const invalid of gBadDurationValues) { + if (typeof invalid === 'string' && !isNaN(parseFloat(invalid))) { + continue; + } + test(t => { + assert_throws_js(TypeError, () => { + createDiv(t).animate(null, invalid); + }); + }, 'Element.animate() does not accept invalid duration value: ' + + (typeof invalid === 'string' ? `"${invalid}"` : invalid)); +} + +for (const invalid of gBadDurationValues) { + test(t => { + assert_throws_js(TypeError, () => { + createDiv(t).animate(null, { duration: invalid }); + }); + }, 'Element.animate() does not accept invalid duration value: ' + + (typeof invalid === 'string' ? `"${invalid}"` : invalid) + + ' using a dictionary object'); +} + +for (const invalidEasing of gInvalidEasings) { + test(t => { + assert_throws_js(TypeError, () => { + createDiv(t).animate({ easing: invalidEasing }, 2000); + }); + }, `Element.animate() does not accept invalid easing: '${invalidEasing}'`); +} + +for (const invalid of gBadIterationStartValues) { + test(t => { + assert_throws_js(TypeError, () => { + createDiv(t).animate(null, { iterationStart: invalid }); + }); + }, 'Element.animate() does not accept invalid iterationStart value: ' + + invalid); +} + +for (const invalid of gBadIterationsValues) { + test(t => { + assert_throws_js(TypeError, () => { + createDiv(t).animate(null, { iterations: invalid }); + }); + }, 'Element.animate() does not accept invalid iterations value: ' + + invalid); +} + +test(t => { + const anim = createDiv(t).animate(null, 2000); + assert_equals(anim.id, ''); +}, 'Element.animate() correctly sets the id attribute when no id is specified'); + +test(t => { + const anim = createDiv(t).animate(null, { id: 'test' }); + assert_equals(anim.id, 'test'); +}, 'Element.animate() correctly sets the id attribute'); + +test(t => { + const anim = createDiv(t).animate(null, 2000); + assert_equals(anim.timeline, document.timeline); +}, 'Element.animate() correctly sets the Animation\'s timeline'); + +async_test(t => { + const iframe = document.createElement('iframe'); + iframe.width = 10; + iframe.height = 10; + + iframe.addEventListener('load', t.step_func(() => { + const div = createDiv(t, iframe.contentDocument); + const anim = div.animate(null, 2000); + assert_equals(anim.timeline, iframe.contentDocument.timeline); + iframe.remove(); + t.done(); + })); + + document.body.appendChild(iframe); +}, 'Element.animate() correctly sets the Animation\'s timeline when ' + + 'triggered on an element in a different document'); + +for (const subtest of gAnimationTimelineTests) { + test(t => { + const anim = createDiv(t).animate(null, { timeline: subtest.timeline }); + assert_not_equals(anim, null, + 'An animation sohuld be created'); + assert_equals(anim.timeline, subtest.expectedTimeline, + 'Animation timeline should be '+ + subtest.expectedTimelineDescription); + }, 'Element.animate() correctly sets the Animation\'s timeline ' + + subtest.description + ' in KeyframeAnimationOptions.'); +} + +test(t => { + const anim = createDiv(t).animate(null, 2000); + assert_equals(anim.playState, 'running'); +}, 'Element.animate() calls play on the Animation'); + +promise_test(async t => { + const div = createDiv(t); + + let gotTransition = false; + div.addEventListener('transitionrun', () => { + gotTransition = true; + }); + + // Setup transition start point. + div.style.transition = 'opacity 100s'; + getComputedStyle(div).opacity; + + // Update specified style but don't flush style. + div.style.opacity = '0.5'; + + // Trigger a new animation at the same time. + const anim = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC); + + // If Element.animate() produces a style change event it will have triggered + // a transition. + // + // If it does NOT produce a style change event, the animation will override + // the before-change style and after-change style such that a transition is + // never triggered. + + // Wait for the animation to start and then for one more animation + // frame to give the transitionrun event a chance to be dispatched. + await anim.ready; + await waitForAnimationFrames(1); + + assert_false(gotTransition, 'A transition should NOT have been triggered'); +}, 'Element.animate() does NOT trigger a style change event'); + +// Tests on pseudo-elements +// Some tests occur twice (on pseudo-elements with and without content) +// in order to test both code paths for tree-abiding pseudo-elements in blink. + +test(t => { + const div = createDiv(t); + div.classList.add('pseudo'); + const anim = div.animate(null, {pseudoElement: '::before'}); + assert_class_string(anim, 'Animation', 'The returned object is an Animation'); +}, 'animate() with pseudoElement parameter creates an Animation object'); + +test(t => { + const div = createDiv(t); + const anim = div.animate(null, {pseudoElement: '::before'}); + assert_class_string(anim, 'Animation', 'The returned object is an Animation'); +}, 'animate() with pseudoElement parameter without content creates an Animation object'); + +test(t => { + const div = createDiv(t); + div.classList.add('pseudo'); + div.style.display = 'list-item'; + const anim = div.animate(null, {pseudoElement: '::marker'}); + assert_class_string(anim, 'Animation', 'The returned object is an Animation for ::marker'); +}, 'animate() with pseudoElement parameter creates an Animation object for ::marker'); + +test(t => { + const div = createDiv(t); + div.classList.add('pseudo'); + div.textContent = 'foo'; + const anim = div.animate(null, {pseudoElement: '::first-line'}); + assert_class_string(anim, 'Animation', 'The returned object is an Animation for ::first-line'); +}, 'animate() with pseudoElement parameter creates an Animation object for ::first-line'); + +test(t => { + const div = createDiv(t); + div.classList.add('pseudo'); + const anim = div.animate(null, {pseudoElement: '::before'}); + assert_equals(anim.effect.target, div, 'The returned element has the correct target element'); + assert_equals(anim.effect.pseudoElement, '::before', + 'The returned Animation targets the correct selector'); +}, 'animate() with pseudoElement an Animation object targeting ' + + 'the correct pseudo-element'); + +test(t => { + const div = createDiv(t); + const anim = div.animate(null, {pseudoElement: '::before'}); + assert_equals(anim.effect.target, div, 'The returned element has the correct target element'); + assert_equals(anim.effect.pseudoElement, '::before', + 'The returned Animation targets the correct selector'); +}, 'animate() with pseudoElement without content creates an Animation object targeting ' + + 'the correct pseudo-element'); + +test(t => { + const div = createDiv(t); + div.classList.add('pseudo'); + div.style.display = 'list-item'; + const anim = div.animate(null, {pseudoElement: '::marker'}); + assert_equals(anim.effect.target, div, 'The returned element has the correct target element'); + assert_equals(anim.effect.pseudoElement, '::marker', + 'The returned Animation targets the correct selector'); +}, 'animate() with pseudoElement an Animation object targeting ' + + 'the correct pseudo-element for ::marker'); + +test(t => { + const div = createDiv(t); + div.classList.add('pseudo'); + div.textContent = 'foo'; + const anim = div.animate(null, {pseudoElement: '::first-line'}); + assert_equals(anim.effect.target, div, 'The returned element has the correct target element'); + assert_equals(anim.effect.pseudoElement, '::first-line', + 'The returned Animation targets the correct selector'); +}, 'animate() with pseudoElement an Animation object targeting ' + + 'the correct pseudo-element for ::first-line'); + +for (const pseudo of [ + '', + 'before', + ':abc', + '::abc', + '::placeholder', +]) { + test(t => { + const div = createDiv(t); + assert_throws_dom("SyntaxError", () => { + div.animate(null, {pseudoElement: pseudo}); + }); + }, `animate() with a non-null invalid pseudoElement '${pseudo}' throws a ` + + `SyntaxError`); +} + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/interfaces/Animatable/getAnimations-iframe.html b/testing/web-platform/tests/web-animations/interfaces/Animatable/getAnimations-iframe.html new file mode 100644 index 0000000000..1851878c41 --- /dev/null +++ b/testing/web-platform/tests/web-animations/interfaces/Animatable/getAnimations-iframe.html @@ -0,0 +1,51 @@ +<!DOCTYPE html> +<title>getAnimations in dirty iframe</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<style> + iframe { + width: 200px; + height: 40px; + } +</style> +<body> +<script> + + const createFrame = async test => { + const iframe = createElement(test, "iframe"); + const contents = "" + + "<style>" + + " div { color: red; }" + + " @keyframes test {" + + " from { color: green; }" + + " to { color: green; }" + + " }" + + " @media (min-width: 300px) {" + + " div { animation: test 1s linear forwards; }" + + " }" + + "</style>" + + "<div id=div>Green</div>"; + iframe.setAttribute("srcdoc", contents); + await new Promise(resolve => iframe.addEventListener("load", resolve)); + return iframe; + }; + + const iframeTest = (getAnimations, interfaceName) => { + promise_test(async test => { + const frame = await createFrame(test); + const inner_div = frame.contentDocument.getElementById('div'); + assert_equals(getComputedStyle(inner_div).color, 'rgb(255, 0, 0)'); + + frame.style.width = '400px'; + const animations = getAnimations(inner_div); + assert_equals(animations.length, 1); + assert_equals(getComputedStyle(inner_div).color, 'rgb(0, 128, 0)'); + }, `Calling ${interfaceName}.getAnimations updates layout of parent frame if needed`); + } + + iframeTest(element => element.getAnimations(), 'Element'); + iframeTest(element => element.ownerDocument.getAnimations(), 'Document'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/interfaces/Animatable/getAnimations.html b/testing/web-platform/tests/web-animations/interfaces/Animatable/getAnimations.html new file mode 100644 index 0000000000..fd8719299d --- /dev/null +++ b/testing/web-platform/tests/web-animations/interfaces/Animatable/getAnimations.html @@ -0,0 +1,355 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Animatable.getAnimations</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#dom-animatable-getanimations"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<body> +<script> +'use strict'; + +test(t => { + const div = createDiv(t); + assert_array_equals(div.getAnimations(), []); +}, 'Returns an empty array for an element with no animations'); + +test(t => { + const div = createDiv(t); + const animationA = div.animate(null, 100 * MS_PER_SEC); + const animationB = div.animate(null, 100 * MS_PER_SEC); + assert_array_equals(div.getAnimations(), [animationA, animationB]); +}, 'Returns both animations for an element with two animations'); + +test(t => { + const divA = createDiv(t); + const divB = createDiv(t); + const animationA = divA.animate(null, 100 * MS_PER_SEC); + const animationB = divB.animate(null, 100 * MS_PER_SEC); + assert_array_equals(divA.getAnimations(), [animationA], 'divA'); + assert_array_equals(divB.getAnimations(), [animationB], 'divB'); +}, 'Returns only the animations specific to each sibling element'); + +test(t => { + const divParent = createDiv(t); + const divChild = createDiv(t); + divParent.appendChild(divChild); + const animationParent = divParent.animate(null, 100 * MS_PER_SEC); + const animationChild = divChild.animate(null, 100 * MS_PER_SEC); + assert_array_equals(divParent.getAnimations(), [animationParent], + 'divParent'); + assert_array_equals(divChild.getAnimations(), [animationChild], 'divChild'); +}, 'Returns only the animations specific to each parent/child element'); + +test(t => { + const divParent = createDiv(t); + const divChild = createDiv(t); + divParent.appendChild(divChild); + const divGrandChildA = createDiv(t); + const divGrandChildB = createDiv(t); + divChild.appendChild(divGrandChildA); + divChild.appendChild(divGrandChildB); + + // Trigger the animations in a somewhat random order + const animGrandChildB = divGrandChildB.animate(null, 100 * MS_PER_SEC); + const animChild = divChild.animate(null, 100 * MS_PER_SEC); + const animGrandChildA = divGrandChildA.animate(null, 100 * MS_PER_SEC); + + assert_array_equals( + divParent.getAnimations({ subtree: true }), + [animGrandChildB, animChild, animGrandChildA], + 'Returns expected animations from parent' + ); + assert_array_equals( + divChild.getAnimations({ subtree: true }), + [animGrandChildB, animChild, animGrandChildA], + 'Returns expected animations from child' + ); + assert_array_equals( + divGrandChildA.getAnimations({ subtree: true }), + [animGrandChildA], + 'Returns expected animations from grandchild A' + ); +}, 'Returns animations on descendants when subtree: true is specified'); + +test(t => { + createStyle(t, { + '@keyframes anim': '', + [`.pseudo::before`]: 'animation: anim 100s; ' + "content: '';", + }); + const div = createDiv(t); + div.classList.add('pseudo'); + + assert_equals( + div.getAnimations().length, + 0, + 'Returns no animations when subtree is false' + ); + assert_equals( + div.getAnimations({ subtree: true }).length, + 1, + 'Returns one animation when subtree is true' + ); +}, 'Returns animations on pseudo-elements when subtree: true is specified'); + +test(t => { + const host = createDiv(t); + const shadow = host.attachShadow({ mode: 'open' }); + + const elem = createDiv(t); + shadow.appendChild(elem); + + const elemChild = createDiv(t); + elem.appendChild(elemChild); + + elemChild.animate(null, 100 * MS_PER_SEC); + + assert_equals( + host.getAnimations({ subtree: true }).length, + 0, + 'Returns no animations with subtree:true when called on the host' + ); + assert_equals( + elem.getAnimations({ subtree: true }).length, + 1, + 'Returns one animation when called on a parent in the shadow tree' + ); +}, 'Does NOT cross shadow-tree boundaries when subtree: true is specified'); + +test(t => { + const foreignElement + = document.createElementNS('http://example.org/test', 'test'); + document.body.appendChild(foreignElement); + t.add_cleanup(() => { + foreignElement.remove(); + }); + + const animation = foreignElement.animate(null, 100 * MS_PER_SEC); + assert_array_equals(foreignElement.getAnimations(), [animation]); +}, 'Returns animations for a foreign element'); + +test(t => { + const div = createDiv(t); + const animation = div.animate(null, 100 * MS_PER_SEC); + animation.finish(); + assert_array_equals(div.getAnimations(), []); +}, 'Does not return finished animations that do not fill forwards'); + +test(t => { + const div = createDiv(t); + const animation = div.animate(null, { + duration: 100 * MS_PER_SEC, + fill: 'forwards', + }); + animation.finish(); + assert_array_equals(div.getAnimations(), [animation]); +}, 'Returns finished animations that fill forwards'); + +test(t => { + const div = createDiv(t); + const animation = div.animate(null, { + duration: 100 * MS_PER_SEC, + delay: 100 * MS_PER_SEC, + }); + assert_array_equals(div.getAnimations(), [animation]); +}, 'Returns animations yet to reach their active phase'); + +test(t => { + const div = createDiv(t); + const animation = div.animate(null, 100 * MS_PER_SEC); + animation.playbackRate = -1; + assert_array_equals(div.getAnimations(), []); +}, 'Does not return reversed finished animations that do not fill backwards'); + +test(t => { + const div = createDiv(t); + const animation = div.animate(null, { + duration: 100 * MS_PER_SEC, + fill: 'backwards', + }); + animation.playbackRate = -1; + assert_array_equals(div.getAnimations(), [animation]); +}, 'Returns reversed finished animations that fill backwards'); + +test(t => { + const div = createDiv(t); + const animation = div.animate(null, 100 * MS_PER_SEC); + animation.playbackRate = -1; + animation.currentTime = 200 * MS_PER_SEC; + assert_array_equals(div.getAnimations(), [animation]); +}, 'Returns reversed animations yet to reach their active phase'); + +test(t => { + const div = createDiv(t); + const animation = div.animate(null, { + duration: 100 * MS_PER_SEC, + delay: 100 * MS_PER_SEC, + }); + animation.playbackRate = 0; + assert_array_equals(div.getAnimations(), []); +}, 'Does not return animations with zero playback rate in before phase'); + +test(t => { + const div = createDiv(t); + const animation = div.animate(null, 100 * MS_PER_SEC); + animation.finish(); + animation.playbackRate = 0; + animation.currentTime = 200 * MS_PER_SEC; + assert_array_equals(div.getAnimations(), []); +}, 'Does not return animations with zero playback rate in after phase'); + +test(t => { + const div = createDiv(t); + const effect = new KeyframeEffect(div, {}, 225); + const animation = new Animation(effect, new DocumentTimeline()); + animation.reverse(); + animation.pause(); + animation.playbackRate = -1;; + animation.updatePlaybackRate(1); + assert_array_equals(div.getAnimations(), []); +}, 'Does not return an animation that has recently been made not current by setting the playback rate'); + +test(t => { + const div = createDiv(t); + const animation = div.animate(null, 100 * MS_PER_SEC); + + animation.finish(); + assert_array_equals(div.getAnimations(), [], + 'Animation should not be returned when it is finished'); + + animation.effect.updateTiming({ + duration: animation.effect.getTiming().duration + 100 * MS_PER_SEC, + }); + assert_array_equals(div.getAnimations(), [animation], + 'Animation should be returned after extending the' + + ' duration'); + + animation.effect.updateTiming({ duration: 0 }); + assert_array_equals(div.getAnimations(), [], + 'Animation should not be returned after setting the' + + ' duration to zero'); +}, 'Returns animations based on dynamic changes to individual' + + ' animations\' duration'); + +test(t => { + const div = createDiv(t); + const animation = div.animate(null, 100 * MS_PER_SEC); + + animation.effect.updateTiming({ endDelay: -200 * MS_PER_SEC }); + assert_array_equals(div.getAnimations(), [], + 'Animation should not be returned after setting a' + + ' negative end delay such that the end time is less' + + ' than the current time'); + + animation.effect.updateTiming({ endDelay: 100 * MS_PER_SEC }); + assert_array_equals(div.getAnimations(), [animation], + 'Animation should be returned after setting a positive' + + ' end delay such that the end time is more than the' + + ' current time'); +}, 'Returns animations based on dynamic changes to individual' + + ' animations\' end delay'); + +test(t => { + const div = createDiv(t); + const animation = div.animate(null, 100 * MS_PER_SEC); + + animation.finish(); + assert_array_equals(div.getAnimations(), [], + 'Animation should not be returned when it is finished'); + + animation.effect.updateTiming({ iterations: 10 }); + assert_array_equals(div.getAnimations(), [animation], + 'Animation should be returned after inreasing the' + + ' number of iterations'); + + animation.effect.updateTiming({ iterations: 0 }); + assert_array_equals(div.getAnimations(), [], + 'Animations should not be returned after setting the' + + ' iteration count to zero'); + + animation.effect.updateTiming({ iterations: Infinity }); + assert_array_equals(div.getAnimations(), [animation], + 'Animation should be returned after inreasing the' + + ' number of iterations to infinity'); +}, 'Returns animations based on dynamic changes to individual' + + ' animations\' iteration count'); + +test(t => { + const div = createDiv(t); + const animation = div.animate(null, + { duration: 100 * MS_PER_SEC, + delay: 50 * MS_PER_SEC, + endDelay: -50 * MS_PER_SEC }); + + assert_array_equals(div.getAnimations(), [animation], + 'Animation should be returned at during delay phase'); + + animation.currentTime = 50 * MS_PER_SEC; + assert_array_equals(div.getAnimations(), [animation], + 'Animation should be returned after seeking to the start' + + ' of the active interval'); + + animation.currentTime = 100 * MS_PER_SEC; + assert_array_equals(div.getAnimations(), [], + 'Animation should not be returned after seeking to the' + + ' clipped end of the active interval'); +}, 'Returns animations based on dynamic changes to individual' + + ' animations\' 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: 1, fill: 'forwards' }); + await animA.finished; + // It is not guaranteed that the mircrotask PerformCheckpoint() happens before + // the animation finish promised got resolved, because the microtask + // checkpoint could also be triggered from other source such as the event_loop + // Thus we wait for one animation frame to make sure the finished animation is + // properly removed. + await waitForNextFrame(1); + assert_array_equals(div.getAnimations(), [animB]); +}, 'Does not return an animation that has been removed'); + +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; + + animA.persist(); + + assert_array_equals(div.getAnimations(), [animA, animB]); +}, 'Returns an animation that has been persisted'); + +promise_test(async t => { + const div = createDiv(t); + const watcher = EventWatcher(t, div, 'transitionrun'); + + // Create a covering animation to prevent transitions from firing after + // calling getAnimations(). + const coveringAnimation = new Animation( + new KeyframeEffect(div, { opacity: [0, 1] }, 100 * MS_PER_SEC) + ); + + // Setup transition start point. + div.style.transition = 'opacity 100s'; + getComputedStyle(div).opacity; + + // Update specified style but don't flush style. + div.style.opacity = '0.5'; + + // Fetch animations + div.getAnimations(); + + // Play the covering animation to ensure that only the call to + // getAnimations() has a chance to trigger transitions. + coveringAnimation.play(); + + // If getAnimations() flushed style, we should get a transitionrun event. + await watcher.wait_for('transitionrun'); +}, 'Triggers a style change event'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/interfaces/Animation/cancel.html b/testing/web-platform/tests/web-animations/interfaces/Animation/cancel.html new file mode 100644 index 0000000000..a7da9755dd --- /dev/null +++ b/testing/web-platform/tests/web-animations/interfaces/Animation/cancel.html @@ -0,0 +1,133 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Animation.cancel</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#dom-animation-cancel"> +<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 div = createDiv(t); + const animation = div.animate( + { transform: ['translate(100px)', 'translate(100px)'] }, + 100 * MS_PER_SEC + ); + return animation.ready.then(() => { + assert_not_equals(getComputedStyle(div).transform, 'none', + 'transform style is animated before cancelling'); + animation.cancel(); + assert_equals(getComputedStyle(div).transform, 'none', + 'transform style is no longer animated after cancelling'); + }); +}, 'Animated style is cleared after calling Animation.cancel()'); + +test(t => { + const div = createDiv(t); + const animation = div.animate({ marginLeft: ['100px', '200px'] }, + 100 * MS_PER_SEC); + animation.effect.updateTiming({ easing: 'linear' }); + animation.cancel(); + assert_equals(getComputedStyle(div).marginLeft, '0px', + 'margin-left style is not animated after cancelling'); + + animation.currentTime = 50 * MS_PER_SEC; + assert_equals(getComputedStyle(div).marginLeft, '150px', + 'margin-left style is updated when cancelled animation is' + + ' seeked'); +}, 'After cancelling an animation, it can still be seeked'); + +promise_test(t => { + const div = createDiv(t); + const animation = div.animate({ marginLeft:['100px', '200px'] }, + 100 * MS_PER_SEC); + return animation.ready.then(() => { + animation.cancel(); + assert_equals(getComputedStyle(div).marginLeft, '0px', + 'margin-left style is not animated after cancelling'); + animation.play(); + assert_equals(getComputedStyle(div).marginLeft, '100px', + 'margin-left style is animated after re-starting animation'); + return animation.ready; + }).then(() => { + assert_equals(animation.playState, 'running', + 'Animation succeeds in running after being re-started'); + }); +}, 'After cancelling an animation, it can still be re-used'); + +promise_test(async t => { + for (const type of ["resolve", "reject"]) { + const anim = new Animation(); + + let isThenGet = false; + let isThenCalled = false; + let resolveFinished; + let rejectFinished; + const thenCalledPromise = new Promise(resolveThenCalledPromise => { + // Make `anim` thenable. + Object.defineProperty(anim, "then", { + get() { + isThenGet = true; + return function(resolve, reject) { + isThenCalled = true; + resolveThenCalledPromise(true); + resolveFinished = resolve; + rejectFinished = reject; + }; + }, + }); + }); + + // Lazily create finished promise. + const finishedPromise = anim.finished; + + assert_false(isThenGet, "then property shouldn't be accessed yet"); + + // Resolve finished promise with `anim`, that gets `then`, and + // calls in the thenable job. + anim.finish(); + + assert_true(isThenGet, "then property should be accessed"); + assert_false(isThenCalled, "then property shouldn't be called yet"); + + // Reject finished promise. + // This should be ignored. + anim.cancel(); + + // Wait for the thenable job. + await thenCalledPromise; + + assert_true(isThenCalled, "then property should be called"); + + const dummyPromise = new Promise(resolve => { + step_timeout(() => { + resolve("dummy"); + }, 100); + }); + const dummy = await Promise.race([finishedPromise, dummyPromise]); + assert_equals(dummy, "dummy", "finishedPromise shouldn't be settled yet"); + + if (type === "resolve") { + resolveFinished("hello"); + const finished = await finishedPromise; + assert_equals(finished, "hello", + "finishedPromise should be resolved with given value"); + } else { + rejectFinished("hello"); + try { + await finishedPromise; + assert_unreached("finishedPromise should be rejected") + } catch (e) { + assert_equals(e, "hello", + "finishedPromise should be rejected with given value"); + } + } + } +}, "Animation.finished promise should not be rejected by cancel method once " + + "it is resolved with inside finish method"); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/interfaces/Animation/commitStyles-crash.html b/testing/web-platform/tests/web-animations/interfaces/Animation/commitStyles-crash.html new file mode 100644 index 0000000000..063fe5a4eb --- /dev/null +++ b/testing/web-platform/tests/web-animations/interfaces/Animation/commitStyles-crash.html @@ -0,0 +1,26 @@ +<!doctype html> +<link rel=help href="https://bugzilla.mozilla.org/show_bug.cgi?id=1741491"> +<script> + class CustomElement0 extends HTMLElement { + constructor () { + super() + } + + static get observedAttributes () { return ["style"] } + + async attributeChangedCallback () { + const animation = this.animate([{ + "boxShadow": "none", + "visibility": "collapse" + }], 1957) + animation.commitStyles() + } + } + + customElements.define("custom-element-0", CustomElement0) + window.addEventListener("load", () => { + const custom = document.createElement("custom-element-0") + document.documentElement.appendChild(custom) + custom.style.fontFamily = "family_name_0" + }) +</script> diff --git a/testing/web-platform/tests/web-animations/interfaces/Animation/commitStyles-svg-crash.html b/testing/web-platform/tests/web-animations/interfaces/Animation/commitStyles-svg-crash.html new file mode 100644 index 0000000000..7fc1fef9ce --- /dev/null +++ b/testing/web-platform/tests/web-animations/interfaces/Animation/commitStyles-svg-crash.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html class=test-wait> +<link rel=help href="https://crbug.com/1385691"> +<svg id=svg></svg> +<script> + let anim = svg.animate({'svg-viewBox': '1 1 1 1'}, 1); + anim.ready.then(() => { + anim.commitStyles(); + document.documentElement.classList.remove('test-wait'); + }); +</script> +</html> diff --git a/testing/web-platform/tests/web-animations/interfaces/Animation/commitStyles.html b/testing/web-platform/tests/web-animations/interfaces/Animation/commitStyles.html new file mode 100644 index 0000000000..9a7dbea8b8 --- /dev/null +++ b/testing/web-platform/tests/web-animations/interfaces/Animation/commitStyles.html @@ -0,0 +1,577 @@ +<!doctype html> +<meta charset=utf-8> +<title>Animation.commitStyles</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#dom-animation-commitstyles"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<style> +.pseudo::before {content: '';} +.pseudo::after {content: '';} +.pseudo::marker {content: '';} +</style> +<body> +<div id="log"></div> +<script> +'use strict'; + +function assert_numeric_style_equals(opacity, expected, description) { + return assert_approx_equals( + parseFloat(opacity), + expected, + 0.0001, + description + ); +} + +test(t => { + const div = createDiv(t); + div.style.opacity = '0.1'; + + const animation = div.animate( + { opacity: 0.2 }, + { duration: 1, fill: 'forwards' } + ); + animation.finish(); + + animation.commitStyles(); + + // Cancel the animation so we can inspect the underlying style + animation.cancel(); + + assert_numeric_style_equals(getComputedStyle(div).opacity, 0.2); +}, 'Commits styles'); + +test(t => { + const div = createDiv(t); + div.style.translate = '100px'; + div.style.rotate = '45deg'; + div.style.scale = '2'; + + const animation = div.animate( + { translate: '200px', + rotate: '90deg', + scale: 3 }, + { duration: 1, fill: 'forwards' } + ); + animation.finish(); + + animation.commitStyles(); + + // Cancel the animation so we can inspect the underlying style + animation.cancel(); + + assert_equals(getComputedStyle(div).translate, '200px'); + assert_equals(getComputedStyle(div).rotate, '90deg'); + assert_numeric_style_equals(getComputedStyle(div).scale, 3); +}, 'Commits styles for individual transform properties'); + +promise_test(async t => { + const div = createDiv(t); + div.style.opacity = '0.1'; + + const animA = div.animate( + { opacity: 0.2 }, + { duration: 1, fill: 'forwards' } + ); + const animB = div.animate( + { opacity: 0.3 }, + { duration: 1, fill: 'forwards' } + ); + + await animA.finished; + + animB.cancel(); + + animA.commitStyles(); + + assert_numeric_style_equals(getComputedStyle(div).opacity, 0.2); +}, 'Commits styles for an animation that has been removed'); + +test(t => { + const div = createDiv(t); + div.style.margin = '10px'; + + const animation = div.animate( + { margin: '20px' }, + { duration: 1, fill: 'forwards' } + ); + animation.finish(); + + animation.commitStyles(); + + animation.cancel(); + + assert_equals(div.style.marginLeft, '20px'); +}, 'Commits shorthand styles'); + +test(t => { + const div = createDiv(t); + div.style.marginLeft = '10px'; + + const animation = div.animate( + { marginInlineStart: '20px' }, + { duration: 1, fill: 'forwards' } + ); + animation.finish(); + + animation.commitStyles(); + + animation.cancel(); + + assert_equals(getComputedStyle(div).marginLeft, '20px'); +}, 'Commits logical properties'); + +test(t => { + const div = createDiv(t); + div.style.marginLeft = '10px'; + + const animation = div.animate( + { marginInlineStart: '20px' }, + { duration: 1, fill: 'forwards' } + ); + animation.finish(); + + animation.commitStyles(); + + animation.cancel(); + + assert_equals(div.style.marginLeft, '20px'); +}, 'Commits logical properties as physical properties'); + +test(t => { + const div = createDiv(t); + div.style.marginLeft = '10px'; + + const animation = div.animate({ opacity: [0.2, 0.7] }, 1000); + animation.currentTime = 500; + animation.commitStyles(); + animation.cancel(); + + assert_numeric_style_equals(getComputedStyle(div).opacity, 0.45); +}, 'Commits values calculated mid-interval'); + +test(t => { + const div = createDiv(t); + div.style.setProperty('--target', '0.5'); + + const animation = div.animate( + { opacity: 'var(--target)' }, + { duration: 1, fill: 'forwards' } + ); + animation.finish(); + animation.commitStyles(); + animation.cancel(); + + assert_numeric_style_equals(getComputedStyle(div).opacity, 0.5); + + // Changes to the variable should have no effect + div.style.setProperty('--target', '1'); + + assert_numeric_style_equals(getComputedStyle(div).opacity, 0.5); +}, 'Commits variable references as their computed values'); + + +test(t => { + const div = createDiv(t); + div.style.setProperty('--target', '0.5'); + div.style.opacity = 'var(--target)'; + const animation = div.animate( + { '--target': 0.8 }, + { duration: 1, fill: 'forwards' } + ); + animation.finish(); + animation.commitStyles(); + animation.cancel(); + + assert_numeric_style_equals(getComputedStyle(div).opacity, 0.8); +}, 'Commits custom variables'); + +test(t => { + const div = createDiv(t); + div.style.fontSize = '10px'; + + const animation = div.animate( + { width: '10em' }, + { duration: 1, fill: 'forwards' } + ); + animation.finish(); + animation.commitStyles(); + animation.cancel(); + + assert_numeric_style_equals(getComputedStyle(div).width, 100); + + div.style.fontSize = '20px'; + assert_numeric_style_equals(getComputedStyle(div).width, 100, + "Changes to the font-size should have no effect"); +}, 'Commits em units as pixel values'); + +test(t => { + const div = createDiv(t); + div.style.fontSize = '10px'; + + const animation = div.animate( + { lineHeight: '1.5' }, + { duration: 1, fill: 'forwards' } + ); + animation.finish(); + animation.commitStyles(); + animation.cancel(); + + assert_numeric_style_equals(getComputedStyle(div).lineHeight, 15); + assert_equals(div.style.lineHeight, "1.5", "line-height is committed as a relative value"); + + div.style.fontSize = '20px'; + assert_numeric_style_equals(getComputedStyle(div).lineHeight, 30, + "Changes to the font-size should affect the committed line-height"); + +}, 'Commits relative line-height'); + +test(t => { + const div = createDiv(t); + const animation = div.animate( + { transform: 'translate(20px, 20px)' }, + { duration: 1, fill: 'forwards' } + ); + animation.finish(); + animation.commitStyles(); + animation.cancel(); + assert_equals(getComputedStyle(div).transform, 'matrix(1, 0, 0, 1, 20, 20)'); +}, 'Commits transforms'); + +test(t => { + const div = createDiv(t); + const animation = div.animate( + { transform: 'translate(20px, 20px)' }, + { duration: 1, fill: 'forwards' } + ); + animation.finish(); + animation.commitStyles(); + animation.cancel(); + assert_equals(div.style.transform, 'translate(20px, 20px)'); +}, 'Commits transforms as a transform list'); + +test(t => { + const div = createDiv(t); + div.style.width = '200px'; + div.style.height = '200px'; + + const animation = div.animate({ transform: ["translate(100%, 0%)", "scale(3)"] }, 1000); + animation.currentTime = 500; + animation.commitStyles(); + animation.cancel(); + + // TODO(https://github.com/w3c/csswg-drafts/issues/2854): + // We can't check the committed value directly since it is not specced yet in this case, + // but it should still produce the correct resolved value. + assert_equals(getComputedStyle(div).transform, "matrix(2, 0, 0, 2, 100, 0)", + "Resolved transform is correct after commit."); +}, 'Commits matrix-interpolated relative transforms'); + +test(t => { + const div = createDiv(t); + div.style.width = '200px'; + div.style.height = '200px'; + + const animation = div.animate({ transform: ["none", "none"] }, 1000); + animation.currentTime = 500; + animation.commitStyles(); + animation.cancel(); + + assert_equals(div.style.transform, "none", + "Resolved transform is correct after commit."); +}, 'Commits "none" transform'); + +promise_test(async t => { + const div = createDiv(t); + div.style.opacity = '0.1'; + + const animA = div.animate( + { opacity: '0.2' }, + { duration: 1, fill: 'forwards' } + ); + const animB = div.animate( + { opacity: '0.2', composite: 'add' }, + { duration: 1, fill: 'forwards' } + ); + const animC = div.animate( + { opacity: '0.3', composite: 'add' }, + { duration: 1, fill: 'forwards' } + ); + + animA.persist(); + animB.persist(); + + await animB.finished; + + // The values above have been chosen such that various error conditions + // produce results that all differ from the desired result: + // + // Expected result: + // + // animA + animB = 0.4 + // + // Likely error results: + // + // <underlying> = 0.1 + // (Commit didn't work at all) + // + // animB = 0.2 + // (Didn't add at all when resolving) + // + // <underlying> + animB = 0.3 + // (Added to the underlying value instead of lower-priority animations when + // resolving) + // + // <underlying> + animA + animB = 0.5 + // (Didn't respect the composite mode of lower-priority animations) + // + // animA + animB + animC = 0.7 + // (Resolved the whole stack, not just up to the target effect) + // + + animB.commitStyles(); + + animA.cancel(); + animB.cancel(); + animC.cancel(); + + assert_numeric_style_equals(getComputedStyle(div).opacity, 0.4); +}, 'Commits the intermediate value of an animation in the middle of stack'); + +promise_test(async t => { + const div = createDiv(t); + div.style.opacity = '0.1'; + + const animA = div.animate( + { opacity: '0.2', composite: 'add' }, + { duration: 1, fill: 'forwards' } + ); + const animB = div.animate( + { opacity: '0.2', composite: 'add' }, + { duration: 1, fill: 'forwards' } + ); + const animC = div.animate( + { opacity: '0.3', composite: 'add' }, + { duration: 1, fill: 'forwards' } + ); + + animA.persist(); + animB.persist(); + await animB.finished; + + // The error cases are similar to the above test with one additional case; + // verifying that the animations composite on top of the correct underlying + // base style. + // + // Expected result: + // + // <underlying> + animA + animB = 0.5 + // + // Additional error results: + // + // <underlying> + animA + animB + animC + animA + animB = 1.0 (saturates) + // (Added to the computed value instead of underlying value when + // resolving) + // + // animA + animB = 0.4 + // Failed to composite on top of underlying value. + // + + animB.commitStyles(); + + animA.cancel(); + animB.cancel(); + animC.cancel(); + + assert_numeric_style_equals(getComputedStyle(div).opacity, 0.5); +}, 'Commit composites on top of the underlying value'); + +promise_test(async t => { + const div = createDiv(t); + div.style.opacity = '0.1'; + + // Setup animation + const animation = div.animate( + { opacity: 0.2 }, + { duration: 1, fill: 'forwards' } + ); + animation.finish(); + + // Setup observer + const mutationRecords = []; + const observer = new MutationObserver(mutations => { + mutationRecords.push(...mutations); + }); + observer.observe(div, { attributes: true, attributeOldValue: true }); + + animation.commitStyles(); + + // Wait for mutation records to be dispatched + await Promise.resolve(); + + assert_equals(mutationRecords.length, 1, 'Should have one mutation record'); + + const mutation = mutationRecords[0]; + assert_equals(mutation.type, 'attributes'); + assert_equals(mutation.oldValue, 'opacity: 0.1;'); + + observer.disconnect(); +}, 'Triggers mutation observers when updating style'); + +promise_test(async t => { + const div = createDiv(t); + div.style.opacity = '0.2'; + + // Setup animation + const animation = div.animate( + { opacity: 0.2 }, + { duration: 1, fill: 'forwards' } + ); + animation.finish(); + + // Setup observer + const mutationRecords = []; + const observer = new MutationObserver(mutations => { + mutationRecords.push(...mutations); + }); + observer.observe(div, { attributes: true }); + + animation.commitStyles(); + + // Wait for mutation records to be dispatched + await Promise.resolve(); + + assert_equals(mutationRecords.length, 0, 'Should have no mutation records'); + + observer.disconnect(); +}, 'Does NOT trigger mutation observers when the change to style is redundant'); + +test(t => { + + const div = createDiv(t); + div.classList.add('pseudo'); + const animation = div.animate( + { opacity: 0 }, + { duration: 1, fill: 'forwards', pseudoElement: '::before' } + ); + + assert_throws_dom('NoModificationAllowedError', () => { + animation.commitStyles(); + }); +}, 'Throws if the target element is a pseudo element'); + +test(t => { + const animation = createDiv(t).animate( + { opacity: 0 }, + { duration: 1, fill: 'forwards' } + ); + + const nonStyleElement + = document.createElementNS('http://example.org/test', 'test'); + document.body.appendChild(nonStyleElement); + animation.effect.target = nonStyleElement; + + assert_throws_dom('NoModificationAllowedError', () => { + animation.commitStyles(); + }); + + nonStyleElement.remove(); +}, 'Throws if the target element is not something with a style attribute'); + +test(t => { + const div = createDiv(t); + const animation = div.animate( + { opacity: 0 }, + { duration: 1, fill: 'forwards' } + ); + + div.style.display = 'none'; + + assert_throws_dom('InvalidStateError', () => { + animation.commitStyles(); + }); +}, 'Throws if the target effect is display:none'); + +test(t => { + const container = createDiv(t); + const div = createDiv(t); + container.append(div); + + const animation = div.animate( + { opacity: 0 }, + { duration: 1, fill: 'forwards' } + ); + + container.style.display = 'none'; + + assert_throws_dom('InvalidStateError', () => { + animation.commitStyles(); + }); +}, "Throws if the target effect's ancestor is display:none"); + +test(t => { + const container = createDiv(t); + const div = createDiv(t); + container.append(div); + + const animation = div.animate( + { opacity: 0 }, + { duration: 1, fill: 'forwards' } + ); + + container.style.display = 'contents'; + + // Should NOT throw + animation.commitStyles(); +}, 'Treats display:contents as rendered'); + +test(t => { + const container = createDiv(t); + const div = createDiv(t); + container.append(div); + + const animation = div.animate( + { opacity: 0 }, + { duration: 1, fill: 'forwards' } + ); + + div.style.display = 'contents'; + container.style.display = 'none'; + + assert_throws_dom('InvalidStateError', () => { + animation.commitStyles(); + }); +}, 'Treats display:contents in a display:none subtree as not rendered'); + +test(t => { + const div = createDiv(t); + const animation = div.animate( + { opacity: 0 }, + { duration: 1, fill: 'forwards' } + ); + + div.remove(); + + assert_throws_dom('InvalidStateError', () => { + animation.commitStyles(); + }); +}, 'Throws if the target effect is disconnected'); + +test(t => { + const div = createDiv(t); + div.classList.add('pseudo'); + const animation = div.animate( + { opacity: 0 }, + { duration: 1, fill: 'forwards', pseudoElement: '::before' } + ); + + div.remove(); + + assert_throws_dom('NoModificationAllowedError', () => { + animation.commitStyles(); + }); +}, 'Checks the pseudo element condition before the not rendered condition'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/interfaces/Animation/constructor.html b/testing/web-platform/tests/web-animations/interfaces/Animation/constructor.html new file mode 100644 index 0000000000..d599fd72ea --- /dev/null +++ b/testing/web-platform/tests/web-animations/interfaces/Animation/constructor.html @@ -0,0 +1,113 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Animation constructor</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#dom-animation-animation"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<body> +<div id="log"></div> +<div id="target"></div> +<script> +'use strict'; + +const gTarget = document.getElementById('target'); + +function createEffect() { + return new KeyframeEffect(gTarget, { opacity: [0, 1] }); +} + +function createNull() { + return null; +} + +const gTestArguments = [ + { + createEffect: createNull, + timeline: null, + expectedTimeline: null, + expectedTimelineDescription: 'null', + description: 'with null effect and null timeline' + }, + { + createEffect: createNull, + timeline: document.timeline, + expectedTimeline: document.timeline, + expectedTimelineDescription: 'document.timeline', + description: 'with null effect and non-null timeline' + }, + { + createEffect: createNull, + expectedTimeline: document.timeline, + expectedTimelineDescription: 'document.timeline', + description: 'with null effect and no timeline parameter' + }, + { + createEffect: createEffect, + timeline: null, + expectedTimeline: null, + expectedTimelineDescription: 'null', + description: 'with non-null effect and null timeline' + }, + { + createEffect: createEffect, + timeline: document.timeline, + expectedTimeline: document.timeline, + expectedTimelineDescription: 'document.timeline', + description: 'with non-null effect and non-null timeline' + }, + { + createEffect: createEffect, + expectedTimeline: document.timeline, + expectedTimelineDescription: 'document.timeline', + description: 'with non-null effect and no timeline parameter' + }, +]; + +for (const args of gTestArguments) { + test(t => { + const effect = args.createEffect(); + const animation = new Animation(effect, args.timeline); + + assert_not_equals(animation, null, + 'An animation sohuld be created'); + assert_equals(animation.effect, effect, + 'Animation returns the same effect passed to ' + + 'the constructor'); + assert_equals(animation.timeline, args.expectedTimeline, + 'Animation timeline should be ' + args.expectedTimelineDescription); + assert_equals(animation.playState, 'idle', + 'Animation.playState should be initially \'idle\''); + }, 'Animation can be constructed ' + args.description); +} + +test(t => { + const effect = new KeyframeEffect(null, + { left: ['10px', '20px'] }, + { duration: 10000, fill: 'forwards' }); + const anim = new Animation(effect, document.timeline); + anim.pause(); + assert_equals(effect.getComputedTiming().progress, 0.0); + anim.currentTime += 5000; + assert_equals(effect.getComputedTiming().progress, 0.5); + anim.finish(); + assert_equals(effect.getComputedTiming().progress, 1.0); +}, 'Animation constructed by an effect with null target runs normally'); + +async_test(t => { + const iframe = document.createElement('iframe'); + + iframe.addEventListener('load', t.step_func(() => { + const div = createDiv(t, iframe.contentDocument); + const effect = new KeyframeEffect(div, null, 10000); + const anim = new Animation(effect); + assert_equals(anim.timeline, document.timeline); + iframe.remove(); + t.done(); + })); + + document.body.appendChild(iframe); +}, 'Animation constructed with a keyframe that target element is in iframe'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/interfaces/Animation/effect.html b/testing/web-platform/tests/web-animations/interfaces/Animation/effect.html new file mode 100644 index 0000000000..cb8bc09c36 --- /dev/null +++ b/testing/web-platform/tests/web-animations/interfaces/Animation/effect.html @@ -0,0 +1,42 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Animation.effect</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#dom-animation-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'; + +test(t => { + const anim = new Animation(); + assert_equals(anim.effect, null, 'initial effect is null'); + + const newEffect = new KeyframeEffect(createDiv(t), null); + anim.effect = newEffect; + assert_equals(anim.effect, newEffect, 'new effect is set'); +}, 'effect is set correctly.'); + +test(t => { + const div = createDiv(t); + const animation = div.animate({ left: ['100px', '100px'] }, + { fill: 'forwards' }); + const effect = animation.effect; + + assert_equals(getComputedStyle(div).left, '100px', + 'animation is initially having an effect'); + + animation.effect = null; + assert_equals(getComputedStyle(div).left, 'auto', + 'animation no longer has an effect'); + + animation.effect = effect; + assert_equals(getComputedStyle(div).left, '100px', + 'animation has an effect again'); +}, 'Clearing and setting Animation.effect should update the computed style' + + ' of the target element'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/interfaces/Animation/finished.html b/testing/web-platform/tests/web-animations/interfaces/Animation/finished.html new file mode 100644 index 0000000000..bee4fd8fb7 --- /dev/null +++ b/testing/web-platform/tests/web-animations/interfaces/Animation/finished.html @@ -0,0 +1,416 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Animation.finished</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#dom-animation-finished"> +<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 div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + const previousFinishedPromise = animation.finished; + return animation.ready.then(() => { + assert_equals(animation.finished, previousFinishedPromise, + 'Finished promise is the same object when playing starts'); + animation.pause(); + assert_equals(animation.finished, previousFinishedPromise, + 'Finished promise does not change when pausing'); + animation.play(); + assert_equals(animation.finished, previousFinishedPromise, + 'Finished promise does not change when play() unpauses'); + + animation.currentTime = 100 * MS_PER_SEC; + + return animation.finished; + }).then(() => { + assert_equals(animation.finished, previousFinishedPromise, + 'Finished promise is the same object when playing completes'); + }); +}, 'Test pausing then playing does not change the finished promise'); + +promise_test(t => { + const div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + let previousFinishedPromise = animation.finished; + animation.finish(); + return animation.finished.then(() => { + assert_equals(animation.finished, previousFinishedPromise, + 'Finished promise is the same object when playing completes'); + animation.play(); + assert_not_equals(animation.finished, previousFinishedPromise, + 'Finished promise changes when replaying animation'); + + previousFinishedPromise = animation.finished; + animation.play(); + assert_equals(animation.finished, previousFinishedPromise, + 'Finished promise is the same after redundant play() call'); + + }); +}, 'Test restarting a finished animation'); + +promise_test(t => { + const div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + let previousFinishedPromise; + animation.finish(); + return animation.finished.then(() => { + previousFinishedPromise = animation.finished; + animation.playbackRate = -1; + assert_not_equals(animation.finished, previousFinishedPromise, + 'Finished promise should be replaced when reversing a ' + + 'finished promise'); + animation.currentTime = 0; + return animation.finished; + }).then(() => { + previousFinishedPromise = animation.finished; + animation.play(); + assert_not_equals(animation.finished, previousFinishedPromise, + 'Finished promise is replaced after play() call on ' + + 'finished, reversed animation'); + }); +}, 'Test restarting a reversed finished animation'); + +promise_test(t => { + const div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + const previousFinishedPromise = animation.finished; + animation.finish(); + return animation.finished.then(() => { + animation.currentTime = 100 * MS_PER_SEC + 1000; + assert_equals(animation.finished, previousFinishedPromise, + 'Finished promise is unchanged jumping past end of ' + + 'finished animation'); + }); +}, 'Test redundant finishing of animation'); + +promise_test(t => { + const div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + // Setup callback to run if finished promise is resolved + let finishPromiseResolved = false; + animation.finished.then(() => { + finishPromiseResolved = true; + }); + return animation.ready.then(() => { + // Jump to mid-way in interval and pause + animation.currentTime = 100 * MS_PER_SEC / 2; + animation.pause(); + return animation.ready; + }).then(() => { + // Jump to the end + // (But don't use finish() since that should unpause as well) + animation.currentTime = 100 * MS_PER_SEC; + return waitForAnimationFrames(2); + }).then(() => { + assert_false(finishPromiseResolved, + 'Finished promise should not resolve when paused'); + }); +}, 'Finished promise does not resolve when paused'); + +promise_test(t => { + const div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + // Setup callback to run if finished promise is resolved + let finishPromiseResolved = false; + animation.finished.then(() => { + finishPromiseResolved = true; + }); + return animation.ready.then(() => { + // Jump to mid-way in interval and pause + animation.currentTime = 100 * MS_PER_SEC / 2; + animation.pause(); + // Jump to the end + animation.currentTime = 100 * MS_PER_SEC; + return waitForAnimationFrames(2); + }).then(() => { + assert_false(finishPromiseResolved, + 'Finished promise should not resolve when pause-pending'); + }); +}, 'Finished promise does not resolve when pause-pending'); + +promise_test(t => { + const div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + animation.finish(); + return animation.finished.then(resolvedAnimation => { + assert_equals(resolvedAnimation, animation, + 'Object identity of animation passed to Promise callback' + + ' matches the animation object owning the Promise'); + }); +}, 'The finished promise is fulfilled with its Animation'); + +promise_test(t => { + const div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + const previousFinishedPromise = animation.finished; + + // Set up listeners on finished promise + const retPromise = animation.finished.then(() => { + assert_unreached('finished promise was fulfilled'); + }).catch(err => { + assert_equals(err.name, 'AbortError', + 'finished promise is rejected with AbortError'); + assert_not_equals(animation.finished, previousFinishedPromise, + 'Finished promise should change after the original is ' + + 'rejected'); + }); + + animation.cancel(); + + return retPromise; +}, 'finished promise is rejected when an animation is canceled by calling ' + + 'cancel()'); + +promise_test(t => { + const div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + const previousFinishedPromise = animation.finished; + animation.finish(); + return animation.finished.then(() => { + animation.cancel(); + assert_not_equals(animation.finished, previousFinishedPromise, + 'A new finished promise should be created when' + + ' canceling a finished animation'); + }); +}, 'canceling an already-finished animation replaces the finished promise'); + +promise_test(t => { + const div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + const HALF_DUR = 100 * MS_PER_SEC / 2; + const QUARTER_DUR = 100 * MS_PER_SEC / 4; + let gotNextFrame = false; + let currentTimeBeforeShortening; + animation.currentTime = HALF_DUR; + return animation.ready.then(() => { + currentTimeBeforeShortening = animation.currentTime; + animation.effect.updateTiming({ duration: QUARTER_DUR }); + // Below we use gotNextFrame to check that shortening of the animation + // duration causes the finished promise to resolve, rather than it just + // getting resolved on the next animation frame. This relies on the fact + // that the promises are resolved as a micro-task before the next frame + // happens. + waitForAnimationFrames(1).then(() => { + gotNextFrame = true; + }); + + return animation.finished; + }).then(() => { + assert_false(gotNextFrame, 'shortening of the animation duration should ' + + 'resolve the finished promise'); + assert_equals(animation.currentTime, currentTimeBeforeShortening, + 'currentTime should be unchanged when duration shortened'); + const previousFinishedPromise = animation.finished; + animation.effect.updateTiming({ duration: 100 * MS_PER_SEC }); + assert_not_equals(animation.finished, previousFinishedPromise, + 'Finished promise should change after lengthening the ' + + 'duration causes the animation to become active'); + }); +}, 'Test finished promise changes for animation duration changes'); + +promise_test(t => { + const div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + const retPromise = animation.ready.then(() => { + animation.playbackRate = 0; + animation.currentTime = 100 * MS_PER_SEC + 1000; + return waitForAnimationFrames(2); + }); + + animation.finished.then(t.step_func(() => { + assert_unreached('finished promise should not resolve when playbackRate ' + + 'is zero'); + })); + + return retPromise; +}, 'Test finished promise changes when playbackRate == 0'); + +promise_test(t => { + const div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + return animation.ready.then(() => { + animation.playbackRate = -1; + return animation.finished; + }); +}, 'Test finished promise resolves when reaching to the natural boundary.'); + +promise_test(t => { + const div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + const previousFinishedPromise = animation.finished; + animation.finish(); + return animation.finished.then(() => { + animation.currentTime = 0; + assert_not_equals(animation.finished, previousFinishedPromise, + 'Finished promise should change once a prior ' + + 'finished promise resolved and the animation ' + + 'falls out finished state'); + }); +}, 'Test finished promise changes when a prior finished promise resolved ' + + 'and the animation falls out finished state'); + +test(t => { + const div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + const previousFinishedPromise = animation.finished; + animation.currentTime = 100 * MS_PER_SEC; + animation.currentTime = 100 * MS_PER_SEC / 2; + assert_equals(animation.finished, previousFinishedPromise, + 'No new finished promise generated when finished state ' + + 'is checked asynchronously'); +}, 'Test no new finished promise generated when finished state ' + + 'is checked asynchronously'); + +test(t => { + const div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + const previousFinishedPromise = animation.finished; + animation.finish(); + animation.currentTime = 100 * MS_PER_SEC / 2; + assert_not_equals(animation.finished, previousFinishedPromise, + 'New finished promise generated when finished state ' + + 'is checked synchronously'); +}, 'Test new finished promise generated when finished state ' + + 'is checked synchronously'); + +promise_test(t => { + const div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + let resolvedFinished = false; + animation.finished.then(() => { + resolvedFinished = true; + }); + return animation.ready.then(() => { + animation.finish(); + animation.currentTime = 100 * MS_PER_SEC / 2; + }).then(() => { + assert_true(resolvedFinished, + 'Animation.finished should be resolved even if ' + + 'the finished state is changed soon'); + }); + +}, 'Test synchronous finished promise resolved even if finished state ' + + 'is changed soon'); + +promise_test(t => { + const div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + let resolvedFinished = false; + animation.finished.then(() => { + resolvedFinished = true; + }); + + return animation.ready.then(() => { + animation.currentTime = 100 * MS_PER_SEC; + animation.finish(); + }).then(() => { + assert_true(resolvedFinished, + 'Animation.finished should be resolved soon after finish() is ' + + 'called even if there are other asynchronous promises just before it'); + }); +}, 'Test synchronous finished promise resolved even if asynchronous ' + + 'finished promise happens just before synchronous promise'); + +promise_test(t => { + const div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + animation.finished.then(t.step_func(() => { + assert_unreached('Animation.finished should not be resolved'); + })); + + return animation.ready.then(() => { + animation.currentTime = 100 * MS_PER_SEC; + animation.currentTime = 100 * MS_PER_SEC / 2; + }); +}, 'Test finished promise is not resolved when the animation ' + + 'falls out finished state immediately'); + +promise_test(t => { + const div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + return animation.ready.then(() => { + animation.currentTime = 100 * MS_PER_SEC; + animation.finished.then(t.step_func(() => { + assert_unreached('Animation.finished should not be resolved'); + })); + animation.currentTime = 0; + }); + +}, 'Test finished promise is not resolved once the animation ' + + 'falls out finished state even though the current finished ' + + 'promise is generated soon after animation state became finished'); + +promise_test(t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + let ready = false; + animation.ready.then( + t.step_func(() => { + ready = true; + }), + t.unreached_func('Ready promise must not be rejected') + ); + + const testSuccess = animation.finished.then( + t.step_func(() => { + assert_true(ready, 'Ready promise has resolved'); + }), + t.unreached_func('Finished promise must not be rejected') + ); + + const timeout = waitForAnimationFrames(3).then(() => { + return Promise.reject('Finished promise did not arrive in time'); + }); + + animation.finish(); + return Promise.race([timeout, testSuccess]); +}, 'Finished promise should be resolved after the ready promise is resolved'); + +promise_test(t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + let caught = false; + animation.ready.then( + t.unreached_func('Ready promise must not be resolved'), + t.step_func(() => { + caught = true; + }) + ); + + const testSuccess = animation.finished.then( + t.unreached_func('Finished promise must not be resolved'), + t.step_func(() => { + assert_true(caught, 'Ready promise has been rejected'); + }) + ); + + const timeout = waitForAnimationFrames(3).then(() => { + return Promise.reject('Finished promise was not rejected in time'); + }); + + animation.cancel(); + return Promise.race([timeout, testSuccess]); +}, 'Finished promise should be rejected after the ready promise is rejected'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + + // Ensure the finished promise is created + const finished = animation.finished; + + window.addEventListener( + 'unhandledrejection', + t.unreached_func('Should not get an unhandled rejection') + ); + + animation.cancel(); + + // Wait a moment to allow a chance for the event to be dispatched. + await waitForAnimationFrames(2); +}, 'Finished promise does not report an unhandledrejection when rejected'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/interfaces/Animation/id.html b/testing/web-platform/tests/web-animations/interfaces/Animation/id.html new file mode 100644 index 0000000000..5b9586bfaf --- /dev/null +++ b/testing/web-platform/tests/web-animations/interfaces/Animation/id.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Animation.id</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#dom-animation-id"> +<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 div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + assert_equals(animation.id, '', 'id for Animation is initially empty'); +}, 'Animation.id initial value'); + +test(t => { + const div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + animation.id = 'anim'; + + assert_equals(animation.id, 'anim', 'animation.id reflects the value set'); +}, 'Animation.id setter'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/interfaces/Animation/oncancel.html b/testing/web-platform/tests/web-animations/interfaces/Animation/oncancel.html new file mode 100644 index 0000000000..d539119609 --- /dev/null +++ b/testing/web-platform/tests/web-animations/interfaces/Animation/oncancel.html @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Animation.oncancel</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#dom-animation-oncancel"> +<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'; + +async_test(t => { + const div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + let finishedTimelineTime; + animation.finished.then().catch(() => { + finishedTimelineTime = animation.timeline.currentTime; + }); + + animation.oncancel = t.step_func_done(event => { + assert_equals(event.currentTime, null, + 'event.currentTime should be null'); + assert_times_equal(event.timelineTime, finishedTimelineTime, + 'event.timelineTime should equal to the animation timeline ' + + 'when finished promise is rejected'); + }); + + animation.cancel(); +}, 'oncancel event is fired when animation.cancel() is called.'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/interfaces/Animation/onfinish.html b/testing/web-platform/tests/web-animations/interfaces/Animation/onfinish.html new file mode 100644 index 0000000000..b58fea0362 --- /dev/null +++ b/testing/web-platform/tests/web-animations/interfaces/Animation/onfinish.html @@ -0,0 +1,119 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Animation.onfinish</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#dom-animation-onfinish"> +<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'; + +async_test(t => { + const div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + let finishedTimelineTime; + animation.finished.then(() => { + finishedTimelineTime = animation.timeline.currentTime; + }); + + animation.onfinish = t.step_func_done(event => { + assert_equals(event.currentTime, 0, + 'event.currentTime should be zero'); + assert_times_equal(event.timelineTime, finishedTimelineTime, + 'event.timelineTime should equal to the animation timeline ' + + 'when finished promise is resolved'); + }); + + animation.playbackRate = -1; +}, 'onfinish event is fired when the currentTime < 0 and ' + + 'the playbackRate < 0'); + +async_test(t => { + const div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + + let finishedTimelineTime; + animation.finished.then(() => { + finishedTimelineTime = animation.timeline.currentTime; + }); + + animation.onfinish = t.step_func_done(event => { + assert_times_equal(event.currentTime, 100 * MS_PER_SEC, + 'event.currentTime should be the effect end'); + assert_times_equal(event.timelineTime, finishedTimelineTime, + 'event.timelineTime should equal to the animation timeline ' + + 'when finished promise is resolved'); + }); + + animation.currentTime = 100 * MS_PER_SEC; +}, 'onfinish event is fired when the currentTime > 0 and ' + + 'the playbackRate > 0'); + +async_test(t => { + const div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + + let finishedTimelineTime; + animation.finished.then(() => { + finishedTimelineTime = animation.timeline.currentTime; + }); + + animation.onfinish = t.step_func_done(event => { + assert_times_equal(event.currentTime, 100 * MS_PER_SEC, + 'event.currentTime should be the effect end'); + assert_times_equal(event.timelineTime, finishedTimelineTime, + 'event.timelineTime should equal to the animation timeline ' + + 'when finished promise is resolved'); + }); + + animation.finish(); +}, 'onfinish event is fired when animation.finish() is called'); + +promise_test(async t => { + const div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + + animation.onfinish = event => { + assert_unreached('onfinish event should not be fired'); + }; + + animation.currentTime = 100 * MS_PER_SEC / 2; + animation.pause(); + + await animation.ready; + animation.currentTime = 100 * MS_PER_SEC; + await waitForAnimationFrames(2); +}, 'onfinish event is not fired when paused'); + +promise_test(async t => { + const div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + animation.onfinish = event => { + assert_unreached('onfinish event should not be fired'); + }; + + await animation.ready; + animation.playbackRate = 0; + animation.currentTime = 100 * MS_PER_SEC; + await waitForAnimationFrames(2); +}, 'onfinish event is not fired when the playbackRate is zero'); + +promise_test(async t => { + const div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + + animation.onfinish = event => { + assert_unreached('onfinish event should not be fired'); + }; + + await animation.ready; + animation.currentTime = 100 * MS_PER_SEC; + animation.currentTime = 100 * MS_PER_SEC / 2; + await waitForAnimationFrames(2); +}, 'onfinish event is not fired when the animation falls out ' + + 'finished state immediately'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/interfaces/Animation/onremove.html b/testing/web-platform/tests/web-animations/interfaces/Animation/onremove.html new file mode 100644 index 0000000000..1a41a3d21c --- /dev/null +++ b/testing/web-platform/tests/web-animations/interfaces/Animation/onremove.html @@ -0,0 +1,58 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Animation.onremove</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#dom-animation-onremove"> +<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'; + +async_test(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' }); + + let finishedTimelineTime = null; + animB.onfinish = event => { + finishedTimelineTime = event.timelineTime; + }; + + animA.onremove = t.step_func_done(event => { + assert_equals(animA.replaceState, 'removed'); + assert_equals(event.currentTime, 1); + assert_true(finishedTimelineTime != null, 'finished event fired'); + assert_equals(event.timelineTime, finishedTimelineTime, + 'timeline time is set'); + }); + +}, 'onremove event is fired when replaced animation is removed.'); + +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 animC = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + const animD = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + + const removed = []; + + animA.onremove = () => { removed.push('A'); }; + animB.onremove = () => { removed.push('B'); }; + animC.onremove = () => { removed.push('C'); }; + + animD.onremove = event => { + assert_unreached('onremove event should not be fired'); + }; + + await waitForAnimationFrames(2); + + assert_equals(removed.join(''), 'ABC'); + +}, 'onremove events are fired in the correct order'); + +</script> +</body> + diff --git a/testing/web-platform/tests/web-animations/interfaces/Animation/pause.html b/testing/web-platform/tests/web-animations/interfaces/Animation/pause.html new file mode 100644 index 0000000000..1d1bd5fd89 --- /dev/null +++ b/testing/web-platform/tests/web-animations/interfaces/Animation/pause.html @@ -0,0 +1,98 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Animation.pause</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#dom-animation-pause"> +<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 div = createDiv(t); + const animation = div.animate({}, 1000 * MS_PER_SEC); + let previousCurrentTime = animation.currentTime; + + return animation.ready.then(waitForAnimationFrames(1)).then(() => { + assert_true(animation.currentTime >= previousCurrentTime, + 'currentTime is initially increasing'); + animation.pause(); + return animation.ready; + }).then(() => { + previousCurrentTime = animation.currentTime; + return waitForAnimationFrames(1); + }).then(() => { + assert_equals(animation.currentTime, previousCurrentTime, + 'currentTime does not increase after calling pause()'); + }); +}, 'pause() a running animation'); + +promise_test(t => { + const div = createDiv(t); + const animation = div.animate({}, 1000 * MS_PER_SEC); + + // Go to idle state then pause + animation.cancel(); + animation.pause(); + + assert_equals(animation.currentTime, 0, 'currentTime is set to 0'); + assert_equals(animation.startTime, null, 'startTime is not set'); + assert_equals(animation.playState, 'paused', 'in paused play state'); + assert_true(animation.pending, 'initially pause-pending'); + + // Check it still resolves as expected + return animation.ready.then(() => { + assert_false(animation.pending, 'no longer pending'); + assert_equals(animation.currentTime, 0, + 'keeps the initially set currentTime'); + }); +}, 'pause() from idle'); + +promise_test(t => { + const div = createDiv(t); + const animation = div.animate({}, 1000 * MS_PER_SEC); + animation.cancel(); + animation.playbackRate = -1; + animation.pause(); + + assert_equals(animation.currentTime, 1000 * MS_PER_SEC, + 'currentTime is set to the effect end'); + + return animation.ready.then(() => { + assert_equals(animation.currentTime, 1000 * MS_PER_SEC, + 'keeps the initially set currentTime'); + }); +}, 'pause() from idle with a negative playbackRate'); + +test(t => { + const div = createDiv(t); + const animation = div.animate({}, {duration: 1000 * MS_PER_SEC, + iterations: Infinity}); + animation.cancel(); + animation.playbackRate = -1; + + assert_throws_dom('InvalidStateError', + () => { animation.pause(); }, + 'Expect InvalidStateError exception on calling pause() ' + + 'from idle with a negative playbackRate and ' + + 'infinite-duration animation'); +}, 'pause() from idle with a negative playbackRate and endless effect'); + +promise_test(t => { + const div = createDiv(t); + const animation = div.animate({}, 1000 * MS_PER_SEC); + return animation.ready + .then(animation => { + animation.finish(); + animation.pause(); + return animation.ready; + }).then(animation => { + assert_equals(animation.currentTime, 1000 * MS_PER_SEC, + 'currentTime after pausing finished animation'); + }); +}, 'pause() on a finished animation'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/interfaces/Animation/pending.html b/testing/web-platform/tests/web-animations/interfaces/Animation/pending.html new file mode 100644 index 0000000000..c200f9e977 --- /dev/null +++ b/testing/web-platform/tests/web-animations/interfaces/Animation/pending.html @@ -0,0 +1,55 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Animation.pending</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#dom-animation-pending"> +<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 div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + + assert_true(animation.pending); + return animation.ready.then(() => { + assert_false(animation.pending); + }); +}, 'reports true -> false when initially played'); + +promise_test(t => { + const div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + animation.pause(); + + assert_true(animation.pending); + return animation.ready.then(() => { + assert_false(animation.pending); + }); +}, 'reports true -> false when paused'); + +promise_test(async t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + null); + animation.play(); + assert_true(animation.pending); + await waitForAnimationFrames(2); + assert_true(animation.pending); +}, 'reports true -> true when played without a timeline'); + +promise_test(async t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + null); + animation.pause(); + assert_true(animation.pending); + await waitForAnimationFrames(2); + assert_true(animation.pending); +}, 'reports true -> true when paused without a timeline'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/interfaces/Animation/persist.html b/testing/web-platform/tests/web-animations/interfaces/Animation/persist.html new file mode 100644 index 0000000000..c18993cbc4 --- /dev/null +++ b/testing/web-platform/tests/web-animations/interfaces/Animation/persist.html @@ -0,0 +1,40 @@ +<!doctype html> +<meta charset=utf-8> +<title>Animation.persist</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#dom-animation-persist"> +<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'; + +async_test(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' }); + + animA.onremove = t.step_func_done(() => { + assert_equals(animA.replaceState, 'removed'); + animA.persist(); + assert_equals(animA.replaceState, 'persisted'); + }); +}, 'Allows an animation to be persisted after being removed'); + +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' }); + + animA.persist(); + + await animA.finished; + + assert_equals(animA.replaceState, 'persisted'); +}, 'Allows an animation to be persisted before being removed'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/interfaces/Animation/play.html b/testing/web-platform/tests/web-animations/interfaces/Animation/play.html new file mode 100644 index 0000000000..6c5d604b1e --- /dev/null +++ b/testing/web-platform/tests/web-animations/interfaces/Animation/play.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Animation.play</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#dom-animation-play"> +<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 div = createDiv(t); + const animation = div.animate({ transform: ['none', 'translate(10px)']}, + { duration: 100 * MS_PER_SEC, + iterations: Infinity }); + return animation.ready.then(() => { + // Seek to a time outside the active range so that play() will have to + // snap back to the start + animation.currentTime = -5 * MS_PER_SEC; + animation.playbackRate = -1; + + assert_throws_dom('InvalidStateError', + () => { animation.play(); }, + 'Expected InvalidStateError exception on calling play() ' + + 'with a negative playbackRate and infinite-duration ' + + 'animation'); + }); +}, 'play() throws when seeking an infinite-duration animation played in ' + + 'reverse'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/interfaces/Animation/ready.html b/testing/web-platform/tests/web-animations/interfaces/Animation/ready.html new file mode 100644 index 0000000000..462e2a0484 --- /dev/null +++ b/testing/web-platform/tests/web-animations/interfaces/Animation/ready.html @@ -0,0 +1,78 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Animation.ready</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#dom-animation-ready"> +<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 div = createDiv(t); + const animation = div.animate(null, 100 * MS_PER_SEC); + const originalReadyPromise = animation.ready; + let pauseReadyPromise; + + return animation.ready.then(() => { + assert_equals(animation.ready, originalReadyPromise, + 'Ready promise is the same object when playing completes'); + animation.pause(); + assert_not_equals(animation.ready, originalReadyPromise, + 'A new ready promise is created when pausing'); + pauseReadyPromise = animation.ready; + // Wait for the promise to fulfill since if we abort the pause the ready + // promise object is reused. + return animation.ready; + }).then(() => { + animation.play(); + assert_not_equals(animation.ready, pauseReadyPromise, + 'A new ready promise is created when playing'); + }); +}, 'A new ready promise is created when play()/pause() is called'); + +promise_test(t => { + const div = createDiv(t); + const animation = div.animate(null, 100 * MS_PER_SEC); + + return animation.ready.then(() => { + const promiseBeforeCallingPlay = animation.ready; + animation.play(); + assert_equals(animation.ready, promiseBeforeCallingPlay, + 'Ready promise has same object identity after redundant call' + + ' to play()'); + }); +}, 'Redundant calls to play() do not generate new ready promise objects'); + +promise_test(t => { + const div = createDiv(t); + const animation = div.animate(null, 100 * MS_PER_SEC); + + return animation.ready.then(resolvedAnimation => { + assert_equals(resolvedAnimation, animation, + 'Object identity of Animation passed to Promise callback' + + ' matches the Animation object owning the Promise'); + }); +}, 'The ready promise is fulfilled with its Animation'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + + // Ensure the ready promise is created + const ready = animation.ready; + + window.addEventListener( + 'unhandledrejection', + t.unreached_func('Should not get an unhandled rejection') + ); + + animation.cancel(); + + // Wait a moment to allow a chance for the event to be dispatched. + await waitForAnimationFrames(2); +}, 'The ready promise does not report an unhandledrejection when rejected'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/interfaces/Animation/startTime.html b/testing/web-platform/tests/web-animations/interfaces/Animation/startTime.html new file mode 100644 index 0000000000..61f76955a3 --- /dev/null +++ b/testing/web-platform/tests/web-animations/interfaces/Animation/startTime.html @@ -0,0 +1,55 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Animation.startTime</title> +<link rel="help" +href="https://drafts.csswg.org/web-animations/#dom-animation-starttime"> +<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(createDiv(t), null), + document.timeline); + assert_equals(animation.startTime, null, 'startTime is unresolved'); +}, 'startTime of a newly created (idle) animation is unresolved'); + +test(t => { + const animation = new Animation(new KeyframeEffect(createDiv(t), null), + document.timeline); + animation.play(); + assert_equals(animation.startTime, null, 'startTime is unresolved'); +}, 'startTime of a play-pending animation is unresolved'); + +test(t => { + const animation = new Animation(new KeyframeEffect(createDiv(t), null), + document.timeline); + animation.pause(); + assert_equals(animation.startTime, null, 'startTime is unresolved'); +}, 'startTime of a pause-pending animation is unresolved'); + +test(t => { + const animation = createDiv(t).animate(null); + assert_equals(animation.startTime, null, 'startTime is unresolved'); +}, 'startTime of a play-pending animation created using Element.animate' + + ' shortcut is unresolved'); + +promise_test(t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + return animation.ready.then(() => { + assert_greater_than(animation.startTime, 0, 'startTime when running'); + }); +}, 'startTime is resolved when running'); + +test(t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.cancel(); + assert_equals(animation.startTime, null); + assert_equals(animation.currentTime, null); +}, 'startTime and currentTime are unresolved when animation is cancelled'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/interfaces/Animation/style-change-events.html b/testing/web-platform/tests/web-animations/interfaces/Animation/style-change-events.html new file mode 100644 index 0000000000..b41f748720 --- /dev/null +++ b/testing/web-platform/tests/web-animations/interfaces/Animation/style-change-events.html @@ -0,0 +1,371 @@ +<!doctype html> +<meta charset=utf-8> +<title>Animation interface: style change events</title> +<link rel="help" + href="https://drafts.csswg.org/web-animations-1/#model-liveness"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<body> +<div id="log"></div> +<script> +'use strict'; + +// Test that each property defined in the Animation interface behaves as +// expected with regards to whether or not it produces style change events. +// +// There are two types of tests: +// +// PlayAnimationTest +// +// For properties that are able to cause the Animation to start affecting +// the target CSS property. +// +// This function takes either: +// +// (a) A function that simply "plays" that passed-in Animation (i.e. makes +// it start affecting the target CSS property. +// +// (b) An object with the following format: +// +// { +// setup: elem => { /* return Animation */ }, +// test: animation => { /* play |animation| */ }, +// shouldFlush: boolean /* optional, defaults to false */ +// } +// +// If the latter form is used, the setup function should return an Animation +// that does NOT (yet) have an in-effect AnimationEffect that affects the +// 'opacity' property. Otherwise, the transition we use to detect if a style +// change event has occurred will never have a chance to be triggered (since +// the animated style will clobber both before-change and after-change +// style). +// +// Examples of valid animations: +// +// - An animation that is idle, or finished but without a fill mode. +// - An animation with an effect that that does not affect opacity. +// +// UsePropertyTest +// +// For properties that cannot cause the Animation to start affecting the +// target CSS property. +// +// The shape of the parameter to the UsePropertyTest is identical to the +// PlayAnimationTest. The only difference is that the function (or 'test' +// function of the object format is used) does not need to play the +// animation, but simply needs to get/set the property under test. + +const PlayAnimationTest = testFuncOrObj => { + let test, setup, shouldFlush; + + if (typeof testFuncOrObj === 'function') { + test = testFuncOrObj; + shouldFlush = false; + } else { + test = testFuncOrObj.test; + if (typeof testFuncOrObj.setup === 'function') { + setup = testFuncOrObj.setup; + } + shouldFlush = !!testFuncOrObj.shouldFlush; + } + + if (!setup) { + setup = elem => + new Animation( + new KeyframeEffect(elem, { opacity: [0, 1] }, 100 * MS_PER_SEC) + ); + } + + return { test, setup, shouldFlush }; +}; + +const UsePropertyTest = testFuncOrObj => { + const { setup, test, shouldFlush } = PlayAnimationTest(testFuncOrObj); + + let coveringAnimation; + return { + setup: elem => { + coveringAnimation = new Animation( + new KeyframeEffect(elem, { opacity: [0, 1] }, 100 * MS_PER_SEC) + ); + + return setup(elem); + }, + test: animation => { + test(animation); + coveringAnimation.play(); + }, + shouldFlush, + }; +}; + +const tests = { + id: UsePropertyTest(animation => (animation.id = 'yer')), + get effect() { + let effect; + return PlayAnimationTest({ + setup: elem => { + // Create a new effect and animation but don't associate them yet + effect = new KeyframeEffect( + elem, + { opacity: [0.5, 1] }, + 100 * MS_PER_SEC + ); + return elem.animate(null, 100 * MS_PER_SEC); + }, + test: animation => { + // Read the effect + animation.effect; + + // Assign the effect + animation.effect = effect; + }, + }); + }, + timeline: PlayAnimationTest({ + setup: elem => { + // Create a new animation with no timeline + const animation = new Animation( + new KeyframeEffect(elem, { opacity: [0.5, 1] }, 100 * MS_PER_SEC), + null + ); + // Set the hold time so that once we assign a timeline it will begin to + // play. + animation.currentTime = 0; + + return animation; + }, + test: animation => { + // Get the timeline + animation.timeline; + + // Play the animation by setting the timeline + animation.timeline = document.timeline; + }, + }), + startTime: PlayAnimationTest(animation => { + // Get the startTime + animation.startTime; + + // Play the animation by setting the startTime + animation.startTime = document.timeline.currentTime; + }), + currentTime: PlayAnimationTest(animation => { + // Get the currentTime + animation.currentTime; + + // Play the animation by setting the currentTime + animation.currentTime = 0; + }), + playbackRate: UsePropertyTest(animation => { + // Get and set the playbackRate + animation.playbackRate = animation.playbackRate * 1.1; + }), + playState: UsePropertyTest(animation => animation.playState), + pending: UsePropertyTest(animation => animation.pending), + replaceState: UsePropertyTest(animation => animation.replaceState), + ready: UsePropertyTest(animation => animation.ready), + finished: UsePropertyTest(animation => { + // Get the finished Promise + animation.finished; + }), + onfinish: UsePropertyTest(animation => { + // Get the onfinish member + animation.onfinish; + + // Set the onfinish menber + animation.onfinish = () => {}; + }), + onremove: UsePropertyTest(animation => { + // Get the onremove member + animation.onremove; + + // Set the onremove menber + animation.onremove = () => {}; + }), + oncancel: UsePropertyTest(animation => { + // Get the oncancel member + animation.oncancel; + + // Set the oncancel menber + animation.oncancel = () => {}; + }), + cancel: UsePropertyTest({ + // Animate _something_ just to make the test more interesting + setup: elem => elem.animate({ color: ['green', 'blue'] }, 100 * MS_PER_SEC), + test: animation => { + animation.cancel(); + }, + }), + finish: PlayAnimationTest({ + setup: elem => + new Animation( + new KeyframeEffect( + elem, + { opacity: [0.5, 1] }, + { + duration: 100 * MS_PER_SEC, + fill: 'both', + } + ) + ), + test: animation => { + animation.finish(); + }, + }), + play: PlayAnimationTest(animation => animation.play()), + pause: PlayAnimationTest(animation => { + // Pause animation -- this will cause the animation to transition from the + // 'idle' state to the 'paused' (but pending) state with hold time zero. + animation.pause(); + }), + updatePlaybackRate: UsePropertyTest(animation => { + animation.updatePlaybackRate(1.1); + }), + // We would like to use a PlayAnimationTest here but reverse() is async and + // doesn't start applying its result until the animation is ready. + reverse: UsePropertyTest({ + setup: elem => { + // Create a new animation and seek it to the end so that it no longer + // affects style (since it has no fill mode). + const animation = elem.animate({ opacity: [0.5, 1] }, 100 * MS_PER_SEC); + animation.finish(); + return animation; + }, + test: animation => { + animation.reverse(); + }, + }), + persist: PlayAnimationTest({ + setup: async elem => { + // Create an animation whose replaceState is 'removed'. + const animA = elem.animate( + { opacity: 1 }, + { duration: 1, fill: 'forwards' } + ); + const animB = elem.animate( + { opacity: 1 }, + { duration: 1, fill: 'forwards' } + ); + await animA.finished; + animB.cancel(); + + return animA; + }, + test: animation => { + animation.persist(); + }, + }), + commitStyles: PlayAnimationTest({ + setup: async elem => { + // Create an animation whose replaceState is 'removed'. + const animA = elem.animate( + // It's important to use opacity of '1' here otherwise we'll create a + // transition due to updating the specified style whereas the transition + // we want to detect is the one from flushing due to calling + // commitStyles. + { opacity: 1 }, + { duration: 1, fill: 'forwards' } + ); + const animB = elem.animate( + { opacity: 1 }, + { duration: 1, fill: 'forwards' } + ); + await animA.finished; + animB.cancel(); + + return animA; + }, + test: animation => { + animation.commitStyles(); + }, + shouldFlush: true, + }), + get ['Animation constructor']() { + let originalElem; + return UsePropertyTest({ + setup: elem => { + originalElem = elem; + // Return a dummy animation so the caller has something to wait on + return elem.animate(null); + }, + test: () => + new Animation( + new KeyframeEffect( + originalElem, + { opacity: [0.5, 1] }, + 100 * MS_PER_SEC + ) + ), + }); + }, +}; + +// Check that each enumerable property and the constructor follow the +// expected behavior with regards to triggering style change events. +const properties = [ + ...Object.keys(Animation.prototype), + 'Animation constructor', +]; + +test(() => { + for (const property of Object.keys(tests)) { + assert_in_array( + property, + properties, + `Test property '${property}' should be one of the properties on ` + + ' Animation' + ); + } +}, 'All property keys are recognized'); + +for (const key of properties) { + promise_test(async t => { + assert_own_property(tests, key, `Should have a test for '${key}' property`); + const { setup, test, shouldFlush } = tests[key]; + + // Setup target element + const div = createDiv(t); + let gotTransition = false; + div.addEventListener('transitionrun', () => { + gotTransition = true; + }); + + // Setup animation + const animation = await setup(div); + + // Setup transition start point + div.style.transition = 'opacity 100s'; + getComputedStyle(div).opacity; + + // Update specified style but don't flush + div.style.opacity = '0.5'; + + // Trigger the property + test(animation); + + // If the test function produced a style change event it will have triggered + // a transition. + + // Wait for the animation to start and then for at least two animation + // frames to give the transitionrun event a chance to be dispatched. + assert_true( + typeof animation.ready !== 'undefined', + 'Should have a valid animation to wait on' + ); + await animation.ready; + await waitForAnimationFrames(2); + + if (shouldFlush) { + assert_true(gotTransition, 'A transition should have been triggered'); + } else { + assert_false( + gotTransition, + 'A transition should NOT have been triggered' + ); + } + }, `Animation.${key} produces expected style change events`); +} +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/interfaces/AnimationEffect/getComputedTiming.html b/testing/web-platform/tests/web-animations/interfaces/AnimationEffect/getComputedTiming.html new file mode 100644 index 0000000000..10bd193361 --- /dev/null +++ b/testing/web-platform/tests/web-animations/interfaces/AnimationEffect/getComputedTiming.html @@ -0,0 +1,214 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>AnimationEffect.getComputedTiming</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#dom-animationeffect-getcomputedtiming"> +<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 effect = new KeyframeEffect(null, null); + + const ct = effect.getComputedTiming(); + assert_equals(ct.delay, 0, 'computed delay'); + assert_equals(ct.endDelay, 0, 'computed endDelay'); + assert_equals(ct.fill, 'none', 'computed fill'); + assert_equals(ct.iterationStart, 0.0, 'computed iterationStart'); + assert_equals(ct.iterations, 1.0, 'computed iterations'); + assert_equals(ct.duration, 0, 'computed duration'); + assert_equals(ct.direction, 'normal', 'computed direction'); + assert_equals(ct.easing, 'linear', 'computed easing'); +}, 'values of getComputedTiming() when a KeyframeEffect is ' + + 'constructed without any KeyframeEffectOptions object'); + +const gGetComputedTimingTests = [ + { desc: 'an empty KeyframeEffectOptions object', + input: { }, + expected: { } }, + { desc: 'a normal KeyframeEffectOptions object', + input: { delay: 1000, + endDelay: 2000, + fill: 'auto', + iterationStart: 0.5, + iterations: 5.5, + duration: 'auto', + direction: 'alternate', + easing: 'steps(2)' }, + expected: { delay: 1000, + endDelay: 2000, + fill: 'none', + iterationStart: 0.5, + iterations: 5.5, + duration: 0, + direction: 'alternate', + easing: 'steps(2)' } }, + { desc: 'a double value', + input: 3000, + timing: { duration: 3000 }, + expected: { delay: 0, + fill: 'none', + iterations: 1, + duration: 3000, + direction: 'normal' } }, + { desc: '+Infinity', + input: Infinity, + expected: { duration: Infinity } }, + { desc: 'an Infinity duration', + input: { duration: Infinity }, + expected: { duration: Infinity } }, + { desc: 'an auto duration', + input: { duration: 'auto' }, + expected: { duration: 0 } }, + { desc: 'an Infinity iterations', + input: { iterations: Infinity }, + expected: { iterations: Infinity } }, + { desc: 'an auto fill', + input: { fill: 'auto' }, + expected: { fill: 'none' } }, + { desc: 'a forwards fill', + input: { fill: 'forwards' }, + expected: { fill: 'forwards' } } +]; + +for (const stest of gGetComputedTimingTests) { + test(t => { + const effect = new KeyframeEffect(null, null, stest.input); + + // Helper function to provide default expected values when the test does + // not supply them. + const expected = (field, defaultValue) => { + return field in stest.expected ? stest.expected[field] : defaultValue; + }; + + const ct = effect.getComputedTiming(); + assert_equals(ct.delay, expected('delay', 0), + 'computed delay'); + assert_equals(ct.endDelay, expected('endDelay', 0), + 'computed endDelay'); + assert_equals(ct.fill, expected('fill', 'none'), + 'computed fill'); + assert_equals(ct.iterationStart, expected('iterationStart', 0), + 'computed iterations'); + assert_equals(ct.iterations, expected('iterations', 1), + 'computed iterations'); + assert_equals(ct.duration, expected('duration', 0), + 'computed duration'); + assert_equals(ct.direction, expected('direction', 'normal'), + 'computed direction'); + assert_equals(ct.easing, expected('easing', 'linear'), + 'computed easing'); + + }, 'values of getComputedTiming() when a KeyframeEffect is' + + ` constructed by ${stest.desc}`); +} + +const gActiveDurationTests = [ + { desc: 'an empty KeyframeEffectOptions object', + input: { }, + expected: 0 }, + { desc: 'a non-zero duration and default iteration count', + input: { duration: 1000 }, + expected: 1000 }, + { desc: 'a non-zero duration and integral iteration count', + input: { duration: 1000, iterations: 7 }, + expected: 7000 }, + { desc: 'a non-zero duration and fractional iteration count', + input: { duration: 1000, iterations: 2.5 }, + expected: 2500 }, + { desc: 'an non-zero duration and infinite iteration count', + input: { duration: 1000, iterations: Infinity }, + expected: Infinity }, + { desc: 'an non-zero duration and zero iteration count', + input: { duration: 1000, iterations: 0 }, + expected: 0 }, + { desc: 'a zero duration and default iteration count', + input: { duration: 0 }, + expected: 0 }, + { desc: 'a zero duration and fractional iteration count', + input: { duration: 0, iterations: 2.5 }, + expected: 0 }, + { desc: 'a zero duration and infinite iteration count', + input: { duration: 0, iterations: Infinity }, + expected: 0 }, + { desc: 'a zero duration and zero iteration count', + input: { duration: 0, iterations: 0 }, + expected: 0 }, + { desc: 'an infinite duration and default iteration count', + input: { duration: Infinity }, + expected: Infinity }, + { desc: 'an infinite duration and zero iteration count', + input: { duration: Infinity, iterations: 0 }, + expected: 0 }, + { desc: 'an infinite duration and fractional iteration count', + input: { duration: Infinity, iterations: 2.5 }, + expected: Infinity }, + { desc: 'an infinite duration and infinite iteration count', + input: { duration: Infinity, iterations: Infinity }, + expected: Infinity }, +]; + +for (const stest of gActiveDurationTests) { + test(t => { + const effect = new KeyframeEffect(null, null, stest.input); + + assert_equals(effect.getComputedTiming().activeDuration, + stest.expected); + + }, `getComputedTiming().activeDuration for ${stest.desc}`); +} + +const gEndTimeTests = [ + { desc: 'an empty KeyframeEffectOptions object', + input: { }, + expected: 0 }, + { desc: 'a non-zero duration and default iteration count', + input: { duration: 1000 }, + expected: 1000 }, + { desc: 'a non-zero duration and non-default iteration count', + input: { duration: 1000, iterations: 2.5 }, + expected: 2500 }, + { desc: 'a non-zero duration and non-zero delay', + input: { duration: 1000, delay: 1500 }, + expected: 2500 }, + { desc: 'a non-zero duration, non-zero delay and non-default iteration', + input: { duration: 1000, delay: 1500, iterations: 2 }, + expected: 3500 }, + { desc: 'an infinite iteration count', + input: { duration: 1000, iterations: Infinity }, + expected: Infinity }, + { desc: 'an infinite duration', + input: { duration: Infinity, iterations: 10 }, + expected: Infinity }, + { desc: 'an infinite duration and delay', + input: { duration: Infinity, iterations: 10, delay: 1000 }, + expected: Infinity }, + { desc: 'an infinite duration and negative delay', + input: { duration: Infinity, iterations: 10, delay: -1000 }, + expected: Infinity }, + { desc: 'an non-zero duration and negative delay', + input: { duration: 1000, iterations: 2, delay: -1000 }, + expected: 1000 }, + { desc: 'an non-zero duration and negative delay greater than active ' + + 'duration', + input: { duration: 1000, iterations: 2, delay: -3000 }, + expected: 0 }, + { desc: 'a zero duration and negative delay', + input: { duration: 0, iterations: 2, delay: -1000 }, + expected: 0 } +]; + +for (const stest of gEndTimeTests) { + test(t => { + const effect = new KeyframeEffect(null, null, stest.input); + + assert_equals(effect.getComputedTiming().endTime, + stest.expected); + + }, `getComputedTiming().endTime for ${stest.desc}`); +} +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/interfaces/AnimationEffect/updateTiming.html b/testing/web-platform/tests/web-animations/interfaces/AnimationEffect/updateTiming.html new file mode 100644 index 0000000000..6a340c0bf4 --- /dev/null +++ b/testing/web-platform/tests/web-animations/interfaces/AnimationEffect/updateTiming.html @@ -0,0 +1,475 @@ +<!doctype html> +<meta charset=utf-8> +<title>AnimationEffect.updateTiming</title> +<link rel="help" href="https://drafts.csswg.org/web-animations-1/#dom-animationeffect-updatetiming"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<script src="../../resources/easing-tests.js"></script> +<script src="../../resources/timing-tests.js"></script> +<body> +<div id="log"></div> +<script> +'use strict'; + +// ------------------------------ +// delay +// ------------------------------ + +test(t => { + const anim = createDiv(t).animate(null, 100); + anim.effect.updateTiming({ delay: 100 }); + assert_equals(anim.effect.getTiming().delay, 100, 'set delay 100'); + assert_equals(anim.effect.getComputedTiming().delay, 100, + 'getComputedTiming() after set delay 100'); +}, 'Allows setting the delay to a positive number'); + +test(t => { + const anim = createDiv(t).animate(null, 100); + anim.effect.updateTiming({ delay: -100 }); + assert_equals(anim.effect.getTiming().delay, -100, 'set delay -100'); + assert_equals(anim.effect.getComputedTiming().delay, -100, + 'getComputedTiming() after set delay -100'); +}, 'Allows setting the delay to a negative number'); + +test(t => { + const anim = createDiv(t).animate(null, 100); + anim.effect.updateTiming({ delay: 100 }); + assert_equals(anim.effect.getComputedTiming().progress, null); + assert_equals(anim.effect.getComputedTiming().currentIteration, null); +}, 'Allows setting the delay of an animation in progress: positive delay that' + + ' causes the animation to be no longer in-effect'); + +test(t => { + const anim = createDiv(t).animate(null, { fill: 'both', duration: 100 }); + anim.effect.updateTiming({ delay: -50 }); + assert_equals(anim.effect.getComputedTiming().progress, 0.5); +}, 'Allows setting the delay of an animation in progress: negative delay that' + + ' seeks into the active interval'); + +test(t => { + const anim = createDiv(t).animate(null, { fill: 'both', duration: 100 }); + anim.effect.updateTiming({ delay: -100 }); + assert_equals(anim.effect.getComputedTiming().progress, 1); + assert_equals(anim.effect.getComputedTiming().currentIteration, 0); +}, 'Allows setting the delay of an animation in progress: large negative delay' + + ' that causes the animation to be finished'); + +for (const invalid of gBadDelayValues) { + test(t => { + const anim = createDiv(t).animate(null); + assert_throws_js(TypeError, () => { + anim.effect.updateTiming({ delay: invalid }); + }); + }, `Throws when setting invalid delay value: ${invalid}`); +} + + +// ------------------------------ +// endDelay +// ------------------------------ + +test(t => { + const anim = createDiv(t).animate(null, 2000); + anim.effect.updateTiming({ endDelay: 123.45 }); + assert_time_equals_literal(anim.effect.getTiming().endDelay, 123.45, + 'set endDelay 123.45'); + assert_time_equals_literal(anim.effect.getComputedTiming().endDelay, 123.45, + 'getComputedTiming() after set endDelay 123.45'); +}, 'Allows setting the endDelay to a positive number'); + +test(t => { + const anim = createDiv(t).animate(null, 2000); + anim.effect.updateTiming({ endDelay: -1000 }); + assert_equals(anim.effect.getTiming().endDelay, -1000, 'set endDelay -1000'); + assert_equals(anim.effect.getComputedTiming().endDelay, -1000, + 'getComputedTiming() after set endDelay -1000'); +}, 'Allows setting the endDelay to a negative number'); + +test(t => { + const anim = createDiv(t).animate(null, 2000); + assert_throws_js(TypeError, () => { + anim.effect.updateTiming({ endDelay: Infinity }); + }); +}, 'Throws when setting the endDelay to infinity'); + +test(t => { + const anim = createDiv(t).animate(null, 2000); + assert_throws_js(TypeError, () => { + anim.effect.updateTiming({ endDelay: -Infinity }); + }); +}, 'Throws when setting the endDelay to negative infinity'); + + +// ------------------------------ +// fill +// ------------------------------ + +for (const fill of ['none', 'forwards', 'backwards', 'both']) { + test(t => { + const anim = createDiv(t).animate(null, 100); + anim.effect.updateTiming({ fill }); + assert_equals(anim.effect.getTiming().fill, fill, 'set fill ' + fill); + assert_equals(anim.effect.getComputedTiming().fill, fill, + 'getComputedTiming() after set fill ' + fill); + }, `Allows setting the fill to '${fill}'`); +} + + +// ------------------------------ +// iterationStart +// ------------------------------ + +test(t => { + const anim = createDiv(t).animate(null, + { iterationStart: 0.2, + iterations: 1, + fill: 'both', + duration: 100, + delay: 1 }); + anim.effect.updateTiming({ iterationStart: 2.5 }); + assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.5); + assert_equals(anim.effect.getComputedTiming().currentIteration, 2); +}, 'Allows setting the iterationStart of an animation in progress:' + + ' backwards-filling'); + +test(t => { + const anim = createDiv(t).animate(null, + { iterationStart: 0.2, + iterations: 1, + fill: 'both', + duration: 100, + delay: 0 }); + anim.effect.updateTiming({ iterationStart: 2.5 }); + assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.5); + assert_equals(anim.effect.getComputedTiming().currentIteration, 2); +}, 'Allows setting the iterationStart of an animation in progress:' + + ' active phase'); + +test(t => { + const anim = createDiv(t).animate(null, + { iterationStart: 0.2, + iterations: 1, + fill: 'both', + duration: 100, + delay: 0 }); + anim.finish(); + anim.effect.updateTiming({ iterationStart: 2.5 }); + assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.5); + assert_equals(anim.effect.getComputedTiming().currentIteration, 3); +}, 'Allows setting the iterationStart of an animation in progress:' + + ' forwards-filling'); + +for (const invalid of gBadIterationStartValues) { + test(t => { + const anim = createDiv(t).animate(null); + assert_throws_js(TypeError, () => { + anim.effect.updateTiming({ iterationStart: invalid }); + }, `setting ${invalid}`); + }, `Throws when setting invalid iterationStart value: ${invalid}`); +} + +// ------------------------------ +// iterations +// ------------------------------ + +test(t => { + const anim = createDiv(t).animate(null, 2000); + anim.effect.updateTiming({ iterations: 2 }); + assert_equals(anim.effect.getTiming().iterations, 2, 'set duration 2'); + assert_equals(anim.effect.getComputedTiming().iterations, 2, + 'getComputedTiming() after set iterations 2'); +}, 'Allows setting iterations to a double value'); + +test(t => { + const anim = createDiv(t).animate(null, 2000); + anim.effect.updateTiming({ iterations: Infinity }); + assert_equals(anim.effect.getTiming().iterations, Infinity, + 'set duration Infinity'); + assert_equals(anim.effect.getComputedTiming().iterations, Infinity, + 'getComputedTiming() after set iterations Infinity'); +}, 'Allows setting iterations to infinity'); + +for (const invalid of gBadIterationsValues) { + test(t => { + const anim = createDiv(t).animate(null); + assert_throws_js(TypeError, () => { + anim.effect.updateTiming({ iterations: invalid }); + }); + }, `Throws when setting invalid iterations value: ${invalid}`); +} + +test(t => { + const anim = createDiv(t).animate(null, { duration: 100000, fill: 'both' }); + + anim.finish(); + + assert_equals(anim.effect.getComputedTiming().progress, 1, + 'progress when animation is finished'); + assert_equals(anim.effect.getComputedTiming().currentIteration, 0, + 'current iteration when animation is finished'); + + anim.effect.updateTiming({ iterations: 2 }); + + assert_time_equals_literal(anim.effect.getComputedTiming().progress, + 0, + 'progress after adding an iteration'); + assert_time_equals_literal(anim.effect.getComputedTiming().currentIteration, + 1, + 'current iteration after adding an iteration'); + + anim.effect.updateTiming({ iterations: 0 }); + + assert_equals(anim.effect.getComputedTiming().progress, 0, + 'progress after setting iterations to zero'); + assert_equals(anim.effect.getComputedTiming().currentIteration, 0, + 'current iteration after setting iterations to zero'); + + anim.effect.updateTiming({ iterations: Infinity }); + + assert_equals(anim.effect.getComputedTiming().progress, 0, + 'progress after setting iterations to Infinity'); + assert_equals(anim.effect.getComputedTiming().currentIteration, 1, + 'current iteration after setting iterations to Infinity'); +}, 'Allows setting the iterations of an animation in progress'); + + +// ------------------------------ +// duration +// ------------------------------ + +for (const duration of gGoodDurationValues) { + test(t => { + const anim = createDiv(t).animate(null, 2000); + anim.effect.updateTiming({ duration: duration.specified }); + if (typeof duration.specified === 'number') { + assert_time_equals_literal(anim.effect.getTiming().duration, + duration.specified, + 'Updates specified duration'); + } else { + assert_equals(anim.effect.getTiming().duration, duration.specified, + 'Updates specified duration'); + } + assert_time_equals_literal(anim.effect.getComputedTiming().duration, + duration.computed, + 'Updates computed duration'); + }, `Allows setting the duration to ${duration.specified}`); +} + +for (const invalid of gBadDurationValues) { + test(t => { + assert_throws_js(TypeError, () => { + createDiv(t).animate(null, { duration: invalid }); + }); + }, 'Throws when setting invalid duration: ' + + (typeof invalid === 'string' ? `"${invalid}"` : invalid)); +} + +test(t => { + const anim = createDiv(t).animate(null, { duration: 100000, fill: 'both' }); + anim.finish(); + assert_equals(anim.effect.getComputedTiming().progress, 1, + 'progress when animation is finished'); + anim.effect.updateTiming({ duration: anim.effect.getTiming().duration * 2 }); + assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.5, + 'progress after doubling the duration'); + anim.effect.updateTiming({ duration: 0 }); + assert_equals(anim.effect.getComputedTiming().progress, 1, + 'progress after setting duration to zero'); + anim.effect.updateTiming({ duration: 'auto' }); + assert_equals(anim.effect.getComputedTiming().progress, 1, + 'progress after setting duration to \'auto\''); +}, 'Allows setting the duration of an animation in progress'); + +promise_test(t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + return anim.ready.then(() => { + const originalStartTime = anim.startTime; + const originalCurrentTime = anim.currentTime; + assert_time_equals_literal( + anim.effect.getComputedTiming().duration, + 100 * MS_PER_SEC, + 'Initial duration should be as set on KeyframeEffect' + ); + + anim.effect.updateTiming({ duration: 200 * MS_PER_SEC }); + assert_time_equals_literal( + anim.effect.getComputedTiming().duration, + 200 * MS_PER_SEC, + 'Effect duration should have been updated' + ); + assert_times_equal(anim.startTime, originalStartTime, + 'startTime should be unaffected by changing effect ' + + 'duration'); + assert_times_equal(anim.currentTime, originalCurrentTime, + 'currentTime should be unaffected by changing effect ' + + 'duration'); + }); +}, 'Allows setting the duration of an animation in progress such that the' + + ' the start and current time do not change'); + + +// ------------------------------ +// direction +// ------------------------------ + +test(t => { + const anim = createDiv(t).animate(null, 2000); + + const directions = ['normal', 'reverse', 'alternate', 'alternate-reverse']; + for (const direction of directions) { + anim.effect.updateTiming({ direction: direction }); + assert_equals(anim.effect.getTiming().direction, direction, + `set direction to ${direction}`); + } +}, 'Allows setting the direction to each of the possible keywords'); + +test(t => { + const anim = createDiv(t).animate(null, { + duration: 10000, + direction: 'normal', + }); + anim.currentTime = 7000; + assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.7, + 'progress before updating direction'); + + anim.effect.updateTiming({ direction: 'reverse' }); + + assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.3, + 'progress after updating direction'); +}, 'Allows setting the direction of an animation in progress from \'normal\' to' + + ' \'reverse\''); + +test(t => { + const anim = createDiv(t).animate(null, + { duration: 10000, direction: 'normal' }); + assert_equals(anim.effect.getComputedTiming().progress, 0, + 'progress before updating direction'); + + anim.effect.updateTiming({ direction: 'reverse' }); + + assert_equals(anim.effect.getComputedTiming().progress, 1, + 'progress after updating direction'); +}, 'Allows setting the direction of an animation in progress from \'normal\' to' + + ' \'reverse\' while at start of active interval'); + +test(t => { + const anim = createDiv(t).animate(null, + { fill: 'backwards', + duration: 10000, + delay: 10000, + direction: 'normal' }); + assert_equals(anim.effect.getComputedTiming().progress, 0, + 'progress before updating direction'); + + anim.effect.updateTiming({ direction: 'reverse' }); + + assert_equals(anim.effect.getComputedTiming().progress, 1, + 'progress after updating direction'); +}, 'Allows setting the direction of an animation in progress from \'normal\' to' + + ' \'reverse\' while filling backwards'); + +test(t => { + const anim = createDiv(t).animate(null, + { iterations: 2, + duration: 10000, + direction: 'normal' }); + anim.currentTime = 17000; + assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.7, + 'progress before updating direction'); + + anim.effect.updateTiming({ direction: 'alternate' }); + + assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.3, + 'progress after updating direction'); +}, 'Allows setting the direction of an animation in progress from \'normal\' to' + + ' \'alternate\''); + +test(t => { + const anim = createDiv(t).animate(null, + { iterations: 2, + duration: 10000, + direction: 'alternate' }); + anim.currentTime = 17000; + assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.3, + 'progress before updating direction'); + + anim.effect.updateTiming({ direction: 'alternate-reverse' }); + + assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.7, + 'progress after updating direction'); +}, 'Allows setting the direction of an animation in progress from \'alternate\'' + + ' to \'alternate-reverse\''); + + +// ------------------------------ +// easing +// ------------------------------ + +function assert_progress(animation, currentTime, easingFunction) { + animation.currentTime = currentTime; + const portion = currentTime / animation.effect.getTiming().duration; + assert_approx_equals(animation.effect.getComputedTiming().progress, + easingFunction(portion), + 0.01, + 'The progress of the animation should be approximately' + + ` ${easingFunction(portion)} at ${currentTime}ms`); +} + +for (const options of gEasingTests) { + test(t => { + const target = createDiv(t); + const anim = target.animate(null, + { duration: 1000 * MS_PER_SEC, + fill: 'forwards' }); + anim.effect.updateTiming({ easing: options.easing }); + assert_equals(anim.effect.getTiming().easing, + options.serialization || options.easing); + + const easing = options.easingFunction; + assert_progress(anim, 0, easing); + assert_progress(anim, 250 * MS_PER_SEC, easing); + assert_progress(anim, 500 * MS_PER_SEC, easing); + assert_progress(anim, 750 * MS_PER_SEC, easing); + assert_progress(anim, 1000 * MS_PER_SEC, easing); + }, `Allows setting the easing to a ${options.desc}`); +} + +for (const easing of gRoundtripEasings) { + test(t => { + const anim = createDiv(t).animate(null); + anim.effect.updateTiming({ easing: easing }); + assert_equals(anim.effect.getTiming().easing, easing); + }, `Updates the specified value when setting the easing to '${easing}'`); +} + +test(t => { + const delay = 1000 * MS_PER_SEC; + + const target = createDiv(t); + const anim = target.animate(null, + { duration: 1000 * MS_PER_SEC, + fill: 'both', + delay: delay, + easing: 'steps(2, start)' }); + + anim.effect.updateTiming({ easing: 'steps(2, end)' }); + assert_equals(anim.effect.getComputedTiming().progress, 0, + 'easing replace to steps(2, end) at before phase'); + + anim.currentTime = delay + 750 * MS_PER_SEC; + assert_equals(anim.effect.getComputedTiming().progress, 0.5, + 'change currentTime to active phase'); + + anim.effect.updateTiming({ easing: 'steps(2, start)' }); + assert_equals(anim.effect.getComputedTiming().progress, 1, + 'easing replace to steps(2, start) at active phase'); + + anim.currentTime = delay + 1500 * MS_PER_SEC; + anim.effect.updateTiming({ easing: 'steps(2, end)' }); + assert_equals(anim.effect.getComputedTiming().progress, 1, + 'easing replace to steps(2, end) again at after phase'); +}, 'Allows setting the easing of an animation in progress'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/interfaces/AnimationPlaybackEvent/constructor.html b/testing/web-platform/tests/web-animations/interfaces/AnimationPlaybackEvent/constructor.html new file mode 100644 index 0000000000..1c40a3fb21 --- /dev/null +++ b/testing/web-platform/tests/web-animations/interfaces/AnimationPlaybackEvent/constructor.html @@ -0,0 +1,30 @@ +<!doctype html> +<meta charset=utf-8> +<title>AnimationPlaybackEvent constructor</title> +<link rel="help" + href="https://drafts.csswg.org/web-animations/#dom-animationplaybackevent-animationplaybackevent"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; + +test(t => { + const evt = new AnimationPlaybackEvent('finish'); + assert_equals(evt.type, 'finish'); + assert_equals(evt.currentTime, null); + assert_equals(evt.timelineTime, null); +}, 'Event created without an event parameter has null time values'); + +test(t => { + const evt = + new AnimationPlaybackEvent('cancel', { + currentTime: -100, + timelineTime: 100, + }); + assert_equals(evt.type, 'cancel'); + assert_equals(evt.currentTime, -100); + assert_equals(evt.timelineTime, 100); +}, 'Created event reflects times specified in constructor'); + +</script> diff --git a/testing/web-platform/tests/web-animations/interfaces/Document/timeline.html b/testing/web-platform/tests/web-animations/interfaces/Document/timeline.html new file mode 100644 index 0000000000..b8b4d74d5e --- /dev/null +++ b/testing/web-platform/tests/web-animations/interfaces/Document/timeline.html @@ -0,0 +1,23 @@ +<!doctype html> +<meta charset=utf-8> +<title>Document.timeline</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#dom-document-timeline"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<div id="log"></div> +<iframe width="10" height="10" id="iframe"></iframe> +<script> +'use strict'; + +test(() => { + assert_equals(document.timeline, document.timeline, + 'Document.timeline returns the same object every time'); + const iframe = document.getElementById('iframe'); + assert_not_equals(document.timeline, iframe.contentDocument.timeline, + 'Document.timeline returns a different object for each document'); + assert_not_equals(iframe.contentDocument.timeline, null, + 'Document.timeline on an iframe is not null'); +}, 'Document.timeline returns the default document timeline'); + +</script> diff --git a/testing/web-platform/tests/web-animations/interfaces/DocumentOrShadowRoot/getAnimations.html b/testing/web-platform/tests/web-animations/interfaces/DocumentOrShadowRoot/getAnimations.html new file mode 100644 index 0000000000..9bcc042a8f --- /dev/null +++ b/testing/web-platform/tests/web-animations/interfaces/DocumentOrShadowRoot/getAnimations.html @@ -0,0 +1,234 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>DocumentOrShadowRoot.getAnimations</title> +<link rel="help" href="https://drafts.csswg.org/web-animations-1/#dom-documentorshadowroot-getanimations"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<body> +<div id="log"></div> +<div id="target"></div> +<script> +'use strict'; + +const gKeyFrames = { 'marginLeft': ['100px', '200px'] }; + +test(t => { + assert_equals(document.getAnimations().length, 0, + 'getAnimations returns an empty sequence for a document ' + + 'with no animations'); +}, 'Document.getAnimations() returns an empty sequence for non-animated' + + ' content'); + +test(t => { + const div = createDiv(t); + const anim1 = div.animate(gKeyFrames, 100 * MS_PER_SEC); + const anim2 = div.animate(gKeyFrames, 100 * MS_PER_SEC); + assert_equals(document.getAnimations().length, 2, + 'getAnimation returns running animations'); + + anim1.finish(); + anim2.finish(); + assert_equals(document.getAnimations().length, 0, + 'getAnimation only returns running animations'); +}, 'Document.getAnimations() returns script-generated animations') + +test(t => { + const div = createDiv(t); + const anim1 = div.animate(gKeyFrames, 100 * MS_PER_SEC); + const anim2 = div.animate(gKeyFrames, 100 * MS_PER_SEC); + assert_array_equals(document.getAnimations(), + [ anim1, anim2 ], + 'getAnimations() returns running animations'); +}, 'Document.getAnimations() returns script-generated animations in the order' + + ' they were created') + +test(t => { + // This element exists but is not a descendent of any document, so isn't + // picked up by getAnimations. + const div = document.createElement('div'); + const anim = div.animate(gKeyFrames, 100 * MS_PER_SEC); + assert_equals(document.getAnimations().length, 0); + + // Now connect the div; it should appear in the list of animations. + document.body.appendChild(div); + t.add_cleanup(() => { div.remove(); }); + assert_equals(document.getAnimations().length, 1); +}, 'Document.getAnimations() does not return a disconnected node'); + +test(t => { + const effect = new KeyframeEffect(null, gKeyFrames, 100 * MS_PER_SEC); + const anim = new Animation(effect, document.timeline); + anim.play(); + + assert_equals(document.getAnimations().length, 0, + 'document.getAnimations() only returns animations targeting ' + + 'elements in this document'); +}, 'Document.getAnimations() does not return an animation with a null target'); + +promise_test(async t => { + const iframe = document.createElement('iframe'); + await insertFrameAndAwaitLoad(t, iframe, document) + + const div = createDiv(t, iframe.contentDocument) + const effect = new KeyframeEffect(div, null, 100 * MS_PER_SEC); + const anim = new Animation(effect, document.timeline); + anim.play(); + + // The animation's timeline is from the main document, but the effect's + // target element is part of the iframe document and that is what matters + // for getAnimations. + assert_equals(document.getAnimations().length, 0); + assert_equals(iframe.contentDocument.getAnimations().length, 1); + anim.finish(); +}, 'Document.getAnimations() returns animations on elements inside same-origin' + + ' iframes'); + +promise_test(async t => { + const iframe1 = document.createElement('iframe'); + const iframe2 = document.createElement('iframe'); + + await insertFrameAndAwaitLoad(t, iframe1, document); + await insertFrameAndAwaitLoad(t, iframe2, document); + + const div_frame1 = createDiv(t, iframe1.contentDocument) + const div_main_frame = createDiv(t) + const effect1 = new KeyframeEffect(div_frame1, null, 100 * MS_PER_SEC); + const anim1 = new Animation(effect1, document.timeline); + anim1.play(); + // Animation of div_frame1 is in iframe with main timeline. + // The animation's timeline is from the iframe, but the effect's target + // element is part of the iframe's document. + assert_equals(document.getAnimations().length, 0); + assert_equals(iframe1.contentDocument.getAnimations().length, 1); + anim1.finish(); + + // animation of div_frame1 in iframe1 with iframe timeline + const effect2 = new KeyframeEffect(div_frame1, null, 100 * MS_PER_SEC); + const anim2 = new Animation(effect2, iframe1.contentDocument.timeline); + anim2.play(); + assert_equals(document.getAnimations().length, 0); + assert_equals(iframe1.contentDocument.getAnimations().length, 1); + anim2.finish(); + + //animation of div_main_frame in main frame with iframe timeline + const effect3 = new KeyframeEffect(div_main_frame, null, 100 * MS_PER_SEC); + const anim3 = new Animation(effect3, iframe1.contentDocument.timeline); + anim3.play(); + assert_equals(document.getAnimations().length, 1); + assert_equals(iframe1.contentDocument.getAnimations().length, 0); + anim3.finish(); + + //animation of div_frame1 in iframe1 with another iframe's timeline + const effect4 = new KeyframeEffect(div_frame1, null, 100 * MS_PER_SEC); + const anim4 = new Animation(effect4, iframe2.contentDocument.timeline); + anim4.play(); + assert_equals(document.getAnimations().length, 0); + assert_equals(iframe1.contentDocument.getAnimations().length, 1); + assert_equals(iframe2.contentDocument.getAnimations().length, 0); + anim4.finish(); +}, 'iframe.contentDocument.getAnimations() returns animations on elements ' + + 'inside same-origin Document'); + +test(t => { + const div = createDiv(t); + const shadow = div.attachShadow({ mode: 'open' }); + + // Create a tree with the following structure + // + // div + // | + // (ShadowRoot) + // / \ + // childA childB + // (*anim2) | + // grandChild + // (*anim1) + // + // This lets us test that: + // + // a) All children of the ShadowRoot are included + // b) Descendants of the children are included + // c) The result is sorted by composite order (since we fire anim1 before + // anim2 despite childA appearing first in tree order) + + const childA = createDiv(t); + shadow.append(childA); + + const childB = createDiv(t); + shadow.append(childB); + + const grandChild = createDiv(t); + childB.append(grandChild); + + const anim1 = grandChild.animate(gKeyFrames, 100 * MS_PER_SEC) + const anim2 = childA.animate(gKeyFrames, 100 * MS_PER_SEC) + + assert_array_equals( + div.shadowRoot.getAnimations(), + [ anim1, anim2 ], + 'getAnimations() called on ShadowRoot returns expected animations' + ); +}, 'ShadowRoot.getAnimations() return all animations in the shadow tree'); + +test(t => { + const div = createDiv(t); + const shadow = div.attachShadow({ mode: 'open' }); + + const child = createDiv(t); + shadow.append(child); + + child.animate(gKeyFrames, 100 * MS_PER_SEC) + + assert_array_equals( + document.getAnimations(), + [], + 'getAnimations() called on Document does not return animations from shadow' + + ' trees' + ); +}, 'Document.getAnimations() does NOT return animations in shadow trees'); + +test(t => { + const div = createDiv(t); + const shadow = div.attachShadow({ mode: 'open' }); + + div.animate(gKeyFrames, 100 * MS_PER_SEC) + + assert_array_equals( + div.shadowRoot.getAnimations(), + [], + 'getAnimations() called on ShadowRoot does not return animations from' + + ' Document' + ); +}, 'ShadowRoot.getAnimations() does NOT return animations in parent document'); + +promise_test(async t => { + const div = createDiv(t); + const watcher = EventWatcher(t, div, 'transitionrun'); + + // Create a covering animation to prevent transitions from firing after + // calling getAnimations(). + const coveringAnimation = new Animation( + new KeyframeEffect(div, { opacity: [0, 1] }, 100 * MS_PER_SEC) + ); + + // Setup transition start point. + div.style.transition = 'opacity 100s'; + getComputedStyle(div).opacity; + + // Update specified style but don't flush style. + div.style.opacity = '0.5'; + + // Fetch animations + document.getAnimations(); + + // Play the covering animation to ensure that only the call to + // getAnimations() has a chance to trigger transitions. + coveringAnimation.play(); + + // If getAnimations() flushed style, we should get a transitionrun event. + await watcher.wait_for('transitionrun'); +}, 'Document.getAnimations() triggers a style change event'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/interfaces/DocumentTimeline/constructor.html b/testing/web-platform/tests/web-animations/interfaces/DocumentTimeline/constructor.html new file mode 100644 index 0000000000..ca0997ac8f --- /dev/null +++ b/testing/web-platform/tests/web-animations/interfaces/DocumentTimeline/constructor.html @@ -0,0 +1,43 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>DocumentTimeline constructor tests</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#the-documenttimeline-interface"> +<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 timeline = new DocumentTimeline(); + + assert_times_equal(timeline.currentTime, document.timeline.currentTime); +}, 'An origin time of zero is used when none is supplied'); + +test(t => { + const timeline = new DocumentTimeline({ originTime: 0 }); + assert_times_equal(timeline.currentTime, document.timeline.currentTime); +}, 'A zero origin time produces a document timeline with a current time ' + + 'identical to the default document timeline'); + +test(t => { + const timeline = new DocumentTimeline({ originTime: 10 * MS_PER_SEC }); + + assert_times_equal(timeline.currentTime, + (document.timeline.currentTime - 10 * MS_PER_SEC)); +}, 'A positive origin time makes the document timeline\'s current time lag ' + + 'behind the default document timeline'); + +test(t => { + const timeline = new DocumentTimeline({ originTime: -10 * MS_PER_SEC }); + + assert_times_equal(timeline.currentTime, + (document.timeline.currentTime + 10 * MS_PER_SEC)); +}, 'A negative origin time makes the document timeline\'s current time run ' + + 'ahead of the default document timeline'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/interfaces/DocumentTimeline/style-change-events.html b/testing/web-platform/tests/web-animations/interfaces/DocumentTimeline/style-change-events.html new file mode 100644 index 0000000000..c1607e6fb9 --- /dev/null +++ b/testing/web-platform/tests/web-animations/interfaces/DocumentTimeline/style-change-events.html @@ -0,0 +1,92 @@ +<!doctype html> +<meta charset=utf-8> +<title>DocumentTimeline interface: style change events</title> +<link rel="help" + href="https://drafts.csswg.org/web-animations-1/#model-liveness"> +<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'; + +// NOTE: If more members are added to the DocumentTimeline interface it might be +// better to rewrite these test in the same style as: +// +// web-animations/interfaces/Animation/style-change-events.html +// web-animations/interfaces/KeyframeEffect/style-change-events.html + +promise_test(async t => { + const div = createDiv(t); + + let gotTransition = false; + div.addEventListener('transitionrun', () => { + gotTransition = true; + }); + + // Create a covering animation but don't play it yet. + const coveringAnimation = new Animation( + new KeyframeEffect(div, { opacity: [0, 1] }, 100 * MS_PER_SEC) + ); + + // Setup transition start point. + div.style.transition = 'opacity 100s'; + getComputedStyle(div).opacity; + + // Update specified style but don't flush style. + div.style.opacity = '0.5'; + + // Get the currentTime + document.timeline.currentTime; + + // Run the covering animation + coveringAnimation.play(); + + // If getting DocumentTimeline.currentTime produced a style change event it + // will trigger a transition. Otherwise, the covering animation will cause + // the before-change and after-change styles to be the same such that no + // transition is triggered on the next restyle. + + // Wait for a couple of animation frames to give the transitionrun event + // a chance to be dispatched. + await waitForAnimationFrames(2); + + assert_false(gotTransition, 'A transition should NOT have been triggered'); +}, 'DocumentTimeline.currentTime does NOT trigger a style change event'); + +promise_test(async t => { + const div = createDiv(t); + + let gotTransition = false; + div.addEventListener('transitionrun', () => { + gotTransition = true; + }); + + // Create a covering animation but don't play it yet. + const coveringAnimation = new Animation( + new KeyframeEffect(div, { opacity: [0, 1] }, 100 * MS_PER_SEC) + ); + + // Setup transition start point. + div.style.transition = 'opacity 100s'; + getComputedStyle(div).opacity; + + // Update specified style but don't flush style. + div.style.opacity = '0.5'; + + // Create a new DocumentTimeline + new DocumentTimeline(); + + // Run the covering animation + coveringAnimation.play(); + + // Wait for a couple of animation frames to give the transitionrun event + // a chance to be dispatched. + await waitForAnimationFrames(2); + + assert_false(gotTransition, 'A transition should NOT have been triggered'); +}, 'DocumentTimeline constructor does NOT trigger a style change event'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/composite.html b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/composite.html new file mode 100644 index 0000000000..bcca2cad24 --- /dev/null +++ b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/composite.html @@ -0,0 +1,47 @@ +<!doctype html> +<meta charset=utf-8> +<title>KeyframeEffect.composite</title> +<link rel="help" + href="https://drafts.csswg.org/web-animations/#dom-keyframeeffect-composite"> +<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 = createDiv(t).animate(null); + assert_equals(anim.effect.composite, 'replace', + 'The default value should be replace'); +}, 'Default value'); + +test(t => { + const anim = createDiv(t).animate(null); + anim.effect.composite = 'add'; + assert_equals(anim.effect.composite, 'add', + 'The effect composite value should be replaced'); +}, 'Change composite value'); + +test(t => { + const anim = createDiv(t).animate({ left: '10px' }); + + anim.effect.composite = 'add'; + const keyframes = anim.effect.getKeyframes(); + assert_equals(keyframes[0].composite, 'auto', + 'unspecified keyframe composite value should be auto even ' + + 'if effect composite is set'); +}, 'Unspecified keyframe composite value when setting effect composite'); + +test(t => { + const anim = createDiv(t).animate({ left: '10px', composite: 'replace' }); + + anim.effect.composite = 'add'; + const keyframes = anim.effect.getKeyframes(); + assert_equals(keyframes[0].composite, 'replace', + 'specified keyframe composite value should not be overridden ' + + 'by setting effect composite'); +}, 'Specified keyframe composite value when setting effect composite'); + +</script> diff --git a/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/constructor.html b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/constructor.html new file mode 100644 index 0000000000..f9d552e63e --- /dev/null +++ b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/constructor.html @@ -0,0 +1,195 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>KeyframeEffect constructor</title> +<link rel="help" + href="https://drafts.csswg.org/web-animations/#dom-keyframeeffect-keyframeeffect"> +<link rel="help" + href="https://drafts.csswg.org/web-animations/#dom-keyframeeffectreadonly-keyframeeffectreadonly"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<script src="../../resources/easing-tests.js"></script> +<script src="../../resources/keyframe-utils.js"></script> +<script src="../../resources/keyframe-tests.js"></script> +<body> +<div id="log"></div> +<div id="target"></div> +<script> +'use strict'; + +const target = document.getElementById('target'); + +test(t => { + for (const frames of gEmptyKeyframeListTests) { + assert_equals(new KeyframeEffect(target, frames).getKeyframes().length, + 0, `number of frames for ${JSON.stringify(frames)}`); + } +}, 'A KeyframeEffect can be constructed with no frames'); + +test(t => { + for (const subtest of gEasingParsingTests) { + const easing = subtest[0]; + const expected = subtest[1]; + const effect = new KeyframeEffect(target, { + left: ['10px', '20px'] + }, { easing: easing }); + assert_equals(effect.getTiming().easing, expected, + `resulting easing for '${easing}'`); + } +}, 'easing values are parsed correctly when passed to the ' + + 'KeyframeEffect constructor in KeyframeEffectOptions'); + +test(t => { + for (const invalidEasing of gInvalidEasings) { + assert_throws_js(TypeError, () => { + new KeyframeEffect(target, null, { easing: invalidEasing }); + }, `TypeError is thrown for easing '${invalidEasing}'`); + } +}, 'Invalid easing values are correctly rejected when passed to the ' + + 'KeyframeEffect constructor in KeyframeEffectOptions'); + +test(t => { + const getKeyframe = + composite => ({ left: [ '10px', '20px' ], composite: composite }); + for (const composite of gGoodKeyframeCompositeValueTests) { + const effect = new KeyframeEffect(target, getKeyframe(composite)); + assert_equals(effect.getKeyframes()[0].composite, composite, + `resulting composite for '${composite}'`); + } + for (const composite of gBadKeyframeCompositeValueTests) { + assert_throws_js(TypeError, () => { + new KeyframeEffect(target, getKeyframe(composite)); + }); + } +}, 'composite values are parsed correctly when passed to the ' + + 'KeyframeEffect constructor in property-indexed keyframes'); + +test(t => { + const getKeyframes = composite => + [ + { offset: 0, left: '10px', composite: composite }, + { offset: 1, left: '20px' } + ]; + for (const composite of gGoodKeyframeCompositeValueTests) { + const effect = new KeyframeEffect(target, getKeyframes(composite)); + assert_equals(effect.getKeyframes()[0].composite, composite, + `resulting composite for '${composite}'`); + } + for (const composite of gBadKeyframeCompositeValueTests) { + assert_throws_js(TypeError, () => { + new KeyframeEffect(target, getKeyframes(composite)); + }); + } +}, 'composite values are parsed correctly when passed to the ' + + 'KeyframeEffect constructor in regular keyframes'); + +test(t => { + for (const composite of gGoodOptionsCompositeValueTests) { + const effect = new KeyframeEffect(target, { + left: ['10px', '20px'] + }, { composite }); + assert_equals(effect.getKeyframes()[0].composite, 'auto', + `resulting composite for '${composite}'`); + } + for (const composite of gBadOptionsCompositeValueTests) { + assert_throws_js(TypeError, () => { + new KeyframeEffect(target, { + left: ['10px', '20px'] + }, { composite: composite }); + }); + } +}, 'composite value is auto if the composite operation specified on the ' + + 'keyframe effect is being used'); + +for (const subtest of gKeyframesTests) { + test(t => { + const effect = new KeyframeEffect(target, subtest.input); + assert_frame_lists_equal(effect.getKeyframes(), subtest.output); + }, `A KeyframeEffect can be constructed with ${subtest.desc}`); + + test(t => { + const effect = new KeyframeEffect(target, subtest.input); + const secondEffect = new KeyframeEffect(target, effect.getKeyframes()); + assert_frame_lists_equal(secondEffect.getKeyframes(), + effect.getKeyframes()); + }, `A KeyframeEffect constructed with ${subtest.desc} roundtrips`); +} + +for (const subtest of gInvalidKeyframesTests) { + test(t => { + assert_throws_js(TypeError, () => { + new KeyframeEffect(target, subtest.input); + }); + }, `KeyframeEffect constructor throws with ${subtest.desc}`); +} + +test(t => { + const effect = new KeyframeEffect(target, { left: ['10px', '20px'] }); + + const timing = effect.getTiming(); + assert_equals(timing.delay, 0, 'default delay'); + assert_equals(timing.endDelay, 0, 'default endDelay'); + assert_equals(timing.fill, 'auto', 'default fill'); + assert_equals(timing.iterations, 1.0, 'default iterations'); + assert_equals(timing.iterationStart, 0.0, 'default iterationStart'); + assert_equals(timing.duration, 'auto', 'default duration'); + assert_equals(timing.direction, 'normal', 'default direction'); + assert_equals(timing.easing, 'linear', 'default easing'); + + assert_equals(effect.composite, 'replace', 'default composite'); + assert_equals(effect.iterationComposite, 'replace', + 'default iterationComposite'); +}, 'A KeyframeEffect constructed without any KeyframeEffectOptions object'); + +for (const subtest of gKeyframeEffectOptionTests) { + test(t => { + const effect = new KeyframeEffect(target, { left: ['10px', '20px'] }, + subtest.input); + + // Helper function to provide default expected values when the test does + // not supply them. + const expected = (field, defaultValue) => { + return field in subtest.expected ? subtest.expected[field] : defaultValue; + }; + + const timing = effect.getTiming(); + assert_equals(timing.delay, expected('delay', 0), + 'timing delay'); + assert_equals(timing.fill, expected('fill', 'auto'), + 'timing fill'); + assert_equals(timing.iterations, expected('iterations', 1), + 'timing iterations'); + assert_equals(timing.duration, expected('duration', 'auto'), + 'timing duration'); + assert_equals(timing.direction, expected('direction', 'normal'), + 'timing direction'); + + }, `A KeyframeEffect constructed by ${subtest.desc}`); +} + +for (const subtest of gInvalidKeyframeEffectOptionTests) { + test(t => { + assert_throws_js(TypeError, () => { + new KeyframeEffect(target, { left: ['10px', '20px'] }, subtest.input); + }); + }, `Invalid KeyframeEffect option by ${subtest.desc}`); +} + +test(t => { + const effect = new KeyframeEffect(null, { left: ['10px', '20px'] }, + { duration: 100 * MS_PER_SEC, + fill: 'forwards' }); + assert_equals(effect.target, null, + 'Effect created with null target has correct target'); +}, 'A KeyframeEffect constructed with null target'); + +test(t => { + const test_error = { name: 'test' }; + + assert_throws_exactly(test_error, () => { + new KeyframeEffect(target, { get left() { throw test_error }}) + }); +}, 'KeyframeEffect constructor propagates exceptions generated by accessing' + + ' the options object'); +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/copy-constructor.html b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/copy-constructor.html new file mode 100644 index 0000000000..e3bc0db00a --- /dev/null +++ b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/copy-constructor.html @@ -0,0 +1,93 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>KeyframeEffect copy constructor</title> +<link rel="help" + href="https://drafts.csswg.org/web-animations/#dom-keyframeeffect-keyframeeffect-source"> +<link rel="help" + href="https://drafts.csswg.org/web-animations/#dom-keyframeeffectreadonly-keyframeeffectreadonly-source"> +<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 effect = new KeyframeEffect(createDiv(t), null); + const copiedEffect = new KeyframeEffect(effect); + assert_equals(copiedEffect.target, effect.target, 'same target'); +}, 'Copied KeyframeEffect has the same target'); + +test(t => { + const effect = + new KeyframeEffect(null, + [ { marginLeft: '0px' }, + { marginLeft: '-20px', easing: 'ease-in', + offset: 0.1 }, + { marginLeft: '100px', easing: 'ease-out' }, + { marginLeft: '50px' } ]); + + const copiedEffect = new KeyframeEffect(effect); + const keyframesA = effect.getKeyframes(); + const keyframesB = copiedEffect.getKeyframes(); + assert_equals(keyframesA.length, keyframesB.length, 'same keyframes length'); + + for (let i = 0; i < keyframesA.length; ++i) { + assert_equals(keyframesA[i].offset, keyframesB[i].offset, + `Keyframe ${i} has the same offset`); + assert_equals(keyframesA[i].computedOffset, keyframesB[i].computedOffset, + `Keyframe ${i} has the same computedOffset`); + assert_equals(keyframesA[i].easing, keyframesB[i].easing, + `Keyframe ${i} has the same easing`); + assert_equals(keyframesA[i].composite, keyframesB[i].composite, + `Keyframe ${i} has the same composite`); + + assert_true(!!keyframesA[i].marginLeft, + `Original keyframe ${i} has a valid property value`); + assert_true(!!keyframesB[i].marginLeft, + `New keyframe ${i} has a valid property value`); + assert_equals(keyframesA[i].marginLeft, keyframesB[i].marginLeft, + `Keyframe ${i} has the same property value pair`); + } +}, 'Copied KeyframeEffect has the same keyframes'); + +test(t => { + const effect = + new KeyframeEffect(null, null, { iterationComposite: 'accumulate' }); + + const copiedEffect = new KeyframeEffect(effect); + assert_equals(copiedEffect.iterationComposite, effect.iterationComposite, + 'same iterationCompositeOperation'); + assert_equals(copiedEffect.composite, effect.composite, + 'same compositeOperation'); +}, 'Copied KeyframeEffect has the same KeyframeEffectOptions'); + +test(t => { + const effect = new KeyframeEffect(null, null, + { duration: 100 * MS_PER_SEC, + delay: -1 * MS_PER_SEC, + endDelay: 2 * MS_PER_SEC, + fill: 'forwards', + iterationStart: 2, + iterations: 20, + easing: 'ease-out', + direction: 'alternate' } ); + + const copiedEffect = new KeyframeEffect(effect); + const timingA = effect.getTiming(); + const timingB = copiedEffect.getTiming(); + assert_not_equals(timingA, timingB, 'different timing objects'); + assert_equals(timingA.delay, timingB.delay, 'same delay'); + assert_equals(timingA.endDelay, timingB.endDelay, 'same endDelay'); + assert_equals(timingA.fill, timingB.fill, 'same fill'); + assert_equals(timingA.iterationStart, timingB.iterationStart, + 'same iterationStart'); + assert_equals(timingA.iterations, timingB.iterations, 'same iterations'); + assert_equals(timingA.duration, timingB.duration, 'same duration'); + assert_equals(timingA.direction, timingB.direction, 'same direction'); + assert_equals(timingA.easing, timingB.easing, 'same easing'); +}, 'Copied KeyframeEffect has the same timing content'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/getKeyframes.html b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/getKeyframes.html new file mode 100644 index 0000000000..1f8d267e4a --- /dev/null +++ b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/getKeyframes.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>KeyframeEffect getKeyframes()</title> +<link rel="help" + href="https://drafts.csswg.org/web-animations/#dom-keyframeeffect-getkeyframes"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../resources/keyframe-utils.js"></script> +<script src="../../resources/keyframe-tests.js"></script> +<body> +<div id="log"></div> +<div id="target"></div> +<script> +'use strict'; + +const target = document.getElementById('target'); + + +for (const subtest of gKeyframeSerializationTests) { + test(t => { + const effect = new KeyframeEffect(target, subtest.input); + assert_frame_lists_equal(effect.getKeyframes(), subtest.output); + }, `getKeyframes() should serialize its css values with ${subtest.desc}`); +} +</script>
\ No newline at end of file diff --git a/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/iterationComposite.html b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/iterationComposite.html new file mode 100644 index 0000000000..bbb8ee2a32 --- /dev/null +++ b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/iterationComposite.html @@ -0,0 +1,36 @@ +<!doctype html> +<meta charset=utf-8> +<title>KeyframeEffect.iterationComposite</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#dom-keyframeeffect-iterationcomposite"> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src="../../testcommon.js"></script> +<div id="log"></div> +<script> +'use strict'; + +test(t => { + const div = createDiv(t); + const anim = div.animate({ marginLeft: ['0px', '10px'] }, + { duration: 100 * MS_PER_SEC, + easing: 'linear', + iterations: 10, + iterationComposite: 'accumulate' }); + anim.pause(); + + anim.currentTime = + anim.effect.getComputedTiming().duration * 2 + + anim.effect.getComputedTiming().duration / 2; + assert_equals(getComputedStyle(div).marginLeft, '25px', + 'Animated style at 50s of the third iteration'); + + anim.effect.iterationComposite = 'replace'; + assert_equals(getComputedStyle(div).marginLeft, '5px', + 'Animated style at 50s of the third iteration'); + + anim.effect.iterationComposite = 'accumulate'; + assert_equals(getComputedStyle(div).marginLeft, '25px', + 'Animated style at 50s of the third iteration'); +}, 'iterationComposite can be updated while an animation is in progress'); + +</script> diff --git a/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/processing-a-keyframes-argument-001.html b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/processing-a-keyframes-argument-001.html new file mode 100644 index 0000000000..271a47b301 --- /dev/null +++ b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/processing-a-keyframes-argument-001.html @@ -0,0 +1,602 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Processing a keyframes argument (property access)</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#processing-a-keyframes-argument"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<script src="../../resources/keyframe-utils.js"></script> +<body> +<div id="log"></div> +<div id="target"></div> +<script> +'use strict'; + +// This file only tests the KeyframeEffect constructor since it is +// assumed that the implementation of the KeyframeEffect constructor, +// Animatable.animate() method, and KeyframeEffect.setKeyframes() method will +// all share common machinery and it is not necessary to test each method. + +// Test that only animatable properties are accessed + +const gNonAnimatableProps = [ + 'animation', // Shorthands where all the longhand sub-properties are not + // animatable, are also not animatable. + 'animationDelay', + 'animationDirection', + 'animationDuration', + 'animationFillMode', + 'animationIterationCount', + 'animationName', + 'animationPlayState', + 'animationTimingFunction', + 'transition', + 'transitionDelay', + 'transitionDuration', + 'transitionProperty', + 'transitionTimingFunction', + 'contain', + 'direction', + 'display', + 'textCombineUpright', + 'textOrientation', + 'unicodeBidi', + 'willChange', + 'writingMode', + + 'unsupportedProperty', + + 'float', // We use the string "cssFloat" to represent "float" property, and + // so reject "float" in the keyframe-like object. + 'font-size', // Supported property that uses dashes +]; + +function TestKeyframe(testProp) { + let _propAccessCount = 0; + + Object.defineProperty(this, testProp, { + get: () => { _propAccessCount++; }, + enumerable: true, + }); + + Object.defineProperty(this, 'propAccessCount', { + get: () => _propAccessCount + }); +} + +function GetTestKeyframeSequence(testProp) { + return [ new TestKeyframe(testProp) ] +} + +for (const prop of gNonAnimatableProps) { + test(() => { + const testKeyframe = new TestKeyframe(prop); + + new KeyframeEffect(null, testKeyframe); + + assert_equals(testKeyframe.propAccessCount, 0, 'Accessor not called'); + }, `non-animatable property '${prop}' is not accessed when using` + + ' a property-indexed keyframe object'); +} + +for (const prop of gNonAnimatableProps) { + test(() => { + const testKeyframes = GetTestKeyframeSequence(prop); + + new KeyframeEffect(null, testKeyframes); + + assert_equals(testKeyframes[0].propAccessCount, 0, 'Accessor not called'); + }, `non-animatable property '${prop}' is not accessed when using` + + ' a keyframe sequence'); +} + +// Test equivalent forms of property-indexed and sequenced keyframe syntax + +function assertEquivalentKeyframeSyntax(keyframesA, keyframesB) { + const processedKeyframesA = + new KeyframeEffect(null, keyframesA).getKeyframes(); + const processedKeyframesB = + new KeyframeEffect(null, keyframesB).getKeyframes(); + assert_frame_lists_equal(processedKeyframesA, processedKeyframesB); +} + +const gEquivalentSyntaxTests = [ + { + description: 'two properties with one value', + indexedKeyframes: { + left: '100px', + opacity: ['1'], + }, + sequencedKeyframes: [ + { left: '100px', opacity: '1' }, + ], + }, + { + description: 'two properties with three values', + indexedKeyframes: { + left: ['10px', '100px', '150px'], + opacity: ['1', '0', '1'], + }, + sequencedKeyframes: [ + { left: '10px', opacity: '1' }, + { left: '100px', opacity: '0' }, + { left: '150px', opacity: '1' }, + ], + }, + { + description: 'two properties with different numbers of values', + indexedKeyframes: { + left: ['0px', '100px', '200px'], + opacity: ['0', '1'] + }, + sequencedKeyframes: [ + { left: '0px', opacity: '0' }, + { left: '100px' }, + { left: '200px', opacity: '1' }, + ], + }, + { + description: 'same easing applied to all keyframes', + indexedKeyframes: { + left: ['10px', '100px', '150px'], + opacity: ['1', '0', '1'], + easing: 'ease', + }, + sequencedKeyframes: [ + { left: '10px', opacity: '1', easing: 'ease' }, + { left: '100px', opacity: '0', easing: 'ease' }, + { left: '150px', opacity: '1', easing: 'ease' }, + ], + }, + { + description: 'same composite applied to all keyframes', + indexedKeyframes: { + left: ['0px', '100px'], + composite: 'add', + }, + sequencedKeyframes: [ + { left: '0px', composite: 'add' }, + { left: '100px', composite: 'add' }, + ], + }, +]; + +for (const {description, indexedKeyframes, sequencedKeyframes} of + gEquivalentSyntaxTests) { + test(() => { + assertEquivalentKeyframeSyntax(indexedKeyframes, sequencedKeyframes); + }, `Equivalent property-indexed and sequenced keyframes: ${description}`); +} + +// Test handling of custom iterable objects. + +function createIterable(iterations) { + return { + [Symbol.iterator]() { + let i = 0; + return { + next() { + return iterations[i++]; + }, + }; + }, + }; +} + +test(() => { + const effect = new KeyframeEffect(null, createIterable([ + { done: false, value: { left: '100px' } }, + { done: false, value: { left: '300px' } }, + { done: false, value: { left: '200px' } }, + { done: true }, + ])); + assert_frame_lists_equal(effect.getKeyframes(), [ + { + offset: null, + computedOffset: 0, + easing: 'linear', + left: '100px', + composite: 'auto', + }, + { + offset: null, + computedOffset: 0.5, + easing: 'linear', + left: '300px', + composite: 'auto', + }, + { + offset: null, + computedOffset: 1, + easing: 'linear', + left: '200px', + composite: 'auto', + }, + ]); +}, 'Keyframes are read from a custom iterator'); + +test(() => { + const keyframes = createIterable([ + { done: false, value: { left: '100px' } }, + { done: false, value: { left: '300px' } }, + { done: false, value: { left: '200px' } }, + { done: true }, + ]); + keyframes.easing = 'ease-in-out'; + keyframes.offset = '0.1'; + const effect = new KeyframeEffect(null, keyframes); + assert_frame_lists_equal(effect.getKeyframes(), [ + { + offset: null, + computedOffset: 0, + easing: 'linear', + left: '100px', + composite: 'auto', + }, + { + offset: null, + computedOffset: 0.5, + easing: 'linear', + left: '300px', + composite: 'auto', + }, + { + offset: null, + computedOffset: 1, + easing: 'linear', + left: '200px', + composite: 'auto', + }, + ]); +}, '\'easing\' and \'offset\' are ignored on iterable objects'); + +test(() => { + const effect = new KeyframeEffect(null, createIterable([ + { done: false, value: { left: '100px', top: '200px' } }, + { done: false, value: { left: '300px' } }, + { done: false, value: { left: '200px', top: '100px' } }, + { done: true }, + ])); + assert_frame_lists_equal(effect.getKeyframes(), [ + { + offset: null, + computedOffset: 0, + easing: 'linear', + left: '100px', + top: '200px', + composite: 'auto', + }, + { + offset: null, + computedOffset: 0.5, + easing: 'linear', + left: '300px', + composite: 'auto', + }, + { + offset: null, + computedOffset: 1, + easing: 'linear', + left: '200px', + top: '100px', + composite: 'auto', + }, + ]); +}, 'Keyframes are read from a custom iterator with multiple properties' + + ' specified'); + +test(() => { + const effect = new KeyframeEffect(null, createIterable([ + { done: false, value: { left: '100px' } }, + { done: false, value: { left: '250px', offset: 0.75 } }, + { done: false, value: { left: '200px' } }, + { done: true }, + ])); + assert_frame_lists_equal(effect.getKeyframes(), [ + { + offset: null, + computedOffset: 0, + easing: 'linear', + left: '100px', + composite: 'auto', + }, + { + offset: 0.75, + computedOffset: 0.75, + easing: 'linear', + left: '250px', + composite: 'auto', + }, + { + offset: null, + computedOffset: 1, + easing: 'linear', + left: '200px', + composite: 'auto', + }, + ]); +}, 'Keyframes are read from a custom iterator with where an offset is' + + ' specified'); + +test(() => { + const test_error = { name: 'test' }; + const bad_keyframe = { get left() { throw test_error; } }; + assert_throws_exactly(test_error, () => { + new KeyframeEffect(null, createIterable([ + { done: false, value: { left: '100px' } }, + { done: false, value: bad_keyframe }, + { done: false, value: { left: '200px' } }, + { done: true }, + ])); + }); +}, 'If a keyframe throws for an animatable property, that exception should be' + + ' propagated'); + +test(() => { + assert_throws_js(TypeError, () => { + new KeyframeEffect(null, createIterable([ + { done: false, value: { left: '100px' } }, + { done: false, value: 1234 }, + { done: false, value: { left: '200px' } }, + { done: true }, + ])); + }); +}, 'Reading from a custom iterator that returns a non-object keyframe' + + ' should throw'); + +test(() => { + assert_throws_js(TypeError, () => { + new KeyframeEffect(null, createIterable([ + { done: false, value: { left: '100px', easing: '' } }, + { done: false, value: 1234 }, + { done: false, value: { left: '200px' } }, + { done: true }, + ])); + }); +}, 'Reading from a custom iterator that returns a non-object keyframe' + + ' and an invalid easing should throw'); + +test(() => { + assert_throws_js(TypeError, () => { + new KeyframeEffect(null, createIterable([ + { done: false, value: { left: '100px' } }, + { done: false, value: { left: '150px', offset: 'o' } }, + { done: false, value: { left: '200px' } }, + { done: true }, + ])); + }); +}, 'Reading from a custom iterator that returns a keyframe with a non finite' + + ' floating-point offset value should throw'); + +test(() => { + assert_throws_js(TypeError, () => { + new KeyframeEffect(null, createIterable([ + { done: false, value: { left: '100px', easing: '' } }, + { done: false, value: { left: '150px', offset: 'o' } }, + { done: false, value: { left: '200px' } }, + { done: true }, + ])); + }); +}, 'Reading from a custom iterator that returns a keyframe with a non finite' + + ' floating-point offset value and an invalid easing should throw'); + +test(() => { + const effect = new KeyframeEffect(null, createIterable([ + { done: false, value: { left: '100px' } }, + { done: false }, // No value member; keyframe is undefined. + { done: false, value: { left: '200px' } }, + { done: true }, + ])); + assert_frame_lists_equal(effect.getKeyframes(), [ + { left: '100px', offset: null, computedOffset: 0, easing: 'linear', composite: 'auto' }, + { offset: null, computedOffset: 0.5, easing: 'linear', composite: 'auto' }, + { left: '200px', offset: null, computedOffset: 1, easing: 'linear', composite: 'auto' }, + ]); +}, 'An undefined keyframe returned from a custom iterator should be treated as a' + + ' default keyframe'); + +test(() => { + const effect = new KeyframeEffect(null, createIterable([ + { done: false, value: { left: '100px' } }, + { done: false, value: null }, + { done: false, value: { left: '200px' } }, + { done: true }, + ])); + assert_frame_lists_equal(effect.getKeyframes(), [ + { left: '100px', offset: null, computedOffset: 0, easing: 'linear', composite: 'auto' }, + { offset: null, computedOffset: 0.5, easing: 'linear', composite: 'auto' }, + { left: '200px', offset: null, computedOffset: 1, easing: 'linear', composite: 'auto' }, + ]); +}, 'A null keyframe returned from a custom iterator should be treated as a' + + ' default keyframe'); + +test(() => { + const effect = new KeyframeEffect(null, createIterable([ + { done: false, value: { left: ['100px', '200px'] } }, + { done: true }, + ])); + assert_frame_lists_equal(effect.getKeyframes(), [ + { offset: null, computedOffset: 1, easing: 'linear', composite: 'auto' } + ]); +}, 'A list of values returned from a custom iterator should be ignored'); + +test(() => { + const test_error = { name: 'test' }; + const keyframe_obj = { + [Symbol.iterator]() { + return { next() { throw test_error; } }; + }, + }; + assert_throws_exactly(test_error, () => { + new KeyframeEffect(null, keyframe_obj); + }); +}, 'If a custom iterator throws from next(), the exception should be rethrown'); + +// Test handling of invalid Symbol.iterator + +test(() => { + const test_error = { name: 'test' }; + const keyframe_obj = { + [Symbol.iterator]() { + throw test_error; + }, + }; + assert_throws_exactly(test_error, () => { + new KeyframeEffect(null, keyframe_obj); + }); +}, 'Accessing a Symbol.iterator property that throws should rethrow'); + +test(() => { + const keyframe_obj = { + [Symbol.iterator]() { + return 42; // Not an object. + }, + }; + assert_throws_js(TypeError, () => { + new KeyframeEffect(null, keyframe_obj); + }); +}, 'A non-object returned from the Symbol.iterator property should cause a' + + ' TypeError to be thrown'); + +test(() => { + const keyframe = {}; + Object.defineProperty(keyframe, 'width', { value: '200px' }); + Object.defineProperty(keyframe, 'height', { + value: '100px', + enumerable: true, + }); + assert_equals(keyframe.width, '200px', 'width of keyframe is readable'); + assert_equals(keyframe.height, '100px', 'height of keyframe is readable'); + + const effect = new KeyframeEffect(null, [keyframe, { height: '200px' }]); + + assert_frame_lists_equal(effect.getKeyframes(), [ + { + offset: null, + computedOffset: 0, + easing: 'linear', + height: '100px', + composite: 'auto', + }, + { + offset: null, + computedOffset: 1, + easing: 'linear', + height: '200px', + composite: 'auto', + }, + ]); +}, 'Only enumerable properties on keyframes are read'); + +test(() => { + const KeyframeParent = function() { this.width = '100px'; }; + KeyframeParent.prototype = { height: '100px' }; + const Keyframe = function() { this.top = '100px'; }; + Keyframe.prototype = Object.create(KeyframeParent.prototype); + Object.defineProperty(Keyframe.prototype, 'left', { + value: '100px', + enumerable: true, + }); + const keyframe = new Keyframe(); + + const effect = new KeyframeEffect(null, [keyframe, { top: '200px' }]); + + assert_frame_lists_equal(effect.getKeyframes(), [ + { + offset: null, + computedOffset: 0, + easing: 'linear', + top: '100px', + composite: 'auto', + }, + { + offset: null, + computedOffset: 1, + easing: 'linear', + top: '200px', + composite: 'auto', + }, + ]); +}, 'Only properties defined directly on keyframes are read'); + +test(() => { + const keyframes = {}; + Object.defineProperty(keyframes, 'width', ['100px', '200px']); + Object.defineProperty(keyframes, 'height', { + value: ['100px', '200px'], + enumerable: true, + }); + + const effect = new KeyframeEffect(null, keyframes); + + assert_frame_lists_equal(effect.getKeyframes(), [ + { + offset: null, + computedOffset: 0, + easing: 'linear', + height: '100px', + composite: 'auto', + }, + { + offset: null, + computedOffset: 1, + easing: 'linear', + height: '200px', + composite: 'auto', + }, + ]); +}, 'Only enumerable properties on property-indexed keyframes are read'); + +test(() => { + const KeyframesParent = function() { this.width = '100px'; }; + KeyframesParent.prototype = { height: '100px' }; + const Keyframes = function() { this.top = ['100px', '200px']; }; + Keyframes.prototype = Object.create(KeyframesParent.prototype); + Object.defineProperty(Keyframes.prototype, 'left', { + value: ['100px', '200px'], + enumerable: true, + }); + const keyframes = new Keyframes(); + + const effect = new KeyframeEffect(null, keyframes); + + assert_frame_lists_equal(effect.getKeyframes(), [ + { + offset: null, + computedOffset: 0, + easing: 'linear', + top: '100px', + composite: 'auto', + }, + { + offset: null, + computedOffset: 1, + easing: 'linear', + top: '200px', + composite: 'auto', + }, + ]); +}, 'Only properties defined directly on property-indexed keyframes are read'); + +test(() => { + const expectedOrder = ['composite', 'easing', 'offset', 'left', 'marginLeft']; + const actualOrder = []; + const kf1 = {}; + for (const {prop, value} of [{ prop: 'marginLeft', value: '10px' }, + { prop: 'left', value: '20px' }, + { prop: 'offset', value: '0' }, + { prop: 'easing', value: 'linear' }, + { prop: 'composite', value: 'replace' }]) { + Object.defineProperty(kf1, prop, { + enumerable: true, + get: () => { actualOrder.push(prop); return value; } + }); + } + const kf2 = { marginLeft: '10px', left: '20px', offset: 1 }; + + new KeyframeEffect(target, [kf1, kf2]); + + assert_array_equals(actualOrder, expectedOrder, 'property access order'); +}, 'Properties are read in ascending order by Unicode codepoint'); + +</script> diff --git a/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/processing-a-keyframes-argument-002.html b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/processing-a-keyframes-argument-002.html new file mode 100644 index 0000000000..8620f883f9 --- /dev/null +++ b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/processing-a-keyframes-argument-002.html @@ -0,0 +1,125 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Processing a keyframes argument (easing)</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#processing-a-keyframes-argument"> +<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'; + +test(() => { + for (const [easing, expected] of gEasingParsingTests) { + const effect = new KeyframeEffect(target, { + left: ['10px', '20px'], + easing: easing + }); + assert_equals(effect.getKeyframes()[0].easing, expected, + `resulting easing for '${easing}'`); + } +}, 'easing values are parsed correctly when set on a property-indexed' + + ' keyframe'); + +test(() => { + for (const [easing, expected] of gEasingParsingTests) { + const effect = new KeyframeEffect(target, [ + { offset: 0, left: '10px', easing: easing }, + { offset: 1, left: '20px' } + ]); + assert_equals(effect.getKeyframes()[0].easing, expected, + `resulting easing for '${easing}'`); + } +}, 'easing values are parsed correctly when using a keyframe sequence'); + +test(() => { + for (const invalidEasing of gInvalidEasings) { + assert_throws_js(TypeError, () => { + new KeyframeEffect(target, { easing: invalidEasing }); + }, `TypeError is thrown for easing '${invalidEasing}'`); + } +}, 'Invalid easing values are correctly rejected when set on a property-' + + 'indexed keyframe'); + +test(() => { + for (const invalidEasing of gInvalidEasings) { + assert_throws_js(TypeError, () => { + new KeyframeEffect(target, [{ easing: invalidEasing }]); + }, `TypeError is thrown for easing '${invalidEasing}'`); + } +}, 'Invalid easing values are correctly rejected when using a keyframe' + + ' sequence'); + +test(() => { + let readToEnd = false; + const keyframe_obj = { + *[Symbol.iterator]() { + yield { left: '100px', easing: '' }; + yield { left: '200px' }; + readToEnd = true; + }, + }; + assert_throws_js( + TypeError, + () => { + new KeyframeEffect(null, keyframe_obj); + }, + 'TypeError is thrown for an invalid easing' + ); + assert_true( + readToEnd, + 'Read all the keyframe properties before reporting invalid easing' + ); +}, 'Invalid easing values are correctly rejected after doing all the' + + ' iterating'); + +test(() => { + let propAccessCount = 0; + const keyframe = {}; + const addProp = prop => { + Object.defineProperty(keyframe, prop, { + get: () => { propAccessCount++; }, + enumerable: true + }); + } + addProp('height'); + addProp('width'); + keyframe.easing = 'easy-peasy'; + + assert_throws_js(TypeError, () => { + new KeyframeEffect(target, keyframe); + }); + assert_equals(propAccessCount, 2, + 'All properties were read before throwing the easing error'); +}, 'Errors from invalid easings on a property-indexed keyframe are thrown after reading all properties'); + +test(() => { + let propAccessCount = 0; + + const addProp = (keyframe, prop) => { + Object.defineProperty(keyframe, prop, { + get: () => { propAccessCount++; }, + enumerable: true + }); + } + + const kf1 = {}; + addProp(kf1, 'height'); + addProp(kf1, 'width'); + kf1.easing = 'easy-peasy'; + + const kf2 = {}; + addProp(kf2, 'height'); + addProp(kf2, 'width'); + + assert_throws_js(TypeError, () => { + new KeyframeEffect(target, [ kf1, kf2 ]); + }); + assert_equals(propAccessCount, 4, + 'All properties were read before throwing the easing error'); +}, 'Errors from invalid easings on a keyframe sequence are thrown after reading all properties'); + +</script> diff --git a/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/setKeyframes.html b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/setKeyframes.html new file mode 100644 index 0000000000..a5c81a29bd --- /dev/null +++ b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/setKeyframes.html @@ -0,0 +1,55 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>KeyframeEffect.setKeyframes</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#dom-keyframeeffect-setkeyframes"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<script src="../../resources/keyframe-utils.js"></script> +<script src="../../resources/keyframe-tests.js"></script> +<body> +<div id="log"></div> +<div id="target"></div> +<script> +'use strict'; + +const target = document.getElementById('target'); + +test(t => { + for (const frame of gEmptyKeyframeListTests) { + const effect = new KeyframeEffect(target, {}); + effect.setKeyframes(frame); + assert_frame_lists_equal(effect.getKeyframes(), []); + } +}, 'Keyframes can be replaced with an empty keyframe'); + +for (const subtest of gKeyframesTests) { + test(t => { + const effect = new KeyframeEffect(target, {}); + effect.setKeyframes(subtest.input); + assert_frame_lists_equal(effect.getKeyframes(), subtest.output); + }, `Keyframes can be replaced with ${subtest.desc}`); +} + +for (const subtest of gInvalidKeyframesTests) { + test(t => { + const effect = new KeyframeEffect(target, {}); + assert_throws_js(TypeError, () => { + effect.setKeyframes(subtest.input); + }); + }, `KeyframeEffect constructor throws with ${subtest.desc}`); +} + +test(t => { + const frames1 = [ { left: '100px' }, { left: '200px' } ]; + const frames2 = [ { left: '200px' }, { left: '300px' } ]; + + const animation = target.animate(frames1, 1000); + animation.currentTime = 500; + assert_equals(getComputedStyle(target).left, "150px"); + + animation.effect.setKeyframes(frames2); + assert_equals(getComputedStyle(target).left, "250px"); +}, 'Changes made via setKeyframes should be immediately visible in style'); +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/style-change-events.html b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/style-change-events.html new file mode 100644 index 0000000000..eecf170cd9 --- /dev/null +++ b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/style-change-events.html @@ -0,0 +1,242 @@ +<!doctype html> +<meta charset=utf-8> +<title>KeyframeEffect interface: style change events</title> +<link rel="help" + href="https://drafts.csswg.org/web-animations-1/#model-liveness"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<body> +<div id="log"></div> +<script> +'use strict'; + +// Test that each property defined in the KeyframeEffect interface does not +// produce style change events. +// +// There are two types of tests: +// +// MakeInEffectTest +// +// For properties that are able to cause the KeyframeEffect to start +// affecting the CSS 'opacity' property. +// +// This function takes either: +// +// (a) A function that makes the passed-in KeyframeEffect affect the +// 'opacity' property. +// +// (b) An object with the following format: +// +// { +// setup: elem => { /* return Animation */ } +// test: effect => { /* make |effect| affect 'opacity' */ } +// } +// +// If the latter form is used, the setup function should return an Animation +// whose KeyframeEffect does NOT (yet) affect the 'opacity' property (or is +// NOT yet in-effect). Otherwise, the transition we use to detect if a style +// change event has occurred will never have a chance to be triggered (since +// the animated style will clobber both before-change and after-change +// style). +// +// UsePropertyTest +// +// For properties that cannot cause the KeyframeEffect to start affecting the +// CSS 'opacity' property. +// +// The shape of the parameter to the UsePropertyTest is identical to the +// MakeInEffectTest. The only difference is that the function (or 'test' +// function of the object format is used) does not need to make the +// KeyframeEffect affect the CSS 'opacity' property, but simply needs to +// get/set the property under test. + +const MakeInEffectTest = testFuncOrObj => { + let test, setup; + + if (typeof testFuncOrObj === 'function') { + test = testFuncOrObj; + } else { + test = testFuncOrObj.test; + if (typeof testFuncOrObj.setup === 'function') { + setup = testFuncOrObj.setup; + } + } + + if (!setup) { + setup = elem => + elem.animate({ color: ['blue', 'green'] }, 100 * MS_PER_SEC); + } + + return { test, setup }; +}; + +const UsePropertyTest = testFuncOrObj => { + const { test, setup } = MakeInEffectTest(testFuncOrObj); + + let coveringAnimation; + return { + setup: elem => { + coveringAnimation = new Animation( + new KeyframeEffect(elem, { opacity: [0, 1] }, 100 * MS_PER_SEC) + ); + + return setup(elem); + }, + test: effect => { + test(effect); + coveringAnimation.play(); + }, + }; +}; + +const tests = { + getTiming: UsePropertyTest(effect => effect.getTiming()), + getComputedTiming: UsePropertyTest(effect => effect.getComputedTiming()), + updateTiming: MakeInEffectTest({ + // Initially put the effect in its before phase (with no fill mode)... + setup: elem => + elem.animate( + { opacity: [0.5, 1] }, + { + duration: 100 * MS_PER_SEC, + delay: 100 * MS_PER_SEC, + } + ), + // ... so that when the delay is removed, it begins to affect the opacity. + test: effect => { + effect.updateTiming({ delay: 0 }); + }, + }), + get target() { + let targetElem; + return MakeInEffectTest({ + setup: (elem, t) => { + targetElem = elem; + const targetB = createDiv(t); + return targetB.animate({ opacity: [0.5, 1] }, 100 * MS_PER_SEC); + }, + test: effect => { + effect.target = targetElem; + }, + }); + }, + pseudoElement: MakeInEffectTest({ + setup: elem => elem.animate( + {opacity: [0.5, 1]}, + {duration: 100 * MS_PER_SEC, pseudoElement: '::before'} + ), + test: effect => { + effect.pseudoElement = null; + }, + }), + iterationComposite: UsePropertyTest(effect => { + // Get iterationComposite + effect.iterationComposite; + + // Set iterationComposite + effect.iterationComposite = 'accumulate'; + }), + composite: UsePropertyTest(effect => { + // Get composite + effect.composite; + + // Set composite + effect.composite = 'add'; + }), + getKeyframes: UsePropertyTest(effect => effect.getKeyframes()), + setKeyframes: MakeInEffectTest(effect => + effect.setKeyframes({ opacity: [0.5, 1] }) + ), + get ['KeyframeEffect constructor']() { + let originalElem; + let animation; + return UsePropertyTest({ + setup: elem => { + originalElem = elem; + // Return a dummy animation so the caller has something to wait on + return elem.animate(null); + }, + test: () => + new KeyframeEffect( + originalElem, + { opacity: [0.5, 1] }, + 100 * MS_PER_SEC + ), + }); + }, + get ['KeyframeEffect copy constructor']() { + let effectToClone; + return UsePropertyTest({ + setup: elem => { + effectToClone = new KeyframeEffect( + elem, + { opacity: [0.5, 1] }, + 100 * MS_PER_SEC + ); + // Return a dummy animation so the caller has something to wait on + return elem.animate(null); + }, + test: () => new KeyframeEffect(effectToClone), + }); + }, +}; + +// Check that each enumerable property and the constructors follow the +// expected behavior with regards to triggering style change events. +const properties = [ + ...Object.keys(AnimationEffect.prototype), + ...Object.keys(KeyframeEffect.prototype), + 'KeyframeEffect constructor', + 'KeyframeEffect copy constructor', +]; + +test(() => { + for (const property of Object.keys(tests)) { + assert_in_array( + property, + properties, + `Test property '${property}' should be one of the properties on ` + + ' KeyframeEffect' + ); + } +}, 'All property keys are recognized'); + +for (const key of properties) { + promise_test(async t => { + assert_own_property(tests, key, `Should have a test for '${key}' property`); + const { setup, test } = tests[key]; + + // Setup target element + const div = createDiv(t); + let gotTransition = false; + div.addEventListener('transitionrun', () => { + gotTransition = true; + }); + + // Setup animation + const animation = setup(div, t); + + // Setup transition start point + div.style.transition = 'opacity 100s'; + getComputedStyle(div).opacity; + + // Update specified style but don't flush + div.style.opacity = '0.5'; + + // Trigger the property + test(animation.effect); + + // If the test function produced a style change event it will have triggered + // a transition. + + // Wait for the animation to start and then for at least one animation + // frame to give the transitionrun event a chance to be dispatched. + await animation.ready; + await waitForAnimationFrames(1); + + assert_false(gotTransition, 'A transition should NOT have been triggered'); + }, `KeyframeEffect.${key} does NOT trigger a style change event`); +} +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/target.html b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/target.html new file mode 100644 index 0000000000..30b2ee6f0c --- /dev/null +++ b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/target.html @@ -0,0 +1,266 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>KeyframeEffect.target and .pseudoElement</title> +<link rel="help" + href="https://drafts.csswg.org/web-animations/#dom-keyframeeffect-target"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<style> + .before::before {content: 'foo'; display: inline-block;} + .after::after {content: 'bar'; display: inline-block;} + .pseudoa::before, .pseudoc::before {margin-left: 10px;} + .pseudob::before, .pseudoc::after {margin-left: 20px;} +</style> +<body> +<div id="log"></div> +<script> +'use strict'; + +const gKeyFrames = { 'marginLeft': ['0px', '100px'] }; + +test(t => { + const div = createDiv(t); + const effect = new KeyframeEffect(null, gKeyFrames, 100 * MS_PER_SEC); + effect.target = div; + + const anim = new Animation(effect, document.timeline); + anim.play(); + + anim.currentTime = 50 * MS_PER_SEC; + assert_equals(getComputedStyle(div).marginLeft, '50px', + 'Value at 50% progress'); +}, 'Test setting target before constructing the associated animation'); + +test(t => { + const div = createDiv(t); + div.style.marginLeft = '10px'; + const effect = new KeyframeEffect(null, gKeyFrames, 100 * MS_PER_SEC); + const anim = new Animation(effect, document.timeline); + anim.play(); + + anim.currentTime = 50 * MS_PER_SEC; + assert_equals(getComputedStyle(div).marginLeft, '10px', + 'Value at 50% progress before setting new target'); + effect.target = div; + assert_equals(getComputedStyle(div).marginLeft, '50px', + 'Value at 50% progress after setting new target'); +}, 'Test setting target from null to a valid target'); + +test(t => { + const div = createDiv(t); + div.style.marginLeft = '10px'; + const anim = div.animate(gKeyFrames, 100 * MS_PER_SEC); + + anim.currentTime = 50 * MS_PER_SEC; + assert_equals(getComputedStyle(div).marginLeft, '50px', + 'Value at 50% progress before clearing the target') + + anim.effect.target = null; + assert_equals(getComputedStyle(div).marginLeft, '10px', + 'Value after clearing the target') +}, 'Test setting target from a valid target to null'); + +test(t => { + const a = createDiv(t); + const b = createDiv(t); + a.style.marginLeft = '10px'; + b.style.marginLeft = '20px'; + const anim = a.animate(gKeyFrames, 100 * MS_PER_SEC); + + anim.currentTime = 50 * MS_PER_SEC; + assert_equals(getComputedStyle(a).marginLeft, '50px', + 'Value of 1st element (currently targeted) before ' + + 'changing the effect target'); + assert_equals(getComputedStyle(b).marginLeft, '20px', + 'Value of 2nd element (currently not targeted) before ' + + 'changing the effect target'); + anim.effect.target = b; + assert_equals(getComputedStyle(a).marginLeft, '10px', + 'Value of 1st element (currently not targeted) after ' + + 'changing the effect target'); + assert_equals(getComputedStyle(b).marginLeft, '50px', + 'Value of 2nd element (currently targeted) after ' + + 'changing the effect target'); + + // This makes sure the animation property is changed correctly on new + // targeted element. + anim.currentTime = 75 * MS_PER_SEC; + assert_equals(getComputedStyle(b).marginLeft, '75px', + 'Value of 2nd target (currently targeted) after ' + + 'changing the animation current time.'); +}, 'Test setting target from a valid target to another target'); + +promise_test(async t => { + const animation = createDiv(t).animate( + { opacity: 0 }, + { duration: 1, fill: 'forwards' } + ); + + const foreignElement + = document.createElementNS('http://example.org/test', 'test'); + document.body.appendChild(foreignElement); + t.add_cleanup(() => { + foreignElement.remove(); + }); + + animation.effect.target = foreignElement; + + // Wait a frame to make sure nothing bad happens when the UA tries to update + // style. + await waitForNextFrame(); +}, 'Target element can be set to a foreign element'); + +// Pseudo-element tests +// (testing target and pseudoElement in these cases) +// Since blink uses separate code paths for handling pseudo-element styles +// depending on whether content is set (putting the pseudo-element in the layout), +// we run tests on both cases. +for (const hasContent of [true, false]){ + test(t => { + const d = createDiv(t); + d.classList.add('pseudoa'); + if (hasContent) { + d.classList.add('before'); + } + + const effect = new KeyframeEffect(null, gKeyFrames, 100 * MS_PER_SEC); + const anim = new Animation(effect, document.timeline); + anim.play(); + + anim.currentTime = 50 * MS_PER_SEC; + assert_equals(getComputedStyle(d, '::before').marginLeft, '10px', + 'Value at 50% progress before setting new target'); + effect.target = d; + effect.pseudoElement = '::before'; + + assert_equals(effect.target, d, "Target element is set correctly"); + assert_equals(effect.pseudoElement, '::before', "Target pseudo-element set correctly"); + assert_equals(getComputedStyle(d, '::before').marginLeft, '50px', + 'Value at 50% progress after setting new target'); + }, "Change target from null to " + (hasContent ? "an existing" : "a non-existing") + + " pseudoElement setting target first."); + + test(t => { + const d = createDiv(t); + d.classList.add('pseudoa'); + if (hasContent) { + d.classList.add('before'); + } + + const effect = new KeyframeEffect(null, gKeyFrames, 100 * MS_PER_SEC); + const anim = new Animation(effect, document.timeline); + anim.play(); + + anim.currentTime = 50 * MS_PER_SEC; + assert_equals(getComputedStyle(d, '::before').marginLeft, '10px', + 'Value at 50% progress before setting new target'); + effect.pseudoElement = '::before'; + effect.target = d; + + assert_equals(effect.target, d, "Target element is set correctly"); + assert_equals(effect.pseudoElement, '::before', "Target pseudo-element set correctly"); + assert_equals(getComputedStyle(d, '::before').marginLeft, '50px', + 'Value at 50% progress after setting new target'); + }, "Change target from null to " + (hasContent ? "an existing" : "a non-existing") + + " pseudoElement setting pseudoElement first."); + + test(t => { + const d = createDiv(t); + d.classList.add('pseudoa'); + if (hasContent) { + d.classList.add('before'); + } + const anim = d.animate(gKeyFrames, {duration: 100 * MS_PER_SEC, pseudoElement: '::before'}); + + anim.currentTime = 50 * MS_PER_SEC; + anim.effect.pseudoElement = null; + assert_equals(anim.effect.target, d, + "Animation targets specified element (target element)"); + assert_equals(anim.effect.pseudoElement, null, + "Animation targets specified element (null pseudo-selector)"); + assert_equals(getComputedStyle(d, '::before').marginLeft, '10px', + 'Value of 1st element (currently not targeted) after ' + + 'changing the effect target'); + assert_equals(getComputedStyle(d).marginLeft, '50px', + 'Value of 2nd element (currently targeted) after ' + + 'changing the effect target'); + }, "Change target from " + (hasContent ? "an existing" : "a non-existing") + " pseudo-element to the originating element."); + + for (const prevHasContent of [true, false]) { + test(t => { + const a = createDiv(t); + a.classList.add('pseudoa'); + const b = createDiv(t); + b.classList.add('pseudob'); + if (prevHasContent) { + a.classList.add('before'); + } + if (hasContent) { + b.classList.add('before'); + } + + const anim = a.animate(gKeyFrames, {duration: 100 * MS_PER_SEC, pseudoElement: '::before'}); + + anim.currentTime = 50 * MS_PER_SEC; + anim.effect.target = b; + assert_equals(anim.effect.target, b, + "Animation targets specified pseudo-element (target element)"); + assert_equals(anim.effect.pseudoElement, '::before', + "Animation targets specified pseudo-element (pseudo-selector)"); + assert_equals(getComputedStyle(a, '::before').marginLeft, '10px', + 'Value of 1st element (currently not targeted) after ' + + 'changing the effect target'); + assert_equals(getComputedStyle(b, '::before').marginLeft, '50px', + 'Value of 2nd element (currently targeted) after ' + + 'changing the effect target'); + }, "Change target from " + (prevHasContent ? "an existing" : "a non-existing") + + " to a different " + (hasContent ? "existing" : "non-existing") + + " pseudo-element by setting target."); + + test(t => { + const d = createDiv(t); + d.classList.add('pseudoc'); + if (prevHasContent) { + d.classList.add('before'); + } + if (hasContent) { + d.classList.add('after'); + } + + const anim = d.animate(gKeyFrames, {duration: 100 * MS_PER_SEC, pseudoElement: '::before'}); + + anim.currentTime = 50 * MS_PER_SEC; + anim.effect.pseudoElement = '::after'; + assert_equals(anim.effect.target, d, + "Animation targets specified pseudo-element (target element)"); + assert_equals(anim.effect.pseudoElement, '::after', + "Animation targets specified pseudo-element (pseudo-selector)"); + assert_equals(getComputedStyle(d, '::before').marginLeft, '10px', + 'Value of 1st element (currently not targeted) after ' + + 'changing the effect target'); + assert_equals(getComputedStyle(d, '::after').marginLeft, '50px', + 'Value of 2nd element (currently targeted) after ' + + 'changing the effect target'); + }, "Change target from " + (prevHasContent ? "an existing" : "a non-existing") + + " to a different " + (hasContent ? "existing" : "non-existing") + + " pseudo-element by setting pseudoElement."); + } +} + +for (const pseudo of [ + '', + 'before', + ':abc', + '::abc', + '::placeholder', +]) { + test(t => { + const effect = new KeyframeEffect(null, gKeyFrames, 100 * MS_PER_SEC); + assert_throws_dom("SyntaxError", () => effect.pseudoElement = pseudo ); + }, `Changing pseudoElement to a non-null invalid pseudo-selector ` + + `'${pseudo}' throws a SyntaxError`); +} + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/resources/easing-tests.js b/testing/web-platform/tests/web-animations/resources/easing-tests.js new file mode 100644 index 0000000000..a05264b0f5 --- /dev/null +++ b/testing/web-platform/tests/web-animations/resources/easing-tests.js @@ -0,0 +1,121 @@ +'use strict'; + +const gEasingTests = [ + { + desc: 'step-start function', + easing: 'step-start', + easingFunction: stepStart(1), + serialization: 'steps(1, start)' + }, + { + desc: 'steps(1, start) function', + easing: 'steps(1, start)', + easingFunction: stepStart(1) + }, + { + desc: 'steps(2, start) function', + easing: 'steps(2, start)', + easingFunction: stepStart(2) + }, + { + desc: 'step-end function', + easing: 'step-end', + easingFunction: stepEnd(1), + serialization: 'steps(1)' + }, + { + desc: 'steps(1) function', + easing: 'steps(1)', + easingFunction: stepEnd(1) + }, + { + desc: 'steps(1, end) function', + easing: 'steps(1, end)', + easingFunction: stepEnd(1), + serialization: 'steps(1)' + }, + { + desc: 'steps(2, end) function', + easing: 'steps(2, end)', + easingFunction: stepEnd(2), + serialization: 'steps(2)' + }, + { + desc: 'linear function', + easing: 'linear', // cubic-bezier(0, 0, 1.0, 1.0) + easingFunction: cubicBezier(0, 0, 1.0, 1.0) + }, + { + desc: 'ease function', + easing: 'ease', // cubic-bezier(0.25, 0.1, 0.25, 1.0) + easingFunction: cubicBezier(0.25, 0.1, 0.25, 1.0) + }, + { + desc: 'ease-in function', + easing: 'ease-in', // cubic-bezier(0.42, 0, 1.0, 1.0) + easingFunction: cubicBezier(0.42, 0, 1.0, 1.0) + }, + { + desc: 'ease-in-out function', + easing: 'ease-in-out', // cubic-bezier(0.42, 0, 0.58, 1.0) + easingFunction: cubicBezier(0.42, 0, 0.58, 1.0) + }, + { + desc: 'ease-out function', + easing: 'ease-out', // cubic-bezier(0, 0, 0.58, 1.0) + easingFunction: cubicBezier(0, 0, 0.58, 1.0) + }, + { + desc: 'easing function which produces values greater than 1', + easing: 'cubic-bezier(0, 1.5, 1, 1.5)', + easingFunction: cubicBezier(0, 1.5, 1, 1.5) + }, + { + desc: 'easing function which produces values less than 1', + easing: 'cubic-bezier(0, -0.5, 1, -0.5)', + easingFunction: cubicBezier(0, -0.5, 1, -0.5) + } +]; + +const gEasingParsingTests = [ + ['linear', 'linear'], + ['ease-in-out', 'ease-in-out'], + ['Ease\\2d in-out', 'ease-in-out'], + ['ease /**/', 'ease'], +]; + +const gInvalidEasings = [ + '', + '7', + 'test', + 'initial', + 'inherit', + 'unset', + 'unrecognized', + 'var(--x)', + 'ease-in-out, ease-out', + 'cubic-bezier(1.1, 0, 1, 1)', + 'cubic-bezier(0, 0, 1.1, 1)', + 'cubic-bezier(-0.1, 0, 1, 1)', + 'cubic-bezier(0, 0, -0.1, 1)', + 'cubic-bezier(0.1, 0, 4, 0.4)', + 'steps(-1, start)', + 'steps(0.1, start)', + 'steps(3, nowhere)', + 'steps(-3, end)', + 'function (a){return a}', + 'function (x){return x}', + 'function(x, y){return 0.3}', +]; + +// Easings that should serialize to the same string +const gRoundtripEasings = [ + 'ease', + 'linear', + 'ease-in', + 'ease-out', + 'ease-in-out', + 'cubic-bezier(0.1, 5, 0.23, 0)', + 'steps(3, start)', + 'steps(3)', +]; diff --git a/testing/web-platform/tests/web-animations/resources/effect-tests.js b/testing/web-platform/tests/web-animations/resources/effect-tests.js new file mode 100644 index 0000000000..8a18ab13c6 --- /dev/null +++ b/testing/web-platform/tests/web-animations/resources/effect-tests.js @@ -0,0 +1,75 @@ +'use strict'; + +// Common utility methods for testing animation effects + +// Tests the |property| member of |animation's| target effect's computed timing +// at the various points indicated by |values|. +// +// |values| has the format: +// +// { +// before, // value to test during before phase +// activeBoundary, // value to test at the very beginning of the active +// // phase when playing forwards, or the very end of +// // the active phase when playing backwards. +// // This should be undefined if the active duration of +// // the effect is zero. +// after, // value to test during the after phase or undefined if the +// // active duration is infinite +// } +// +function assert_computed_timing_for_each_phase(animation, property, values) { + // Some computed timing properties (e.g. 'progress') require floating-point + // comparison, whilst exact equality suffices for others. + const assert_property_equals = + (property === 'progress') ? assert_times_equal : assert_equals; + + const effect = animation.effect; + const timing = effect.getComputedTiming(); + + // The following calculations are based on the definitions here: + // https://drafts.csswg.org/web-animations/#animation-effect-phases-and-states + const beforeActive = Math.max(Math.min(timing.delay, timing.endTime), 0); + const activeAfter = + Math.max(Math.min(timing.delay + timing.activeDuration, timing.endTime), 0); + const direction = animation.playbackRate < 0 ? 'backwards' : 'forwards'; + + // Before phase + if (direction === 'forwards') { + animation.currentTime = beforeActive - 1; + } else { + animation.currentTime = beforeActive; + } + assert_property_equals(effect.getComputedTiming()[property], values.before, + `Value of ${property} in the before phase`); + + // Active phase + if (effect.getComputedTiming().activeDuration > 0) { + if (direction === 'forwards') { + animation.currentTime = beforeActive; + } else { + animation.currentTime = activeAfter; + } + assert_property_equals(effect.getComputedTiming()[property], values.activeBoundary, + `Value of ${property} at the boundary of the active phase`); + } else { + assert_equals(values.activeBoundary, undefined, + 'Test specifies a value to check during the active phase but' + + ' the animation has a zero duration'); + } + + // After phase + if (effect.getComputedTiming().activeDuration !== Infinity) { + if (direction === 'forwards') { + animation.currentTime = activeAfter; + } else { + animation.currentTime = activeAfter + 1; + } + assert_property_equals(effect.getComputedTiming()[property], values.after, + `Value of ${property} in the after phase`); + } else { + assert_equals(values.after, undefined, + 'Test specifies a value to check during the after phase but' + + ' the animation has an infinite duration'); + } +} diff --git a/testing/web-platform/tests/web-animations/resources/keyframe-tests.js b/testing/web-platform/tests/web-animations/resources/keyframe-tests.js new file mode 100644 index 0000000000..43e0d7575f --- /dev/null +++ b/testing/web-platform/tests/web-animations/resources/keyframe-tests.js @@ -0,0 +1,827 @@ +'use strict'; + +// ============================== +// +// Common keyframe test data +// +// ============================== + + +// ------------------------------ +// Composite values +// ------------------------------ + +const gGoodKeyframeCompositeValueTests = [ + 'replace', 'add', 'accumulate', 'auto' +]; + +const gBadKeyframeCompositeValueTests = [ + 'unrecognised', 'replace ', 'Replace', null +]; + +const gGoodOptionsCompositeValueTests = [ + 'replace', 'add', 'accumulate' +]; + +const gBadOptionsCompositeValueTests = [ + 'unrecognised', 'replace ', 'Replace', null +]; + +// ------------------------------ +// Keyframes +// ------------------------------ + +const gEmptyKeyframeListTests = [ + [], + null, + undefined, +]; + +// Helper methods to make defining computed keyframes more readable. + +const offset = offset => ({ + offset, + computedOffset: offset, +}); + +const computedOffset = computedOffset => ({ + offset: null, + computedOffset, +}); + +const keyframe = (offset, props, easing='linear', composite) => { + // The object spread operator is not yet available in all browsers so we use + // Object.assign instead. + const result = {}; + Object.assign(result, offset, props, { easing }); + result.composite = composite || 'auto'; + return result; +}; + +const gKeyframesTests = [ + + // ----------- Property-indexed keyframes: property handling ----------- + + { + desc: 'a one property two value property-indexed keyframes specification', + input: { left: ['10px', '20px'] }, + output: [keyframe(computedOffset(0), { left: '10px' }), + keyframe(computedOffset(1), { left: '20px' })], + }, + { + desc: 'a one shorthand property two value property-indexed keyframes' + + ' specification', + input: { margin: ['10px', '10px 20px 30px 40px'] }, + output: [keyframe(computedOffset(0), { margin: '10px' }), + keyframe(computedOffset(1), { margin: '10px 20px 30px 40px' })], + }, + { + desc: 'a two property (one shorthand and one of its longhand components)' + + ' two value property-indexed keyframes specification', + input: { marginTop: ['50px', '60px'], + margin: ['10px', '10px 20px 30px 40px'] }, + output: [keyframe(computedOffset(0), + { marginTop: '50px', margin: '10px' }), + keyframe(computedOffset(1), + { marginTop: '60px', margin: '10px 20px 30px 40px' })], + }, + { + desc: 'a two property (one shorthand and one of its shorthand components)' + + ' two value property-indexed keyframes specification', + input: { border: ['pink', '2px'], + borderColor: ['green', 'blue'] }, + output: [keyframe(computedOffset(0), + { border: 'pink', borderColor: 'green' }), + keyframe(computedOffset(1), + { border: '2px', borderColor: 'blue' })], + }, + { + desc: 'a two property two value property-indexed keyframes specification', + input: { left: ['10px', '20px'], + top: ['30px', '40px'] }, + output: [keyframe(computedOffset(0), { left: '10px', top: '30px' }), + keyframe(computedOffset(1), { left: '20px', top: '40px' })], + }, + { + desc: 'a two property property-indexed keyframes specification with' + + ' different numbers of values', + input: { left: ['10px', '20px', '30px'], + top: ['40px', '50px'] }, + output: [keyframe(computedOffset(0), { left: '10px', top: '40px' }), + keyframe(computedOffset(0.5), { left: '20px' }), + keyframe(computedOffset(1), { left: '30px', top: '50px' })], + }, + { + desc: 'a property-indexed keyframes specification with an invalid value', + input: { left: ['10px', '20px', '30px', '40px', '50px'], + top: ['15px', '25px', 'invalid', '45px', '55px'] }, + output: [keyframe(computedOffset(0), { left: '10px', top: '15px' }), + keyframe(computedOffset(0.25), { left: '20px', top: '25px' }), + keyframe(computedOffset(0.5), { left: '30px' }), + keyframe(computedOffset(0.75), { left: '40px', top: '45px' }), + keyframe(computedOffset(1), { left: '50px', top: '55px' })], + }, + { + desc: 'a one property two value property-indexed keyframes specification' + + ' that needs to stringify its values', + input: { opacity: [0, 1] }, + output: [keyframe(computedOffset(0), { opacity: '0' }), + keyframe(computedOffset(1), { opacity: '1' })], + }, + { + desc: 'a property-indexed keyframes specification with a CSS variable' + + ' reference', + input: { left: [ 'var(--dist)', 'calc(var(--dist) + 100px)' ] }, + output: [keyframe(computedOffset(0), { left: 'var(--dist)' }), + keyframe(computedOffset(1), { left: 'calc(var(--dist) + 100px)' })] + }, + { + desc: 'a property-indexed keyframes specification with a CSS variable' + + ' reference in a shorthand property', + input: { margin: [ 'var(--dist)', 'calc(var(--dist) + 100px)' ] }, + output: [keyframe(computedOffset(0), + { margin: 'var(--dist)' }), + keyframe(computedOffset(1), + { margin: 'calc(var(--dist) + 100px)' })], + }, + { + desc: 'a one property one value property-indexed keyframes specification', + input: { left: ['10px'] }, + output: [keyframe(computedOffset(1), { left: '10px' })], + }, + { + desc: 'a one property one non-array value property-indexed keyframes' + + ' specification', + input: { left: '10px' }, + output: [keyframe(computedOffset(1), { left: '10px' })], + }, + { + desc: 'a one property two value property-indexed keyframes specification' + + ' where the first value is invalid', + input: { left: ['invalid', '10px'] }, + output: [keyframe(computedOffset(0), {}), + keyframe(computedOffset(1), { left: '10px' })] + }, + { + desc: 'a one property two value property-indexed keyframes specification' + + ' where the second value is invalid', + input: { left: ['10px', 'invalid'] }, + output: [keyframe(computedOffset(0), { left: '10px' }), + keyframe(computedOffset(1), {})] + }, + { + desc: 'a property-indexed keyframes specification with a CSS variable as' + + ' the property', + input: { '--custom': ['1', '2'] }, + output: [keyframe(computedOffset(0), { '--custom': '1' }), + keyframe(computedOffset(1), { '--custom': '2' })] + }, + + // ----------- Property-indexed keyframes: offset handling ----------- + + { + desc: 'a property-indexed keyframe with a single offset', + input: { left: ['10px', '20px', '30px'], offset: 0.5 }, + output: [keyframe(offset(0.5), { left: '10px' }), + keyframe(computedOffset(0.75), { left: '20px' }), + keyframe(computedOffset(1), { left: '30px' })], + }, + { + desc: 'a property-indexed keyframe with an array of offsets', + input: { left: ['10px', '20px', '30px'], offset: [ 0.1, 0.25, 0.8 ] }, + output: [keyframe(offset(0.1), { left: '10px' }), + keyframe(offset(0.25), { left: '20px' }), + keyframe(offset(0.8), { left: '30px' })], + }, + { + desc: 'a property-indexed keyframe with an array of offsets that is too' + + ' short', + input: { left: ['10px', '20px', '30px'], offset: [ 0, 0.25 ] }, + output: [keyframe(offset(0), { left: '10px' }), + keyframe(offset(0.25), { left: '20px' }), + keyframe(computedOffset(1), { left: '30px' })], + }, + { + desc: 'a property-indexed keyframe with an array of offsets that is too' + + ' long', + input: { left: ['10px', '20px', '30px'], + offset: [ 0, 0.25, 0.5, 0.75, 1 ] }, + output: [keyframe(offset(0), { left: '10px' }), + keyframe(offset(0.25), { left: '20px' }), + keyframe(offset(0.5), { left: '30px' })], + }, + { + desc: 'a property-indexed keyframe with an empty array of offsets', + input: { left: ['10px', '20px', '30px'], offset: [] }, + output: [keyframe(computedOffset(0), { left: '10px' }), + keyframe(computedOffset(0.5), { left: '20px' }), + keyframe(computedOffset(1), { left: '30px' })], + }, + { + desc: 'a property-indexed keyframe with an array of offsets with an' + + ' embedded null value', + input: { left: ['10px', '20px', '30px'], + offset: [ 0, null, 0.5 ] }, + output: [keyframe(offset(0), { left: '10px' }), + keyframe(computedOffset(0.25), { left: '20px' }), + keyframe(offset(0.5), { left: '30px' })], + }, + { + desc: 'a property-indexed keyframe with an array of offsets with a' + + ' trailing null value', + input: { left: ['10px', '20px', '30px'], + offset: [ 0, 0.25, null ] }, + output: [keyframe(offset(0), { left: '10px' }), + keyframe(offset(0.25), { left: '20px' }), + keyframe(computedOffset(1), { left: '30px' })], + }, + { + desc: 'a property-indexed keyframe with an array of offsets with leading' + + ' and trailing null values', + input: { left: ['10px', '20px', '30px'], + offset: [ null, 0.25, null ] }, + output: [keyframe(computedOffset(0), { left: '10px' }), + keyframe(offset(0.25), { left: '20px' }), + keyframe(computedOffset(1), { left: '30px' })], + }, + { + desc: 'a property-indexed keyframe with an array of offsets with' + + ' adjacent null values', + input: { left: ['10px', '20px', '30px'], + offset: [ null, null, 0.5 ] }, + output: [keyframe(computedOffset(0), { left: '10px' }), + keyframe(computedOffset(0.25), { left: '20px' }), + keyframe(offset(0.5), { left: '30px' })], + }, + { + desc: 'a property-indexed keyframe with an array of offsets with' + + ' all null values (and too many at that)', + input: { left: ['10px', '20px', '30px'], + offset: [ null, null, null, null, null ] }, + output: [keyframe(computedOffset(0), { left: '10px' }), + keyframe(computedOffset(0.5), { left: '20px' }), + keyframe(computedOffset(1), { left: '30px' })], + }, + { + desc: 'a property-indexed keyframe with a single null offset', + input: { left: ['10px', '20px', '30px'], offset: null }, + output: [keyframe(computedOffset(0), { left: '10px' }), + keyframe(computedOffset(0.5), { left: '20px' }), + keyframe(computedOffset(1), { left: '30px' })], + }, + { + desc: 'a property-indexed keyframe with an array of offsets that is not' + + ' strictly ascending in the unused part of the array', + input: { left: ['10px', '20px', '30px'], + offset: [ 0, 0.2, 0.8, 0.6 ] }, + output: [keyframe(offset(0), { left: '10px' }), + keyframe(offset(0.2), { left: '20px' }), + keyframe(offset(0.8), { left: '30px' })], + }, + + // ----------- Property-indexed keyframes: easing handling ----------- + + { + desc: 'a property-indexed keyframe without any specified easing', + input: { left: ['10px', '20px', '30px'] }, + output: [keyframe(computedOffset(0), { left: '10px' }, 'linear'), + keyframe(computedOffset(0.5), { left: '20px' }, 'linear'), + keyframe(computedOffset(1), { left: '30px' }, 'linear')], + }, + { + desc: 'a property-indexed keyframe with a single easing', + input: { left: ['10px', '20px', '30px'], easing: 'ease-in' }, + output: [keyframe(computedOffset(0), { left: '10px' }, 'ease-in'), + keyframe(computedOffset(0.5), { left: '20px' }, 'ease-in'), + keyframe(computedOffset(1), { left: '30px' }, 'ease-in')], + }, + { + desc: 'a property-indexed keyframe with an array of easings', + input: { left: ['10px', '20px', '30px'], + easing: ['ease-in', 'ease-out', 'ease-in-out'] }, + output: [keyframe(computedOffset(0), { left: '10px' }, 'ease-in'), + keyframe(computedOffset(0.5), { left: '20px' }, 'ease-out'), + keyframe(computedOffset(1), { left: '30px' }, 'ease-in-out')], + }, + { + desc: 'a property-indexed keyframe with an array of easings that is too' + + ' short', + input: { left: ['10px', '20px', '30px'], + easing: ['ease-in', 'ease-out'] }, + output: [keyframe(computedOffset(0), { left: '10px' }, 'ease-in'), + keyframe(computedOffset(0.5), { left: '20px' }, 'ease-out'), + keyframe(computedOffset(1), { left: '30px' }, 'ease-in')], + }, + { + desc: 'a property-indexed keyframe with a single-element array of' + + ' easings', + input: { left: ['10px', '20px', '30px'], easing: ['ease-in'] }, + output: [keyframe(computedOffset(0), { left: '10px' }, 'ease-in'), + keyframe(computedOffset(0.5), { left: '20px' }, 'ease-in'), + keyframe(computedOffset(1), { left: '30px' }, 'ease-in')], + }, + { + desc: 'a property-indexed keyframe with an empty array of easings', + input: { left: ['10px', '20px', '30px'], easing: [] }, + output: [keyframe(computedOffset(0), { left: '10px' }, 'linear'), + keyframe(computedOffset(0.5), { left: '20px' }, 'linear'), + keyframe(computedOffset(1), { left: '30px' }, 'linear')], + }, + { + desc: 'a property-indexed keyframe with an array of easings that is too' + + ' long', + input: { left: ['10px', '20px', '30px'], + easing: ['steps(1)', 'steps(2)', 'steps(3)', 'steps(4)'] }, + output: [keyframe(computedOffset(0), { left: '10px' }, 'steps(1)'), + keyframe(computedOffset(0.5), { left: '20px' }, 'steps(2)'), + keyframe(computedOffset(1), { left: '30px' }, 'steps(3)')], + }, + + // ----------- Property-indexed keyframes: composite handling ----------- + + { + desc: 'a property-indexed keyframe with a single composite operation', + input: { left: ['10px', '20px', '30px'], composite: 'add' }, + output: [keyframe(computedOffset(0), { left: '10px' }, 'linear', 'add'), + keyframe(computedOffset(0.5), { left: '20px' }, 'linear', 'add'), + keyframe(computedOffset(1), { left: '30px' }, 'linear', 'add')], + }, + { + desc: 'a property-indexed keyframe with a composite array', + input: { left: ['10px', '20px', '30px'], + composite: ['add', 'replace', 'accumulate'] }, + output: [keyframe(computedOffset(0), { left: '10px' }, + 'linear', 'add'), + keyframe(computedOffset(0.5), { left: '20px' }, + 'linear', 'replace'), + keyframe(computedOffset(1), { left: '30px' }, + 'linear', 'accumulate')], + }, + { + desc: 'a property-indexed keyframe with a composite array that is too' + + ' short', + input: { left: ['10px', '20px', '30px', '40px', '50px'], + composite: ['add', 'replace'] }, + output: [keyframe(computedOffset(0), { left: '10px' }, + 'linear', 'add'), + keyframe(computedOffset(0.25), { left: '20px' }, + 'linear', 'replace'), + keyframe(computedOffset(0.5), { left: '30px' }, + 'linear', 'add'), + keyframe(computedOffset(0.75), { left: '40px' }, + 'linear', 'replace'), + keyframe(computedOffset(1), { left: '50px' }, + 'linear', 'add')], + }, + { + desc: 'a property-indexed keyframe with a composite array that is too' + + ' long', + input: { left: ['10px', '20px'], + composite: ['add', 'replace', 'accumulate'] }, + output: [keyframe(computedOffset(0), { left: '10px' }, + 'linear', 'add'), + keyframe(computedOffset(1), { left: '20px' }, + 'linear', 'replace')], + }, + { + desc: 'a property-indexed keyframe with a single-element composite array', + input: { left: ['10px', '20px', '30px'], + composite: ['add'] }, + output: [keyframe(computedOffset(0), { left: '10px' }, 'linear', 'add'), + keyframe(computedOffset(0.5), { left: '20px' }, 'linear', 'add'), + keyframe(computedOffset(1), { left: '30px' }, 'linear', 'add')], + }, + + // ----------- Keyframe sequence: property handling ----------- + + { + desc: 'a one property one keyframe sequence', + input: [{ offset: 1, left: '10px' }], + output: [keyframe(offset(1), { left: '10px' })], + }, + { + desc: 'a one property two keyframe sequence', + input: [{ offset: 0, left: '10px' }, + { offset: 1, left: '20px' }], + output: [keyframe(offset(0), { left: '10px' }), + keyframe(offset(1), { left: '20px' })], + }, + { + desc: 'a two property two keyframe sequence', + input: [{ offset: 0, left: '10px', top: '30px' }, + { offset: 1, left: '20px', top: '40px' }], + output: [keyframe(offset(0), { left: '10px', top: '30px' }), + keyframe(offset(1), { left: '20px', top: '40px' })], + }, + { + desc: 'a one shorthand property two keyframe sequence', + input: [{ offset: 0, margin: '10px' }, + { offset: 1, margin: '20px 30px 40px 50px' }], + output: [keyframe(offset(0), { margin: '10px' }), + keyframe(offset(1), { margin: '20px 30px 40px 50px' })], + }, + { + desc: 'a two property (a shorthand and one of its component longhands)' + + ' two keyframe sequence', + input: [{ offset: 0, margin: '10px', marginTop: '20px' }, + { offset: 1, marginTop: '70px', margin: '30px 40px 50px 60px' }], + output: [keyframe(offset(0), { margin: '10px', marginTop: '20px' }), + keyframe(offset(1), { marginTop: '70px', + margin: '30px 40px 50px 60px' })], + }, + { + desc: 'a two property keyframe sequence where one property is missing' + + ' from the first keyframe', + input: [{ offset: 0, left: '10px' }, + { offset: 1, left: '20px', top: '30px' }], + output: [keyframe(offset(0), { left: '10px' }), + keyframe(offset(1), { left: '20px', top: '30px' })], + }, + { + desc: 'a two property keyframe sequence where one property is missing' + + ' from the last keyframe', + input: [{ offset: 0, left: '10px', top: '20px' }, + { offset: 1, left: '30px' }], + output: [keyframe(offset(0), { left: '10px', top: '20px' }), + keyframe(offset(1), { left: '30px' })], + }, + { + desc: 'a one property two keyframe sequence that needs to stringify' + + ' its values', + input: [{ offset: 0, opacity: 0 }, + { offset: 1, opacity: 1 }], + output: [keyframe(offset(0), { opacity: '0' }), + keyframe(offset(1), { opacity: '1' })], + }, + { + desc: 'a keyframe sequence with a CSS variable reference', + input: [{ left: 'var(--dist)' }, + { left: 'calc(var(--dist) + 100px)' }], + output: [keyframe(computedOffset(0), { left: 'var(--dist)' }), + keyframe(computedOffset(1), { left: 'calc(var(--dist) + 100px)' })] + }, + { + desc: 'a keyframe sequence with a CSS variable reference in a shorthand' + + ' property', + input: [{ margin: 'var(--dist)' }, + { margin: 'calc(var(--dist) + 100px)' }], + output: [keyframe(computedOffset(0), + { margin: 'var(--dist)' }), + keyframe(computedOffset(1), + { margin: 'calc(var(--dist) + 100px)' })], + }, + { + desc: 'a keyframe sequence with a CSS variable as its property', + input: [{ '--custom': 'a' }, + { '--custom': 'b' }], + output: [keyframe(computedOffset(0), { '--custom': 'a' }), + keyframe(computedOffset(1), { '--custom': 'b' })] + }, + + // ----------- Keyframe sequence: offset handling ----------- + + { + desc: 'a keyframe sequence with duplicate values for a given interior' + + ' offset', + input: [{ offset: 0.0, left: '10px' }, + { offset: 0.5, left: '20px' }, + { offset: 0.5, left: '30px' }, + { offset: 0.5, left: '40px' }, + { offset: 1.0, left: '50px' }], + output: [keyframe(offset(0), { left: '10px' }), + keyframe(offset(0.5), { left: '20px' }), + keyframe(offset(0.5), { left: '30px' }), + keyframe(offset(0.5), { left: '40px' }), + keyframe(offset(1), { left: '50px' })], + }, + { + desc: 'a keyframe sequence with duplicate values for offsets 0 and 1', + input: [{ offset: 0, left: '10px' }, + { offset: 0, left: '20px' }, + { offset: 0, left: '30px' }, + { offset: 1, left: '40px' }, + { offset: 1, left: '50px' }, + { offset: 1, left: '60px' }], + output: [keyframe(offset(0), { left: '10px' }), + keyframe(offset(0), { left: '20px' }), + keyframe(offset(0), { left: '30px' }), + keyframe(offset(1), { left: '40px' }), + keyframe(offset(1), { left: '50px' }), + keyframe(offset(1), { left: '60px' })], + }, + { + desc: 'a two property four keyframe sequence', + input: [{ offset: 0, left: '10px' }, + { offset: 0, top: '20px' }, + { offset: 1, top: '30px' }, + { offset: 1, left: '40px' }], + output: [keyframe(offset(0), { left: '10px' }), + keyframe(offset(0), { top: '20px' }), + keyframe(offset(1), { top: '30px' }), + keyframe(offset(1), { left: '40px' })], + }, + { + desc: 'a single keyframe sequence with omitted offset', + input: [{ left: '10px' }], + output: [keyframe(computedOffset(1), { left: '10px' })], + }, + { + desc: 'a single keyframe sequence with null offset', + input: [{ offset: null, left: '10px' }], + output: [keyframe(computedOffset(1), { left: '10px' })], + }, + { + desc: 'a single keyframe sequence with string offset', + input: [{ offset: '0.5', left: '10px' }], + output: [keyframe(offset(0.5), { left: '10px' })], + }, + { + desc: 'a one property keyframe sequence with some omitted offsets', + input: [{ offset: 0.00, left: '10px' }, + { offset: 0.25, left: '20px' }, + { left: '30px' }, + { left: '40px' }, + { offset: 1.00, left: '50px' }], + output: [keyframe(offset(0), { left: '10px' }), + keyframe(offset(0.25), { left: '20px' }), + keyframe(computedOffset(0.5), { left: '30px' }), + keyframe(computedOffset(0.75), { left: '40px' }), + keyframe(offset(1), { left: '50px' })], + }, + { + desc: 'a one property keyframe sequence with some null offsets', + input: [{ offset: 0.00, left: '10px' }, + { offset: 0.25, left: '20px' }, + { offset: null, left: '30px' }, + { offset: null, left: '40px' }, + { offset: 1.00, left: '50px' }], + output: [keyframe(offset(0), { left: '10px' }), + keyframe(offset(0.25), { left: '20px' }), + keyframe(computedOffset(0.5), { left: '30px' }), + keyframe(computedOffset(0.75), { left: '40px' }), + keyframe(offset(1), { left: '50px' })], + }, + { + desc: 'a two property keyframe sequence with some omitted offsets', + input: [{ offset: 0.00, left: '10px', top: '20px' }, + { offset: 0.25, left: '30px' }, + { left: '40px' }, + { left: '50px', top: '60px' }, + { offset: 1.00, left: '70px', top: '80px' }], + output: [keyframe(offset(0), { left: '10px', top: '20px' }), + keyframe(offset(0.25), { left: '30px' }), + keyframe(computedOffset(0.5), { left: '40px' }), + keyframe(computedOffset(0.75), { left: '50px', top: '60px' }), + keyframe(offset(1), { left: '70px', top: '80px' })], + }, + { + desc: 'a one property keyframe sequence with all omitted offsets', + input: [{ left: '10px' }, + { left: '20px' }, + { left: '30px' }, + { left: '40px' }, + { left: '50px' }], + output: [keyframe(computedOffset(0), { left: '10px' }), + keyframe(computedOffset(0.25), { left: '20px' }), + keyframe(computedOffset(0.5), { left: '30px' }), + keyframe(computedOffset(0.75), { left: '40px' }), + keyframe(computedOffset(1), { left: '50px' })], + }, + + // ----------- Keyframe sequence: easing handling ----------- + + { + desc: 'a keyframe sequence with different easing values, but the same' + + ' easing value for a given offset', + input: [{ offset: 0.0, easing: 'ease', left: '10px'}, + { offset: 0.0, easing: 'ease', top: '20px'}, + { offset: 0.5, easing: 'linear', left: '30px' }, + { offset: 0.5, easing: 'linear', top: '40px' }, + { offset: 1.0, easing: 'step-end', left: '50px' }, + { offset: 1.0, easing: 'step-end', top: '60px' }], + output: [keyframe(offset(0), { left: '10px' }, 'ease'), + keyframe(offset(0), { top: '20px' }, 'ease'), + keyframe(offset(0.5), { left: '30px' }, 'linear'), + keyframe(offset(0.5), { top: '40px' }, 'linear'), + keyframe(offset(1), { left: '50px' }, 'steps(1)'), + keyframe(offset(1), { top: '60px' }, 'steps(1)')], + }, + + // ----------- Keyframe sequence: composite handling ----------- + + { + desc: 'a keyframe sequence with different composite values, but the' + + ' same composite value for a given offset', + input: [{ offset: 0.0, composite: 'replace', left: '10px' }, + { offset: 0.0, composite: 'replace', top: '20px' }, + { offset: 0.5, composite: 'add', left: '30px' }, + { offset: 0.5, composite: 'add', top: '40px' }, + { offset: 1.0, composite: 'replace', left: '50px' }, + { offset: 1.0, composite: 'replace', top: '60px' }], + output: [keyframe(offset(0), { left: '10px' }, 'linear', 'replace'), + keyframe(offset(0), { top: '20px' }, 'linear', 'replace'), + keyframe(offset(0.5), { left: '30px' }, 'linear', 'add'), + keyframe(offset(0.5), { top: '40px' }, 'linear', 'add'), + keyframe(offset(1), { left: '50px' }, 'linear', 'replace'), + keyframe(offset(1), { top: '60px' }, 'linear', 'replace')], + }, +]; + +const gInvalidKeyframesTests = [ + { + desc: 'keyframes with an out-of-bounded positive offset', + input: [ { opacity: 0 }, + { opacity: 0.5, offset: 2 }, + { opacity: 1 } ], + }, + { + desc: 'keyframes with an out-of-bounded negative offset', + input: [ { opacity: 0 }, + { opacity: 0.5, offset: -1 }, + { opacity: 1 } ], + }, + { + desc: 'property-indexed keyframes not loosely sorted by offset', + input: { opacity: [ 0, 1 ], offset: [ 1, 0 ] }, + }, + { + desc: 'property-indexed keyframes not loosely sorted by offset even' + + ' though not all offsets are specified', + input: { opacity: [ 0, 0.5, 1 ], offset: [ 0.5, 0 ] }, + }, + { + desc: 'property-indexed keyframes with offsets out of range', + input: { opacity: [ 0, 0.5, 1 ], offset: [ 0, 1.1 ] }, + }, + { + desc: 'keyframes not loosely sorted by offset', + input: [ { opacity: 0, offset: 1 }, + { opacity: 1, offset: 0 } ], + }, + { + desc: 'property-indexed keyframes with an invalid easing value', + input: { opacity: [ 0, 0.5, 1 ], + easing: 'inherit' }, + }, + { + desc: 'property-indexed keyframes with an invalid easing value as one of' + + ' the array values', + input: { opacity: [ 0, 0.5, 1 ], + easing: [ 'ease-in', 'inherit' ] }, + }, + { + desc: 'property-indexed keyframe with an invalid easing in the unused' + + ' part of the array of easings', + input: { left: ['10px', '20px', '30px'], + easing: ['steps(1)', 'steps(2)', 'steps(3)', 'invalid'] }, + }, + { + desc: 'empty property-indexed keyframe with an invalid easing', + input: { easing: 'invalid' }, + }, + { + desc: 'empty property-indexed keyframe with an invalid easings array', + input: { easing: ['invalid'] }, + }, + { + desc: 'a keyframe sequence with an invalid easing value', + input: [ { opacity: 0, easing: 'jumpy' }, + { opacity: 1 } ], + }, + { + desc: 'property-indexed keyframes with an invalid composite value', + input: { opacity: [ 0, 0.5, 1 ], + composite: 'alternate' }, + }, + { + desc: 'property-indexed keyframes with an invalid composite value as one' + + ' of the array values', + input: { opacity: [ 0, 0.5, 1 ], + composite: [ 'add', 'alternate' ] }, + }, + { + desc: 'keyframes with an invalid composite value', + input: [ { opacity: 0, composite: 'alternate' }, + { opacity: 1 } ], + }, +]; + + +const gKeyframeSerializationTests = [ + { + desc: 'a on keyframe sequence which requires value serilaization of its' + + ' values', + input: [{offset: 0, backgroundColor: 'rgb(1,2,3)' }], + output: [keyframe(offset(0), { backgroundColor: 'rgb(1, 2, 3)' })], + }, +]; + + + +// ------------------------------ +// KeyframeEffectOptions +// ------------------------------ + +const gKeyframeEffectOptionTests = [ + { + desc: 'an empty KeyframeEffectOptions object', + input: { }, + expected: { }, + }, + { + desc: 'a normal KeyframeEffectOptions object', + input: { delay: 1000, + fill: 'auto', + iterations: 5.5, + duration: 'auto', + direction: 'alternate' }, + expected: { delay: 1000, + fill: 'auto', + iterations: 5.5, + duration: 'auto', + direction: 'alternate' }, + }, + { + desc: 'a double value', + input: 3000, + expected: { duration: 3000 }, + }, + { + desc: '+Infinity', + input: Infinity, + expected: { duration: Infinity }, + }, + { + desc: 'an Infinity duration', + input: { duration: Infinity }, + expected: { duration: Infinity }, + }, + { + desc: 'an auto duration', + input: { duration: 'auto' }, + expected: { duration: 'auto' }, + }, + { + desc: 'an Infinity iterations', + input: { iterations: Infinity }, + expected: { iterations: Infinity }, + }, + { + desc: 'an auto fill', + input: { fill: 'auto' }, + expected: { fill: 'auto' }, + }, + { + desc: 'a forwards fill', + input: { fill: 'forwards' }, + expected: { fill: 'forwards' }, + } +]; + +const gInvalidKeyframeEffectOptionTests = [ + { desc: '-Infinity', input: -Infinity }, + { desc: 'NaN', input: NaN }, + { desc: 'a negative value', input: -1 }, + { desc: 'a negative Infinity duration', input: { duration: -Infinity } }, + { desc: 'a NaN duration', input: { duration: NaN } }, + { desc: 'a negative duration', input: { duration: -1 } }, + { desc: 'a string duration', input: { duration: 'merrychristmas' } }, + { desc: 'a negative Infinity iterations', input: { iterations: -Infinity} }, + { desc: 'a NaN iterations', input: { iterations: NaN } }, + { desc: 'a negative iterations', input: { iterations: -1 } }, + { desc: 'a blank easing', input: { easing: '' } }, + { desc: 'an unrecognized easing', input: { easing: 'unrecognised' } }, + { desc: 'an \'initial\' easing', input: { easing: 'initial' } }, + { desc: 'an \'inherit\' easing', input: { easing: 'inherit' } }, + { desc: 'a variable easing', input: { easing: 'var(--x)' } }, + { desc: 'a multi-value easing', input: { easing: 'ease-in-out, ease-out' } }, +]; + +// There is currently only ScrollTimeline that can be constructed and used here +// beyond document timeline. Given that ScrollTimeline is not stable as of yet +// it's tested in scroll-animations/animation-with-animatable-interface.html. +const gAnimationTimelineTests = [ + { + expectedTimeline: document.timeline, + expectedTimelineDescription: 'document.timeline', + description: 'with no timeline parameter' + }, + { + timeline: undefined, + expectedTimeline: document.timeline, + expectedTimelineDescription: 'document.timeline', + description: 'with undefined timeline' + }, + { + timeline: null, + expectedTimeline: null, + expectedTimelineDescription: 'null', + description: 'with null timeline' + }, + { + timeline: document.timeline, + expectedTimeline: document.timeline, + expectedTimelineDescription: 'document.timeline', + description: 'with DocumentTimeline' + }, +];
\ No newline at end of file diff --git a/testing/web-platform/tests/web-animations/resources/keyframe-utils.js b/testing/web-platform/tests/web-animations/resources/keyframe-utils.js new file mode 100644 index 0000000000..fdea1d8d93 --- /dev/null +++ b/testing/web-platform/tests/web-animations/resources/keyframe-utils.js @@ -0,0 +1,35 @@ +'use strict'; + +// ======================================= +// +// Utility functions for testing keyframes +// +// ======================================= + + +// ------------------------------ +// Helper functions +// ------------------------------ + +/** + * Test equality between two lists of computed keyframes + * @param {Array.<ComputedKeyframe>} a - actual computed keyframes + * @param {Array.<ComputedKeyframe>} b - expected computed keyframes + */ +function assert_frame_lists_equal(a, b) { + assert_equals(a.length, b.length, 'number of frames'); + for (let i = 0; i < Math.min(a.length, b.length); i++) { + assert_frames_equal(a[i], b[i], `ComputedKeyframe #${i}`); + } +} + +/** Helper for assert_frame_lists_equal */ +function assert_frames_equal(a, b, name) { + assert_equals(Object.keys(a).sort().toString(), + Object.keys(b).sort().toString(), + `properties on ${name} should match`); + // Iterates sorted keys to ensure stable failures. + for (const p of Object.keys(a).sort()) { + assert_equals(a[p], b[p], `value for '${p}' on ${name}`); + } +} diff --git a/testing/web-platform/tests/web-animations/resources/timing-override.js b/testing/web-platform/tests/web-animations/resources/timing-override.js new file mode 100644 index 0000000000..a1d65030f0 --- /dev/null +++ b/testing/web-platform/tests/web-animations/resources/timing-override.js @@ -0,0 +1,18 @@ +// Firefox implements unconditional clamping of 20 usec; and for certain web-animation tests, +// we hit some test failures because the Time Precision is too small. We override these functions +// on a per-test basis for Firefox only. +if(navigator.userAgent.toLowerCase().indexOf('firefox') > -1){ + window.assert_times_equal = (actual, expected, description) => { + let TIME_PRECISION = 0.02; + assert_approx_equals(actual, expected, TIME_PRECISION * 2, description); + }; + + window.assert_time_equals_literal = (actual, expected, description) => { + let TIME_PRECISION = 0.02; + if (Math.abs(expected) === Infinity) { + assert_equals(actual, expected, description); + } else { + assert_approx_equals(actual, expected, TIME_PRECISION, description); + } + } +} diff --git a/testing/web-platform/tests/web-animations/resources/timing-tests.js b/testing/web-platform/tests/web-animations/resources/timing-tests.js new file mode 100644 index 0000000000..4b0f021f74 --- /dev/null +++ b/testing/web-platform/tests/web-animations/resources/timing-tests.js @@ -0,0 +1,46 @@ +'use strict'; + +// ================================= +// +// Common timing parameter test data +// +// ================================= + + +// ------------------------------ +// Delay values +// ------------------------------ + +const gBadDelayValues = [ + NaN, Infinity, -Infinity +]; + +// ------------------------------ +// Duration values +// ------------------------------ + +const gGoodDurationValues = [ + { specified: 123.45, computed: 123.45 }, + { specified: 'auto', computed: 0 }, + { specified: Infinity, computed: Infinity }, +]; + +const gBadDurationValues = [ + -1, NaN, -Infinity, 'abc', '100' +]; + +// ------------------------------ +// iterationStart values +// ------------------------------ + +const gBadIterationStartValues = [ + -1, NaN, Infinity, -Infinity +]; + +// ------------------------------ +// iterations values +// ------------------------------ + +const gBadIterationsValues = [ + -1, -Infinity, NaN +]; diff --git a/testing/web-platform/tests/web-animations/resources/timing-utils.js b/testing/web-platform/tests/web-animations/resources/timing-utils.js new file mode 100644 index 0000000000..d7267f94f2 --- /dev/null +++ b/testing/web-platform/tests/web-animations/resources/timing-utils.js @@ -0,0 +1,39 @@ +'use strict'; + +// ======================================= +// +// Utility functions for testing timing +// +// ======================================= + + +// ------------------------------ +// Helper functions +// ------------------------------ + +// Utility function to check that a subset of timing properties have their +// default values. +function assert_default_timing_except(effect, propertiesToSkip) { + const defaults = { + delay: 0, + endDelay: 0, + fill: 'auto', + iterationStart: 0, + iterations: 1, + duration: 'auto', + direction: 'normal', + easing: 'linear', + }; + + for (const prop of Object.keys(defaults)) { + if (propertiesToSkip.includes(prop)) { + continue; + } + + assert_equals( + effect.getTiming()[prop], + defaults[prop], + `${prop} parameter has default value:` + ); + } +} diff --git a/testing/web-platform/tests/web-animations/resources/xhr-doc.py b/testing/web-platform/tests/web-animations/resources/xhr-doc.py new file mode 100644 index 0000000000..0023a44e22 --- /dev/null +++ b/testing/web-platform/tests/web-animations/resources/xhr-doc.py @@ -0,0 +1,5 @@ +def main(request, response): + headers = [(b"Content-type", b"text/html;charset=utf-8")] + content = u"<!doctype html><div id=test></div>" + + return headers, content diff --git a/testing/web-platform/tests/web-animations/responsive/assorted-lengths.html b/testing/web-platform/tests/web-animations/responsive/assorted-lengths.html new file mode 100644 index 0000000000..50e01f766c --- /dev/null +++ b/testing/web-platform/tests/web-animations/responsive/assorted-lengths.html @@ -0,0 +1,102 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<div id='container'> +<div id='element'></div> +</div> + +<script> +var element = document.getElementById('element'); +var container = document.getElementById('container'); + +test(function() { + element.style.fontSize = '10px'; + var player = element.animate([{bottom: '3em'}, {bottom: '5em'}], 10); + player.pause(); + player.currentTime = 5; + element.style.fontSize = '20px'; + assert_equals(getComputedStyle(element).bottom, '80px'); +}, 'bottom responsive to style changes'); + +test(function() { + element.style.fontSize = '10px'; + var player = element.animate([{height: '3em'}, {height: '5em'}], 10); + player.pause(); + player.currentTime = 5; + element.style.fontSize = '20px'; + assert_equals(getComputedStyle(element).height, '80px'); +}, 'height responsive to style changes'); + +test(function() { + element.style.fontSize = '10px'; + var player = element.animate([{letterSpacing: '3em'}, {letterSpacing: '5em'}], 10); + player.pause(); + player.currentTime = 5; + element.style.fontSize = '20px'; + assert_equals(getComputedStyle(element).letterSpacing, '80px'); +}, 'letterSpacing responsive to style changes'); + +test(function() { + var player = element.animate([{letterSpacing: 'normal'}, {letterSpacing: 'normal'}], 10); + player.pause(); + player.currentTime = 5; + assert_equals(getComputedStyle(element).letterSpacing, 'normal'); +}, 'letterSpacing can be normal'); + +test(function() { + element.style.fontSize = '10px'; + var player = element.animate([{marginRight: '3em'}, {marginRight: '5em'}], 10); + player.pause(); + player.currentTime = 5; + element.style.fontSize = '20px'; + assert_equals(getComputedStyle(element).marginRight, '80px'); +}, 'marginRight responsive to style changes'); + +test(function() { + element.style.fontSize = '10px'; + container.style.width = '300px'; + var player = element.animate([{marginRight: '3em'}, {marginRight: '50%'}], 10); + player.pause(); + player.currentTime = 5; + element.style.fontSize = '20px'; + container.style.width = '600px'; + assert_equals(getComputedStyle(element).marginRight, '180px'); +}, 'marginRight allows percentages'); + +test(function() { + element.style.fontSize = '10px'; + var player = element.animate([{outlineOffset: '3em'}, {outlineOffset: '5em'}], 10); + player.pause(); + player.currentTime = 5; + element.style.outline = 'dashed thin'; + element.style.fontSize = '20px'; + assert_equals(getComputedStyle(element).outlineOffset, '80px'); +}, 'outlineOffset responsive to style changes'); + +test(function() { + container.style.fontSize = '10px'; + var player = container.animate([{perspective: '3em'}, {perspective: '5em'}], 10); + player.pause(); + player.currentTime = 5; + container.style.fontSize = '20px'; + assert_equals(getComputedStyle(container).perspective, '80px'); +}, 'perspective responsive to style changes'); + +test(function() { + var player = element.animate([{perspective: 'none'}, {perspective: 'none'}], 10); + player.pause(); + player.currentTime = 10; + assert_equals(getComputedStyle(element).perspective, 'none'); +}, 'perspective can be none'); + +test(function() { + element.style.fontSize = '10px'; + var player = element.animate([{wordSpacing: '3em'}, {wordSpacing: '5em'}], 10); + player.pause(); + player.currentTime = 5; + element.style.fontSize = '20px'; + assert_equals(getComputedStyle(element).wordSpacing, '80px'); +}, 'wordSpacing responsive to style changes'); + +</script> diff --git a/testing/web-platform/tests/web-animations/responsive/backgroundPosition.html b/testing/web-platform/tests/web-animations/responsive/backgroundPosition.html new file mode 100644 index 0000000000..cd6c6991a3 --- /dev/null +++ b/testing/web-platform/tests/web-animations/responsive/backgroundPosition.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<div id='container'> +<div id='element'></div> +</div> + +<script> +var element = document.getElementById('element'); +var container = document.getElementById('container'); + +test(function() { + element.style.fontSize = '10px'; + var player = element.animate([ + {backgroundPosition: '10% 1em'}, + {backgroundPosition: '20% 5em'}], + 10); + player.pause(); + player.currentTime = 5; + element.style.fontSize = '20px'; + assert_equals(getComputedStyle(element).backgroundPosition, '15% 60px'); +}, 'Background position responsive to style changes'); + +</script> diff --git a/testing/web-platform/tests/web-animations/responsive/backgroundSize.html b/testing/web-platform/tests/web-animations/responsive/backgroundSize.html new file mode 100644 index 0000000000..1e9ccf96a1 --- /dev/null +++ b/testing/web-platform/tests/web-animations/responsive/backgroundSize.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<div id='container'> +<div id='element'></div> +</div> + +<script> +var element = document.getElementById('element'); +var container = document.getElementById('container'); + +test(function() { + container.style.fontSize = '50px'; + var player = element.animate([{backgroundSize: '300px 30px'}, {backgroundSize: '10em 1em'}], 10); + player.pause(); + player.currentTime = 5; + container.style.fontSize = '10px'; + assert_equals(getComputedStyle(element).backgroundSize, '200px 20px'); +}, 'Border image width responsive to font size changes'); + +</script> diff --git a/testing/web-platform/tests/web-animations/responsive/baselineShift.html b/testing/web-platform/tests/web-animations/responsive/baselineShift.html new file mode 100644 index 0000000000..4ccaaf33ac --- /dev/null +++ b/testing/web-platform/tests/web-animations/responsive/baselineShift.html @@ -0,0 +1,43 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<svg> + <text> + <tspan id='container'> + <tspan id='element'></tspan> + </tspan> + </text> +</svg> + +<script> +var element = document.getElementById('element'); +var container = document.getElementById('container'); + +test(function() { + element.style.fontSize = '10px'; + var player = element.animate([{baselineShift: '3em'}, {baselineShift: '5em'}], 10); + player.pause(); + player.currentTime = 5; + element.style.fontSize = '20px'; + assert_equals(getComputedStyle(element).baselineShift, '80px'); +}, 'baselineShift responsive to style changes'); + +test(function() { + container.style.baselineShift = 'sub'; + var player = element.animate([{baselineShift: 'inherit'}, {baselineShift: '20px'}], 4000); + player.pause(); + + player.currentTime = 1000; + assert_equals(getComputedStyle(element).baselineShift, 'sub'); + + container.style.baselineShift = 'super'; + assert_equals(getComputedStyle(element).baselineShift, 'super'); + + container.style.baselineShift = '100px'; + assert_equals(getComputedStyle(element).baselineShift, '80px'); + + container.style.baselineShift = 'sub'; + assert_equals(getComputedStyle(element).baselineShift, 'sub'); +}, 'baselineShift responsive to inherited changes'); +</script> diff --git a/testing/web-platform/tests/web-animations/responsive/borderImageWidth.html b/testing/web-platform/tests/web-animations/responsive/borderImageWidth.html new file mode 100644 index 0000000000..4b832889bd --- /dev/null +++ b/testing/web-platform/tests/web-animations/responsive/borderImageWidth.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<div id='container'> +<div id='element'></div> +</div> + +<script> +var element = document.getElementById('element'); +var container = document.getElementById('container'); + +test(function() { + container.style.fontSize = '50px'; + var player = element.animate([{borderImageWidth: '300px 30px'}, {borderImageWidth: '10em 1em'}], 10); + player.pause(); + player.currentTime = 5; + container.style.fontSize = '10px'; + assert_equals(getComputedStyle(element).borderImageWidth, '200px 20px'); +}, 'Border image width responsive to font size changes'); + +</script> diff --git a/testing/web-platform/tests/web-animations/responsive/borderRadius.html b/testing/web-platform/tests/web-animations/responsive/borderRadius.html new file mode 100644 index 0000000000..c59696ec90 --- /dev/null +++ b/testing/web-platform/tests/web-animations/responsive/borderRadius.html @@ -0,0 +1,39 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id='element'></div> +<script> + +var element = document.getElementById('element'); + +test(function() { + var keyframes = [ + {borderTopLeftRadius: '8% 16%'}, + {borderTopLeftRadius: '12% 24%'} + ]; + + element.style.width = '100'; + element.style.height = '200'; + var player = element.animate(keyframes, 10); + player.pause(); + player.currentTime = 5; + element.style.width = '300'; + element.style.height = '600'; + assert_equals(getComputedStyle(element).borderTopLeftRadius, '10% 20%'); +}, 'Border radius percentages are supported'); + +test(function() { + var keyframes = [ + {borderTopLeftRadius: '8em 16em'}, + {borderTopLeftRadius: '12em 24em'} + ]; + + element.style.fontSize = '10px'; + var player = element.animate(keyframes, 10); + player.pause(); + player.currentTime = 5; + element.style.fontSize = '20px'; + assert_equals(getComputedStyle(element).borderTopLeftRadius, '200px 400px'); +}, 'Border radius lengths respond to style changes'); + +</script> diff --git a/testing/web-platform/tests/web-animations/responsive/borderWidth.html b/testing/web-platform/tests/web-animations/responsive/borderWidth.html new file mode 100644 index 0000000000..090d9a185a --- /dev/null +++ b/testing/web-platform/tests/web-animations/responsive/borderWidth.html @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<div id='container'> +<div id='element'></div> +</div> + +<script> +var element = document.getElementById('element'); +var container = document.getElementById('container'); + +test(function() { + element.style.fontSize = '10px'; + element.style.border = 'none'; + var player = element.animate([{borderBottomWidth: '300px'}, {borderBottomWidth: '10em'}], 10); + player.pause(); + player.currentTime = 5; + element.style.border = 'solid'; + assert_equals(getComputedStyle(element).borderBottomWidth, '200px'); +}, 'Border width responsive to border style changes'); + +test(function() { + element.style.fontSize = '50px'; + element.style.border = 'solid'; + var player = element.animate([{borderBottomWidth: '300px'}, {borderBottomWidth: '10em'}], 10); + player.pause(); + player.currentTime = 5; + element.style.fontSize = '10px'; + assert_equals(getComputedStyle(element).borderBottomWidth, '200px'); +}, 'Border width responsive to font size changes'); + +</script> diff --git a/testing/web-platform/tests/web-animations/responsive/boxShadow.html b/testing/web-platform/tests/web-animations/responsive/boxShadow.html new file mode 100644 index 0000000000..bd1911132d --- /dev/null +++ b/testing/web-platform/tests/web-animations/responsive/boxShadow.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id='container'> + <div id='element'></div> +</div> +<script> + +var container = document.getElementById('container'); +var element = document.getElementById('element'); + +test(function() { + container.style.fontSize = '10px'; + container.style.color = 'red'; + + var keyframes = [ + {boxShadow: '10em 10em currentColor'}, + {boxShadow: '10em 10em currentColor'} + ]; + var player = element.animate(keyframes, 10); + player.pause(); + player.currentTime = 5; + + var boxShadow = getComputedStyle(element).boxShadow; + container.style.fontSize = '20px'; + container.style.color = 'green'; + assert_not_equals(getComputedStyle(element).boxShadow, boxShadow); +}, 'boxShadow responsive to style changes'); + +</script> diff --git a/testing/web-platform/tests/web-animations/responsive/clip.html b/testing/web-platform/tests/web-animations/responsive/clip.html new file mode 100644 index 0000000000..316e977576 --- /dev/null +++ b/testing/web-platform/tests/web-animations/responsive/clip.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id='container'> + <div id='element'></div> +</div> +<script> + +var container = document.getElementById('container'); +var element = document.getElementById('element'); + +test(function() { + var keyframes = [ + {clip: 'inherit'}, + {clip: 'rect(10px, 20px, 30px, 40px)'} + ]; + + container.style.clip = 'rect(10px, 20px, 30px, 40px)'; + var player = element.animate(keyframes, 20); + + player.pause(); + player.currentTime = 5; + assert_equals(getComputedStyle(element).clip, 'rect(10px, 20px, 30px, 40px)'); + + container.style.clip = 'rect(10px, 20px, 430px, 440px)'; + assert_equals(getComputedStyle(element).clip, 'rect(10px, 20px, 330px, 340px)'); +}, 'clip responsive to inherited clip changes'); + +test(function() { + var keyframes = [ + {clip: 'inherit'}, + {clip: 'rect(10px, 20px, 30px, auto)'} + ]; + + container.style.clip = 'auto'; + var player = element.animate(keyframes, 20); + + player.pause(); + player.currentTime = 5; + assert_equals(getComputedStyle(element).clip, 'auto'); + + container.style.clip = 'rect(410px, 420px, 30px, auto)'; + assert_equals(getComputedStyle(element).clip, 'rect(310px, 320px, 30px, auto)'); +}, 'clip responsive to inherited clip changes from auto'); +</script> diff --git a/testing/web-platform/tests/web-animations/responsive/columnCount.html b/testing/web-platform/tests/web-animations/responsive/columnCount.html new file mode 100644 index 0000000000..c92d5cbf8f --- /dev/null +++ b/testing/web-platform/tests/web-animations/responsive/columnCount.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id='container'> + <div id='element'></div> +</div> +<script> + +var container = document.getElementById('container'); +var element = document.getElementById('element'); + +test(function() { + var keyframes = [ + {columnCount: 'inherit'}, + {columnCount: '6'} + ]; + + container.style.columnCount = 'auto'; + var player = element.animate(keyframes, 20); + + player.pause(); + player.currentTime = 5; + assert_equals(getComputedStyle(element).columnCount, 'auto'); + + container.style.columnCount = '2'; + assert_equals(getComputedStyle(element).columnCount, '3'); + + player.currentTime = 10; + container.style.columnCount = '8'; + assert_equals(getComputedStyle(element).columnCount, '7'); +}, 'column-count responsive to inherited column-count changes'); +</script> diff --git a/testing/web-platform/tests/web-animations/responsive/columnGap.html b/testing/web-platform/tests/web-animations/responsive/columnGap.html new file mode 100644 index 0000000000..43d44152cb --- /dev/null +++ b/testing/web-platform/tests/web-animations/responsive/columnGap.html @@ -0,0 +1,49 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<div id='container'> +<div id='element'></div> +</div> + +<script> +var element = document.getElementById('element'); +var container = document.getElementById('container'); + +test(function() { + element.style.fontSize = '10px'; + var player = element.animate([{columnGap: '3em'}, {columnGap: '5em'}], 10); + player.pause(); + player.currentTime = 5; + element.style.fontSize = '20px'; + assert_equals(getComputedStyle(element).columnGap, '80px'); +}, 'column-gap responsive to style changes'); + +test(function() { + element.style.fontSize = '10px'; + var player = element.animate([{columnGap: '40px'}, {columnGap: 'calc(40px - 2em)'}], 10); + player.pause(); + + player.currentTime = 5; + element.style.fontSize = '40px'; + assert_equals(getComputedStyle(element).columnGap, '20px'); + + player.currentTime = 7.5; + assert_equals(getComputedStyle(element).columnGap, '10px'); +}, 'column-gap clamped to 0px on keyframes'); + +test(function() { + container.style.columnGap = 'normal'; + var player = element.animate([{columnGap: 'inherit'}, {columnGap: '20px'}], 4000); + player.pause(); + + player.currentTime = 1000; + assert_equals(getComputedStyle(element).columnGap, 'normal'); + + container.style.columnGap = '100px'; + assert_equals(getComputedStyle(element).columnGap, '80px'); + + container.style.columnGap = 'normal'; + assert_equals(getComputedStyle(element).columnGap, 'normal'); +}, 'column-gap responsive to inherited changes'); +</script> diff --git a/testing/web-platform/tests/web-animations/responsive/d.html b/testing/web-platform/tests/web-animations/responsive/d.html new file mode 100644 index 0000000000..55c6c23a0e --- /dev/null +++ b/testing/web-platform/tests/web-animations/responsive/d.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id='container'> + <div id='element'></div> +</div> +<script> + +var container = document.getElementById('container'); +var element = document.getElementById('element'); + +test(function() { + var keyframes = [ + {d: 'inherit'}, + {d: 'path("M 0 0 H 200")'} + ]; + + container.style.d = 'path("M 0 0 H 100")'; + var player = element.animate(keyframes, 10); + + player.pause(); + player.currentTime = 5; + container.style.d = 'path("M 0 0 H 400")'; + assert_equals(getComputedStyle(element).d, 'path("M 0 0 H 300")'); +}, 'd responsive to inherited d changes'); + +</script> diff --git a/testing/web-platform/tests/web-animations/responsive/font-size-adjust.html b/testing/web-platform/tests/web-animations/responsive/font-size-adjust.html new file mode 100644 index 0000000000..282ec7fede --- /dev/null +++ b/testing/web-platform/tests/web-animations/responsive/font-size-adjust.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id='container'> + <div id='element'></div> +</div> +<script> + +var container = document.getElementById('container'); +var element = document.getElementById('element'); + +test(function() { + var keyframes = [ + {fontSizeAdjust: 'inherit'}, + {fontSizeAdjust: '6'} + ]; + + container.style.fontSizeAdjust = 'none'; + var player = element.animate(keyframes, 20); + + player.pause(); + player.currentTime = 5; + assert_equals(getComputedStyle(element).fontSizeAdjust, 'none'); + + container.style.fontSizeAdjust = '2'; + assert_equals(getComputedStyle(element).fontSizeAdjust, '3'); + + player.currentTime = 10; + container.style.fontSizeAdjust = '8'; + assert_equals(getComputedStyle(element).fontSizeAdjust, '7'); +}, 'font-size-adjust responsive to inherited font-size-adjust changes'); +</script> diff --git a/testing/web-platform/tests/web-animations/responsive/fontSize.html b/testing/web-platform/tests/web-animations/responsive/fontSize.html new file mode 100644 index 0000000000..d65aeacd46 --- /dev/null +++ b/testing/web-platform/tests/web-animations/responsive/fontSize.html @@ -0,0 +1,72 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id='container'> + <div id='element'></div> +</div> +<script> + +var container = document.getElementById('container'); +var element = document.getElementById('element'); + +test(function() { + var keyframes = [ + {fontSize: 'larger'}, + {fontSize: 'larger'} + ]; + + container.style.fontSize = 'small'; + var player = element.animate(keyframes, 10); + player.pause(); + player.currentTime = 5; + fontSize = getComputedStyle(element).fontSize; + container.style.fontSize = 'medium'; + assert_not_equals(getComputedStyle(element).fontSize, fontSize); +}, 'Relative font size larger responsive to style changes'); + +test(function() { + var keyframes = [ + {fontSize: 'smaller'}, + {fontSize: 'smaller'} + ]; + + container.style.fontSize = 'large'; + var player = element.animate(keyframes, 10); + player.pause(); + player.currentTime = 5; + fontSize = getComputedStyle(element).fontSize; + container.style.fontSize = 'medium'; + assert_not_equals(getComputedStyle(element).fontSize, fontSize); +}, 'Relative font size smaller responsive to style changes'); + +test(function() { + var keyframes = [ + {fontSize: 'medium'}, + {fontSize: 'medium'} + ]; + + container.style.fontFamily = 'cursive'; + var player = element.animate(keyframes, 10); + player.pause(); + player.currentTime = 5; + fontSize = getComputedStyle(element).fontSize; + container.style.fontFamily = 'monospace'; + assert_not_equals(getComputedStyle(element).fontSize, fontSize); +}, 'Font size medium responsive to style changes'); + +test(function() { + var keyframes = [ + {fontSize: 'initial'}, + {fontSize: 'initial'} + ]; + + container.style.fontFamily = 'monospace'; + var player = element.animate(keyframes, 10); + player.pause(); + player.currentTime = 5; + fontSize = getComputedStyle(element).fontSize; + container.style.fontFamily = 'serif'; + assert_not_equals(getComputedStyle(element).fontSize, fontSize); +}, 'Font size initial responsive to style changes'); + +</script> diff --git a/testing/web-platform/tests/web-animations/responsive/fontWeight.html b/testing/web-platform/tests/web-animations/responsive/fontWeight.html new file mode 100644 index 0000000000..e8fbbae0e7 --- /dev/null +++ b/testing/web-platform/tests/web-animations/responsive/fontWeight.html @@ -0,0 +1,42 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id='container'> + <div id='element'></div> +</div> +<script> + +var container = document.getElementById('container'); +var element = document.getElementById('element'); + +test(function() { + var keyframes = [ + {fontWeight: 'bolder'}, + {fontWeight: 'bolder'} + ]; + + container.style.fontWeight = '100'; + var player = element.animate(keyframes, 10); + player.pause(); + player.currentTime = 5; + fontWeight = getComputedStyle(element).fontWeight; + container.style.fontWeight = '800'; + assert_not_equals(getComputedStyle(element).fontWeight, fontWeight); +}, 'Relative font weight bolder responsive to style changes'); + +test(function() { + var keyframes = [ + {fontWeight: 'lighter'}, + {fontWeight: 'lighter'} + ]; + + container.style.fontWeight = '900'; + var player = element.animate(keyframes, 10); + player.pause(); + player.currentTime = 5; + fontWeight = getComputedStyle(element).fontWeight; + container.style.fontWeight = '200'; + assert_not_equals(getComputedStyle(element).fontWeight, fontWeight); +}, 'Relative font weight lighter responsive to style changes'); + +</script> diff --git a/testing/web-platform/tests/web-animations/responsive/lineHeight.html b/testing/web-platform/tests/web-animations/responsive/lineHeight.html new file mode 100644 index 0000000000..03d45e54c1 --- /dev/null +++ b/testing/web-platform/tests/web-animations/responsive/lineHeight.html @@ -0,0 +1,80 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<div id='container'> +<div id='element'></div> +</div> + +<script> +var element = document.getElementById('element'); +var container = document.getElementById('container'); + +test(function() { + element.style.fontSize = '10px'; + var player = element.animate([{lineHeight: '3em'}, {lineHeight: '5em'}], 10); + player.pause(); + player.currentTime = 5; + element.style.fontSize = '20px'; + assert_equals(getComputedStyle(element).lineHeight, '80px'); +}, 'lineHeight responsive to style changes'); + +test(function() { + element.style.fontSize = '10px'; + var player = element.animate([{lineHeight: '40px'}, {lineHeight: 'calc(40px - 2em)'}], 10); + player.pause(); + + player.currentTime = 5; + element.style.fontSize = '40px'; + assert_equals(getComputedStyle(element).lineHeight, '20px'); + + player.currentTime = 7.5; + assert_equals(getComputedStyle(element).lineHeight, '10px'); +}, 'lineHeight clamped to 0px on keyframes'); + +test(function() { + container.style.lineHeight = 'normal'; + var player = element.animate([{lineHeight: 'inherit'}, {lineHeight: '20px'}], 4000); + player.pause(); + + player.currentTime = 1000; + assert_equals(getComputedStyle(element).lineHeight, 'normal'); + + container.style.lineHeight = '100px'; + assert_equals(getComputedStyle(element).lineHeight, '80px'); + + container.style.lineHeight = 'normal'; + assert_equals(getComputedStyle(element).lineHeight, 'normal'); +}, 'lineHeight responsive to inherited changes from keyword'); + +test(function() { + container.style.fontSize = '10px'; + container.style.lineHeight = '1.0'; + const expected = getComputedStyle(container).lineHeight; + var player = element.animate([{lineHeight: 'inherit'}, {lineHeight: '20px'}], 4000); + player.pause(); + + player.currentTime = 1000; + getComputedStyle(element).lineHeight; + + container.style.lineHeight = '100px'; + assert_equals(getComputedStyle(element).lineHeight, '80px'); +}, 'lineHeight responsive to inherited changes from number'); + +test(function() { + container.style.fontSize = '10px'; + container.style.lineHeight = '1'; + var player = element.animate([{lineHeight: 'inherit'}, {lineHeight: '2'}], 4000); + player.pause(); + + player.currentTime = 1000; + const expected = getComputedStyle(element).lineHeight; + + container.style.lineHeight = '97px'; + assert_equals(getComputedStyle(element).lineHeight, '97px'); + + container.style.lineHeight = '1'; + assert_equals(getComputedStyle(element).lineHeight, expected); +}, 'lineHeight responsive to inherited changes from length'); + +</script> diff --git a/testing/web-platform/tests/web-animations/responsive/minHeight.html b/testing/web-platform/tests/web-animations/responsive/minHeight.html new file mode 100644 index 0000000000..07474bc3f4 --- /dev/null +++ b/testing/web-platform/tests/web-animations/responsive/minHeight.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<div id='container'> +<div id='element'></div> +</div> + +<script> +var element = document.getElementById('element'); +var container = document.getElementById('container'); + +test(function() { + element.style.fontSize = '10px'; + var player = element.animate([{minHeight: '3em'}, {minHeight: '5em'}], 10); + player.pause(); + player.currentTime = 5; + element.style.fontSize = '20px'; + assert_equals(getComputedStyle(element).minHeight, '80px'); +}, 'minHeight responsive to style changes'); + +test(function() { + element.style.fontSize = '10px'; + var player = element.animate([{minHeight: '40px'}, {minHeight: 'calc(40px - 2em)'}], 10); + player.pause(); + + player.currentTime = 5; + element.style.fontSize = '40px'; + assert_equals(getComputedStyle(element).minHeight, '20px'); + + player.currentTime = 7.5; + assert_equals(getComputedStyle(element).minHeight, '10px'); +}, 'minHeight clamped to 0px on keyframes'); + +</script> diff --git a/testing/web-platform/tests/web-animations/responsive/offset-path.html b/testing/web-platform/tests/web-animations/responsive/offset-path.html new file mode 100644 index 0000000000..eff8ede60f --- /dev/null +++ b/testing/web-platform/tests/web-animations/responsive/offset-path.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id='container'> + <div id='element'></div> +</div> +<script> + +var container = document.getElementById('container'); +var element = document.getElementById('element'); + +test(function() { + var keyframes = [ + {offsetPath: 'inherit'}, + {offsetPath: 'path("M 0 0 H 200")'} + ]; + + container.style.offsetPath = 'path("M 0 0 H 100")'; + var player = element.animate(keyframes, 10); + + player.pause(); + player.currentTime = 5; + container.style.offsetPath = 'path("M 0 0 H 400")'; + assert_equals(getComputedStyle(element).offsetPath, 'path("M 0 0 H 300")'); +}, 'offset-path responsive to inherited offset-path changes'); + +</script> diff --git a/testing/web-platform/tests/web-animations/responsive/offsetDistance.html b/testing/web-platform/tests/web-animations/responsive/offsetDistance.html new file mode 100644 index 0000000000..d56da9c767 --- /dev/null +++ b/testing/web-platform/tests/web-animations/responsive/offsetDistance.html @@ -0,0 +1,57 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id='container'> + <div id='element'></div> +</div> +<script> + +var container = document.getElementById('container'); +var element = document.getElementById('element'); + +test(function() { + var keyframes = [ + {offsetDistance: '4%'}, + {offsetDistance: '12%'} + ]; + + element.style.width = '100'; + element.style.height = '200'; + var player = element.animate(keyframes, 20); + player.pause(); + player.currentTime = 15; + element.style.width = '300'; + element.style.height = '600'; + assert_equals(getComputedStyle(element).offsetDistance, '10%'); +}, 'offsetDistance percentages are supported'); + +test(function() { + var keyframes = [ + {offsetDistance: '8em'}, + {offsetDistance: '16em'} + ]; + + element.style.fontSize = '10px'; + var player = element.animate(keyframes, 20); + player.pause(); + player.currentTime = 5; + element.style.fontSize = '20px'; + assert_equals(getComputedStyle(element).offsetDistance, '200px'); +}, 'offsetDistance lengths respond to style changes'); + +test(function() { + var keyframes = [ + {offsetDistance: 'inherit'}, + {offsetDistance: '200px'} + ]; + + container.style.offsetDistance = '100px'; + var player = element.animate(keyframes, 10); + + player.pause(); + player.currentTime = 5; + + container.style.offsetDistance = '400px'; + assert_equals(getComputedStyle(element).offsetDistance, '300px'); +}, 'offsetDistance responsive to inherited offsetDistance changes'); +</script> diff --git a/testing/web-platform/tests/web-animations/responsive/offsetRotate.html b/testing/web-platform/tests/web-animations/responsive/offsetRotate.html new file mode 100644 index 0000000000..f0957e15c5 --- /dev/null +++ b/testing/web-platform/tests/web-animations/responsive/offsetRotate.html @@ -0,0 +1,43 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id='container'> + <div id='element'></div> +</div> +<script> + +var container = document.getElementById('container'); +var element = document.getElementById('element'); + +test(function() { + var keyframes = [ + {offsetRotate: 'inherit'}, + {offsetRotate: 'auto 200deg'} + ]; + + container.style.offsetRotate = 'auto 100deg'; + var player = element.animate(keyframes, 10); + + player.pause(); + player.currentTime = 5; + assert_equals(getComputedStyle(element).offsetRotate, 'auto 150deg'); + + container.style.offsetRotate = '400deg'; + assert_equals(getComputedStyle(element).offsetRotate, 'auto 200deg'); + + container.style.offsetRotate = '400deg auto'; + assert_equals(getComputedStyle(element).offsetRotate, 'auto 300deg'); + + container.style.offsetRotate = '800deg auto'; + assert_equals(getComputedStyle(element).offsetRotate, 'auto 500deg'); + + container.style.offsetRotate = '400deg'; + assert_equals(getComputedStyle(element).offsetRotate, 'auto 200deg'); + + container.style.offsetRotate = '800deg auto'; + assert_equals(getComputedStyle(element).offsetRotate, 'auto 500deg'); + + container.style.offsetRotate = '400deg auto'; + assert_equals(getComputedStyle(element).offsetRotate, 'auto 300deg'); +}, 'offsetRotate responsive to inherited offsetRotate changes'); +</script> diff --git a/testing/web-platform/tests/web-animations/responsive/opacity.html b/testing/web-platform/tests/web-animations/responsive/opacity.html new file mode 100644 index 0000000000..0bedfc879a --- /dev/null +++ b/testing/web-platform/tests/web-animations/responsive/opacity.html @@ -0,0 +1,48 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id='container'> + <div id='element'></div> +</div> +<script> + +'use strict'; +var container = document.getElementById('container'); +var element = document.getElementById('element'); + +var properties = [ + 'fillOpacity', + 'floodOpacity', + 'opacity', + 'shapeImageThreshold', + 'stopOpacity', + 'strokeOpacity', +]; + +for (var property of properties) { + test(function() { + var initialKeyframe = {}; + initialKeyframe[property] = 'inherit'; + var finalKeyframe = {}; + finalKeyframe[property] = '0.5'; + var keyframes = [ initialKeyframe, finalKeyframe ]; + + container.style[property] = 1; + var player = element.animate(keyframes, 10); + + player.pause(); + player.currentTime = 5; + assert_equals(getComputedStyle(element)[property], '0.75'); + + container.style[property] = 0.25; + assert_equals(getComputedStyle(element)[property], '0.375'); + + container.style[property] = -0.5; // clamps to 0 + assert_equals(getComputedStyle(element)[property], '0.25'); + + container.style[property] = 2; // clamps to 1 + assert_equals(getComputedStyle(element)[property], '0.75'); + }, property + ' responsive to inherited changes'); +} + +</script> diff --git a/testing/web-platform/tests/web-animations/responsive/perspective.html b/testing/web-platform/tests/web-animations/responsive/perspective.html new file mode 100644 index 0000000000..f32b4dbad3 --- /dev/null +++ b/testing/web-platform/tests/web-animations/responsive/perspective.html @@ -0,0 +1,49 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<div id='container'> +<div id='element'></div> +</div> + +<script> +var element = document.getElementById('element'); +var container = document.getElementById('container'); + +test(function() { + element.style.fontSize = '10px'; + var player = element.animate([{perspective: '3em'}, {perspective: '5em'}], 10); + player.pause(); + player.currentTime = 5; + element.style.fontSize = '20px'; + assert_equals(getComputedStyle(element).perspective, '80px'); +}, 'perspective responsive to style changes'); + +test(function() { + element.style.fontSize = '10px'; + var player = element.animate([{perspective: '40px'}, {perspective: 'calc(40px - 2em)'}], 10); + player.pause(); + + player.currentTime = 5; + element.style.fontSize = '40px'; + assert_equals(getComputedStyle(element).perspective, '20px'); + + player.currentTime = 7.5; + assert_equals(getComputedStyle(element).perspective, '10px'); +}, 'perspective clamped to 0px on keyframes'); + +test(function() { + container.style.perspective = 'none'; + var player = element.animate([{perspective: 'inherit'}, {perspective: '20px'}], 4000); + player.pause(); + + player.currentTime = 1000; + assert_equals(getComputedStyle(element).perspective, 'none'); + + container.style.perspective = '100px'; + assert_equals(getComputedStyle(element).perspective, '80px'); + + container.style.perspective = 'none'; + assert_equals(getComputedStyle(element).perspective, 'none'); +}, 'perspective responsive to inherited changes'); +</script> diff --git a/testing/web-platform/tests/web-animations/responsive/resources/block.html b/testing/web-platform/tests/web-animations/responsive/resources/block.html new file mode 100644 index 0000000000..8284055969 --- /dev/null +++ b/testing/web-platform/tests/web-animations/responsive/resources/block.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<style> + .testBlock { + width: 100px; + height: 100px; + background: #00cc66; + border-top: 50px solid #ffff00; + } +</style> +<div class="testBlock" id="testBlock"></div>
\ No newline at end of file diff --git a/testing/web-platform/tests/web-animations/responsive/rotate.html b/testing/web-platform/tests/web-animations/responsive/rotate.html new file mode 100644 index 0000000000..b0e4213200 --- /dev/null +++ b/testing/web-platform/tests/web-animations/responsive/rotate.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<div id='container'> +<div id='element'></div> +</div> + +<script> +var element = document.getElementById('element'); +var container = document.getElementById('container'); + +test(function() { + container.style.rotate = 'none'; + var player = element.animate([{rotate: 'inherit'}, {rotate: '100deg'}], 10); + player.pause(); + player.currentTime = 2; + container.style.rotate = '200deg'; + assert_equals(getComputedStyle(element).rotate, '180deg'); +}, 'Rotate responsive to inherited rotate changes'); + +</script> diff --git a/testing/web-platform/tests/web-animations/responsive/rowGap.html b/testing/web-platform/tests/web-animations/responsive/rowGap.html new file mode 100644 index 0000000000..b7dae62f23 --- /dev/null +++ b/testing/web-platform/tests/web-animations/responsive/rowGap.html @@ -0,0 +1,49 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<div id='container'> +<div id='element'></div> +</div> + +<script> +var element = document.getElementById('element'); +var container = document.getElementById('container'); + +test(function() { + element.style.fontSize = '10px'; + var player = element.animate([{rowGap: '3em'}, {rowGap: '5em'}], 10); + player.pause(); + player.currentTime = 5; + element.style.fontSize = '20px'; + assert_equals(getComputedStyle(element).rowGap, '80px'); +}, 'row-gap responsive to style changes'); + +test(function() { + element.style.fontSize = '10px'; + var player = element.animate([{rowGap: '40px'}, {rowGap: 'calc(40px - 2em)'}], 10); + player.pause(); + + player.currentTime = 5; + element.style.fontSize = '40px'; + assert_equals(getComputedStyle(element).rowGap, '20px'); + + player.currentTime = 7.5; + assert_equals(getComputedStyle(element).rowGap, '10px'); +}, 'row-gap clamped to 0px on keyframes'); + +test(function() { + container.style.rowGap = 'normal'; + var player = element.animate([{rowGap: 'inherit'}, {rowGap: '20px'}], 4000); + player.pause(); + + player.currentTime = 1000; + assert_equals(getComputedStyle(element).rowGap, 'normal'); + + container.style.rowGap = '100px'; + assert_equals(getComputedStyle(element).rowGap, '80px'); + + container.style.rowGap = 'normal'; + assert_equals(getComputedStyle(element).rowGap, 'normal'); +}, 'row-gap responsive to inherited changes'); +</script> diff --git a/testing/web-platform/tests/web-animations/responsive/shapeMargin.html b/testing/web-platform/tests/web-animations/responsive/shapeMargin.html new file mode 100644 index 0000000000..02d6151ad0 --- /dev/null +++ b/testing/web-platform/tests/web-animations/responsive/shapeMargin.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<div id='container'> +<div id='element'></div> +</div> + +<script> +var element = document.getElementById('element'); +var container = document.getElementById('container'); + +test(function() { + element.style.fontSize = '10px'; + element.style.width = '200px'; + var player = element.animate([{shapeMargin: '3em'}, {shapeMargin: '80%'}], 10); + player.pause(); + player.currentTime = 5; + element.style.fontSize = '20px'; + element.style.width = '100px'; + assert_equals(getComputedStyle(element).shapeMargin, 'calc(40% + 30px)'); +}, 'shapeMargin responsive to style changes'); + +</script> diff --git a/testing/web-platform/tests/web-animations/responsive/shapeOutside.html b/testing/web-platform/tests/web-animations/responsive/shapeOutside.html new file mode 100644 index 0000000000..2a54576a33 --- /dev/null +++ b/testing/web-platform/tests/web-animations/responsive/shapeOutside.html @@ -0,0 +1,59 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id='container'> + <div id='element'></div> +</div> +<script> + +var container = document.getElementById('container'); +var element = document.getElementById('element'); + +test(function() { + container.style.fontSize = '10px'; + + var keyframes = [ + {shapeOutside: 'circle(10em at 50% 50%)'}, + {shapeOutside: 'circle(10em at 50% 50%)'} + ]; + var player = element.animate(keyframes, 10); + player.pause(); + player.currentTime = 5; + + var shapeOutside = getComputedStyle(element).shapeOutside; + container.style.fontSize = '20px'; + assert_not_equals(getComputedStyle(element).shapeOutside, shapeOutside); +}, 'shapeOutside responsive to style changes'); + +test(function() { + var keyframes = [ + {shapeOutside: 'inherit'}, + {shapeOutside: 'circle(200px at 50% 50%)'} + ]; + + container.style.shapeOutside = 'circle(100px at 50% 50%)'; + var player = element.animate(keyframes, 10); + + player.pause(); + player.currentTime = 5; + assert_equals(getComputedStyle(element).shapeOutside, 'circle(150px at 50% 50%)'); + + container.style.shapeOutside = 'inset(100%)'; + assert_equals(getComputedStyle(element).shapeOutside, 'circle(200px at 50% 50%)'); + + container.style.shapeOutside = 'circle(400px at 50% 50%)'; + assert_equals(getComputedStyle(element).shapeOutside, 'circle(300px at 50% 50%)'); + + container.style.shapeOutside = 'circle(800px at 50% 50%)'; + assert_equals(getComputedStyle(element).shapeOutside, 'circle(500px at 50% 50%)'); + + container.style.shapeOutside = 'inset(100%)'; + assert_equals(getComputedStyle(element).shapeOutside, 'circle(200px at 50% 50%)'); + + container.style.shapeOutside = 'circle(800px at 50% 50%)'; + assert_equals(getComputedStyle(element).shapeOutside, 'circle(500px at 50% 50%)'); + + container.style.shapeOutside = 'circle(400px at 50% 50%)'; + assert_equals(getComputedStyle(element).shapeOutside, 'circle(300px at 50% 50%)'); +}, 'shapeOutside responsive to inherited shapeOutside changes'); +</script> diff --git a/testing/web-platform/tests/web-animations/responsive/strokeDasharray.html b/testing/web-platform/tests/web-animations/responsive/strokeDasharray.html new file mode 100644 index 0000000000..a1ccb30e90 --- /dev/null +++ b/testing/web-platform/tests/web-animations/responsive/strokeDasharray.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id='container'> + <div id='element'></div> +</div> +<script> + +var container = document.getElementById('container'); +var element = document.getElementById('element'); + +test(function() { + container.style.fontSize = '10px'; + + var keyframes = [ + {strokeDasharray: '10em 10em'}, + {strokeDasharray: '10em 10em'} + ]; + var player = element.animate(keyframes, 10); + player.pause(); + player.currentTime = 5; + + var strokeDasharray = getComputedStyle(element).strokeDasharray; + container.style.fontSize = '20px'; + assert_not_equals(getComputedStyle(element).strokeDasharray, strokeDasharray); +}, 'strokeDasharray responsive to style changes'); + +</script> diff --git a/testing/web-platform/tests/web-animations/responsive/text-size-adjust.html b/testing/web-platform/tests/web-animations/responsive/text-size-adjust.html new file mode 100644 index 0000000000..203b067a77 --- /dev/null +++ b/testing/web-platform/tests/web-animations/responsive/text-size-adjust.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id='container'> + <div id='element'></div> +</div> +<script> + +var container = document.getElementById('container'); +var element = document.getElementById('element'); + +test(function() { + var keyframes = [ + {textSizeAdjust: 'inherit'}, + {textSizeAdjust: '60%'} + ]; + + container.style.textSizeAdjust = '100%'; + var player = element.animate(keyframes, 20); + + player.pause(); + player.currentTime = 5; + assert_equals(getComputedStyle(element).textSizeAdjust, '90%'); + + player.currentTime = 10; + container.style.textSizeAdjust = '50%'; + assert_equals(getComputedStyle(element).textSizeAdjust, '55%'); +}, 'text-size-adjust responsive to inherited text-size-adjust changes'); +</script> diff --git a/testing/web-platform/tests/web-animations/responsive/textIndent.html b/testing/web-platform/tests/web-animations/responsive/textIndent.html new file mode 100644 index 0000000000..430417d28b --- /dev/null +++ b/testing/web-platform/tests/web-animations/responsive/textIndent.html @@ -0,0 +1,59 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id='container'> + <div id='element'></div> +</div> +<script> + +var container = document.getElementById('container'); +var element = document.getElementById('element'); + +test(function() { + container.style.fontSize = '10px'; + + var keyframes = [ + {textIndent: '10em hanging'}, + {textIndent: '10em hanging'} + ]; + var player = element.animate(keyframes, 10); + player.pause(); + player.currentTime = 5; + + var textIndent = getComputedStyle(element).textIndent; + container.style.fontSize = '20px'; + assert_not_equals(getComputedStyle(element).textIndent, textIndent); +}, 'textIndent responsive to style changes'); + +test(function() { + var keyframes = [ + {textIndent: 'inherit'}, + {textIndent: '200px hanging each-line'} + ]; + + container.style.textIndent = '100px hanging each-line'; + var player = element.animate(keyframes, 10); + + player.pause(); + player.currentTime = 5; + assert_equals(getComputedStyle(element).textIndent, '150px hanging each-line'); + + container.style.textIndent = '400px hanging'; + assert_equals(getComputedStyle(element).textIndent, '200px hanging each-line'); + + container.style.textIndent = '400px hanging each-line'; + assert_equals(getComputedStyle(element).textIndent, '300px hanging each-line'); + + container.style.textIndent = '800px hanging each-line'; + assert_equals(getComputedStyle(element).textIndent, '500px hanging each-line'); + + container.style.textIndent = '400px each-line'; + assert_equals(getComputedStyle(element).textIndent, '200px hanging each-line'); + + container.style.textIndent = '800px hanging each-line'; + assert_equals(getComputedStyle(element).textIndent, '500px hanging each-line'); + + container.style.textIndent = '400px hanging each-line'; + assert_equals(getComputedStyle(element).textIndent, '300px hanging each-line'); +}, 'textIndent responsive to inherited textIndent changes'); +</script> diff --git a/testing/web-platform/tests/web-animations/responsive/to-color-change.html b/testing/web-platform/tests/web-animations/responsive/to-color-change.html new file mode 100644 index 0000000000..6c3fcccf62 --- /dev/null +++ b/testing/web-platform/tests/web-animations/responsive/to-color-change.html @@ -0,0 +1,253 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<style> +a { visibility: hidden; } +</style> + +<div id='container'> + <div id='child'></div> +</div> +<div id='element'></div> +<svg> + <rect id='svgElement'></rect> +</svg> + +<a href='example.com/unvisited' id='unvisited'><div id='unvisitedchild'>Unvisited</div></a> +<a href='' id='visited'><div id='visitedchild'>Visited</div></a> + +<script> +'use strict'; +var container = document.getElementById('container'); +var child = document.getElementById('child'); +var element = document.getElementById('element'); +var unvisited = document.getElementById('unvisited'); +var visited = document.getElementById('visited'); +var unvisitedChild = document.getElementById('unvisitedchild'); +var visitedChild = document.getElementById('visitedchild'); + +test(function() { + var keyframes = [ + {backgroundColor: 'currentColor'}, + {backgroundColor: 'rgb(0, 68, 0)'} + ]; + + element.style.color = 'rgb(204, 0, 0)'; + var player = element.animate(keyframes, 10); + player.pause(); + player.currentTime = 5; + element.style.color = 'rgb(0, 0, 204)'; + assert_equals(getComputedStyle(element).backgroundColor, 'rgb(0, 34, 102)'); +}, 'Background color responsive to currentColor changes'); + +test(function() { + var keyframes = [ + {fill: 'currentColor'}, + {fill: 'rgb(0, 68, 0)'} + ]; + + svgElement.style.color = 'rgb(204, 0, 0)'; + var player = svgElement.animate(keyframes, 10); + player.pause(); + player.currentTime = 5; + svgElement.style.color = 'rgb(0, 0, 204)'; + assert_equals(getComputedStyle(svgElement).fill, 'rgb(0, 34, 102)'); +}, 'Fill color responsive to currentColor changes'); + +test(function() { + var keyframes = [ + {backgroundColor: 'rgba(250, 240, 220, 0.431372549)'}, + {backgroundColor: 'currentColor'} + ]; + + element.style.color = 'rgb(204, 0, 0)'; + var player = element.animate(keyframes, 10); + player.pause(); + player.currentTime = 3; + element.style.color = 'rgba(160, 190, 180, 0.980392157)'; + assert_equals(getComputedStyle(element).backgroundColor, 'rgba(206, 215, 200, 0.596)'); +}, 'Color interpolation uses pre-multiplied colors'); + +test(function() { + var keyframes = [ + { + borderBottomColor: 'currentColor', + borderLeftColor: 'currentColor', + borderRightColor: 'currentColor', + borderTopColor: 'currentColor', + offset: 0 + }, + { + borderBottomColor: 'rgb(0, 68, 0)', + borderLeftColor: 'rgb(0, 70, 0)', + borderRightColor: 'rgb(0, 72, 0)', + borderTopColor: 'rgb(0, 74, 0)', + offset: 1 + } + ]; + + element.style.color = 'rgb(204, 0, 0)'; + var player = element.animate(keyframes, 10); + player.pause(); + player.currentTime = 5; + element.style.color = 'rgb(0, 0, 204)'; + assert_equals(getComputedStyle(element).borderBottomColor, 'rgb(0, 34, 102)'); + assert_equals(getComputedStyle(element).borderLeftColor, 'rgb(0, 35, 102)'); + assert_equals(getComputedStyle(element).borderRightColor, 'rgb(0, 36, 102)'); + assert_equals(getComputedStyle(element).borderTopColor, 'rgb(0, 37, 102)'); +}, 'Border color responsive to currentColor changes'); + +test(function() { + var keyframes = [ + { + borderBottomColor: 'currentColor', + borderLeftColor: 'currentColor', + borderRightColor: 'currentColor', + borderTopColor: 'currentColor', + offset: 0 + }, + { + borderBottomColor: 'rgb(0, 68, 0)', + borderLeftColor: 'rgb(0, 70, 0)', + borderRightColor: 'rgb(0, 72, 0)', + borderTopColor: 'rgb(0, 74, 0)', + offset: 1 + } + ]; + + element.style.color = 'rgb(204, 0, 0)'; + var player = element.animate(keyframes, 10); + player.pause(); + player.currentTime = 5; + element.style.color = 'rgb(0, 0, 200)'; + assert_equals(getComputedStyle(element).borderBottomColor, 'rgb(0, 34, 100)'); + assert_equals(getComputedStyle(element).borderLeftColor, 'rgb(0, 35, 100)'); + assert_equals(getComputedStyle(element).borderRightColor, 'rgb(0, 36, 100)'); + assert_equals(getComputedStyle(element).borderTopColor, 'rgb(0, 37, 100)'); +}, 'Border color responsive to currentColor changes again'); + +test(function() { + var keyframes = [ + {outlineColor: 'currentColor'}, + {outlineColor: 'rgb(0, 68, 0)'} + ]; + + element.style.color = 'rgb(204, 0, 0)'; + var player = element.animate(keyframes, 10); + player.pause(); + player.currentTime = 5; + element.style.color = 'rgb(0, 0, 204)'; + assert_equals(getComputedStyle(element).outlineColor, 'rgb(0, 34, 102)'); +}, 'Outline color responsive to currentColor changes'); + +test(function() { + var keyframes = [ + {stroke: 'rgb(0, 68, 0)'}, + {stroke: 'currentColor'} + ]; + + svgElement.style.color = 'rgb(204, 0, 0)'; + var player = svgElement.animate(keyframes, 10); + player.pause(); + player.currentTime = 5; + svgElement.style.color = 'rgb(0, 0, 204)'; + assert_equals(getComputedStyle(svgElement).stroke, 'rgb(0, 34, 102)'); +}, 'Stroke color responsive to currentColor changes'); + +test(function() { + var keyframes = [ + {textDecorationColor: 'rgb(0, 68, 0)'}, + {textDecorationColor: 'currentColor'} + ]; + + element.style.color = 'rgb(204, 0, 0)'; + var player = element.animate(keyframes, 10); + player.pause(); + player.currentTime = 5; + element.style.color = 'rgb(0, 0, 204)'; + assert_equals(getComputedStyle(element).textDecorationColor, 'rgb(0, 34, 102)'); +}, 'Text decoration color responsive to currentColor changes'); + +test(function() { + var keyframes = [ + {color: 'currentColor'}, + {color: 'rgb(0, 68, 0)'} + ]; + + child.style.color = 'rgb(10, 10, 10)'; // Should be ignored + container.style.color = 'rgb(204, 0, 0)'; + var player = child.animate(keyframes, 10); + player.pause(); + player.currentTime = 5; + container.style.color = 'rgb(0, 0, 204)'; + assert_equals(getComputedStyle(child).color, 'rgb(0, 34, 102)'); + player.currentTime = 7.5; + container.style.color = 'rgb(136, 0, 136)'; + assert_equals(getComputedStyle(child).color, 'rgb(34, 51, 34)'); +}, 'Color responsive to parent currentColor changes'); + +test(function() { + var keyframes = [ + {color: 'rgb(0, 68, 0)'}, + {color: 'currentColor'} + ]; + + child.style.color = 'rgb(10, 10, 10)'; // Should be ignored + container.style.color = 'rgb(204, 0, 0)'; + var player = child.animate(keyframes, 10); + player.pause(); + player.currentTime = 5; + container.style.color = 'rgb(0, 0, 204)'; + assert_equals(getComputedStyle(child).color, 'rgb(0, 34, 102)'); + player.currentTime = 7.5; + container.style.color = 'rgb(136, 0, 68)'; + assert_equals(getComputedStyle(child).color, 'rgb(102, 17, 51)'); +}, 'Color responsive to repeated parent currentColor changes'); + +test(function() { + var keyframes = [ + {backgroundColor: 'currentColor'}, + {backgroundColor: 'rgb(100, 150, 200)'} + ]; + + var player1 = unvisited.animate(keyframes, 10); + var player2 = visited.animate(keyframes, 10); + player1.pause(); + player2.pause(); + player1.currentTime = 5; + player2.currentTime = 5; + assert_equals(getComputedStyle(unvisited).backgroundColor, getComputedStyle(visited).backgroundColor); +}, 'Color animations do not expose visited status'); + +test(function() { + var keyframes = [ + {color: 'rgb(100, 150, 200)'}, + {color: 'currentColor'} + ]; + + var player1 = unvisitedChild.animate(keyframes, 10); + var player2 = visitedChild.animate(keyframes, 10); + player1.pause(); + player2.pause(); + player1.currentTime = 5; + player2.currentTime = 5; + assert_equals(getComputedStyle(unvisitedChild).color, getComputedStyle(visitedChild).color); +}, 'Color animations do not expose parent visited status'); + +test(function() { + var keyframes = [ + {columnRuleColor: 'inherit'}, + {columnRuleColor: 'rgb(70, 70, 170)'} + ]; + + container.style.columnRuleColor = 'rgb(170, 70, 70)'; + var player = child.animate(keyframes, 10); + + player.pause(); + player.currentTime = 5; + assert_equals(getComputedStyle(child).columnRuleColor, 'rgb(120, 70, 120)'); + + container.style.columnRuleColor = 'rgb(70, 170, 70)'; + assert_equals(getComputedStyle(child).columnRuleColor, 'rgb(70, 120, 120)'); +}, 'Color animations respond to inherited changes'); +</script> diff --git a/testing/web-platform/tests/web-animations/responsive/to-inherited-change.html b/testing/web-platform/tests/web-animations/responsive/to-inherited-change.html new file mode 100644 index 0000000000..96ef5d24e1 --- /dev/null +++ b/testing/web-platform/tests/web-animations/responsive/to-inherited-change.html @@ -0,0 +1,56 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<div id='container'> + <div id='element'></div> +</div> + +<script> + +var container = document.getElementById('container'); +var element = document.getElementById('element'); + +test(function() { + var keyframes = [ + {fontSize: 'inherit'}, + {fontSize: '20px'} + ]; + + container.style.fontSize = '10px'; + var player = element.animate(keyframes, 10); + player.pause(); + player.currentTime = 5; + container.style.fontSize = '30px'; + assert_equals(getComputedStyle(element).fontSize, '25px'); +}, 'Font size responsive to inherited changes at start'); + +test(function() { + var keyframes = [ + {fontSize: '50px'}, + {fontSize: 'inherit'} + ]; + + container.style.fontSize = '40px'; + var player = element.animate(keyframes, 10); + player.pause(); + player.currentTime = 5; + container.style.fontSize = '60px'; + assert_equals(getComputedStyle(element).fontSize, '55px'); +}, 'Font size responsive to inherited changes at end'); + +test(function() { + var keyframes = [ + {fontSize: 'inherit'}, + {fontSize: 'inherit'} + ]; + + container.style.fontSize = '70px'; + var player = element.animate(keyframes, 10); + player.pause(); + player.currentTime = 5; + container.style.fontSize = '80px'; + assert_equals(getComputedStyle(element).fontSize, '80px'); +}, 'Font size responsive to inherited changes'); + +</script> diff --git a/testing/web-platform/tests/web-animations/responsive/to-style-change.html b/testing/web-platform/tests/web-animations/responsive/to-style-change.html new file mode 100644 index 0000000000..8f4bf4c8b5 --- /dev/null +++ b/testing/web-platform/tests/web-animations/responsive/to-style-change.html @@ -0,0 +1,44 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<div id='container'> +<div id='element'></div> +</div> + +<script> +var element = document.getElementById('element'); +var container = document.getElementById('container'); + +test(function() { + element.style.fontSize = '10px'; + container.style.width = '1000px'; + var player = element.animate([{left: 'calc(100px + 80%)'}, {left: '10em'}], 10); + player.pause(); + player.currentTime = 5; + assert_equals(getComputedStyle(element).left, 'calc(40% + 100px)'); + element.style.fontSize = '20px'; + assert_equals(getComputedStyle(element).left, 'calc(40% + 150px)'); + container.style.width = '500px'; + assert_equals(getComputedStyle(element).left, 'calc(40% + 150px)'); +}, 'Lengths responsive to style changes'); + +test(function() { + container.style.width = '1000px'; + var player = element.animate([{paddingTop: '30%'}, {paddingTop: '50%'}], 10); + player.pause(); + player.currentTime = 5; + container.style.width = '700px'; + assert_equals(getComputedStyle(element).paddingTop, '280px'); +}, 'Percentages responsive to width style changes'); + +test(function() { + element.style.fontSize = '1px'; + var player = element.animate([{lineHeight: '9'}, {lineHeight: '13'}], 10); + player.pause(); + player.currentTime = 2.5; + element.style.fontSize = '7px'; + assert_equals(getComputedStyle(element).lineHeight, '70px'); +}, 'Numbers responsive to style changes'); + +</script> diff --git a/testing/web-platform/tests/web-animations/responsive/toggle-animated-iframe-visibility-ref.html b/testing/web-platform/tests/web-animations/responsive/toggle-animated-iframe-visibility-ref.html new file mode 100644 index 0000000000..dab5bed7c8 --- /dev/null +++ b/testing/web-platform/tests/web-animations/responsive/toggle-animated-iframe-visibility-ref.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<style> + .rotated { + transform: rotate(90deg); + } +</style> +<div id="container"> + <iframe class="rotated" src="resources/block.html"> + </iframe> +</div>
\ No newline at end of file diff --git a/testing/web-platform/tests/web-animations/responsive/toggle-animated-iframe-visibility.html b/testing/web-platform/tests/web-animations/responsive/toggle-animated-iframe-visibility.html new file mode 100644 index 0000000000..f50ffaad34 --- /dev/null +++ b/testing/web-platform/tests/web-animations/responsive/toggle-animated-iframe-visibility.html @@ -0,0 +1,55 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<meta name="assert" content="This should resume the animation after unhiding the iframe."> +<title>CSS Test (Animations): Unhiding iframe visibility should restart animation. </title> +<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=616270"> +<link rel="match" href="toggle-animated-iframe-visibility-ref.html"> +<script src="/common/reftest-wait.js"></script> + +<div id="container"></div> + +<div id="log"></div> + +<script> + var container; + var block; + var logDiv; + + function verifyVisibility(expected_visibility, message) { + if (getComputedStyle(block).visibility !== expected_visibility) + logDiv.innerHTML = `FAIL: ${message}`; + } + + async function runTest() { + var animation = block.animate( + { transform: [ 'rotate(0deg)', 'rotate(180deg)' ] }, + { + duration: 10000000, + delay: -5000000, + easing: 'cubic-bezier(0, 1, 1, 0)' + }); + + await animation.ready; + + container.style.visibility = 'hidden'; + requestAnimationFrame(() => { + verifyVisibility('hidden', 'style.visibility should be hidden'); + container.style.visibility = 'visible'; + + requestAnimationFrame(() => { + verifyVisibility('visible', 'style.visiblity should be visible'); + takeScreenshot(); + }); + }); + } + + window.onload = function () { + logDiv = document.getElementById('log'); + container = document.getElementById('container'); + block = document.createElement('iframe'); + + container.appendChild(block); + block.onload = runTest; + block.src = 'resources/block.html'; + }; +</script> diff --git a/testing/web-platform/tests/web-animations/responsive/transform.html b/testing/web-platform/tests/web-animations/responsive/transform.html new file mode 100644 index 0000000000..d57f8b136a --- /dev/null +++ b/testing/web-platform/tests/web-animations/responsive/transform.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<div id='container'> +<div id='element'></div> +</div> + +<script> +var element = document.getElementById('element'); +var container = document.getElementById('container'); + +test(function() { + container.style.fontSize = '10px'; + var player = element.animate([{transform: 'translateX(10em)'}, {transform: 'translateX(20em)'}], 10); + player.pause(); + player.currentTime = 2; + container.style.fontSize = '20px'; + assert_equals(getComputedStyle(element).transform, 'matrix(1, 0, 0, 1, 240, 0)'); +}, 'Transform responsive to font size changes'); + +</script> diff --git a/testing/web-platform/tests/web-animations/responsive/translate.html b/testing/web-platform/tests/web-animations/responsive/translate.html new file mode 100644 index 0000000000..8df4d8577d --- /dev/null +++ b/testing/web-platform/tests/web-animations/responsive/translate.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<div id='container'> +<div id='element'></div> +</div> + +<script> +var element = document.getElementById('element'); +var container = document.getElementById('container'); + +test(function() { + container.style.fontSize = '10px'; + var player = element.animate([{translate: '1em 2em'}, {translate: '10em 20em'}], 10); + player.pause(); + player.currentTime = 2; + container.style.fontSize = '20px'; + assert_equals(getComputedStyle(element).translate, '56px 112px'); +}, 'Translate responsive to font size changes'); + +</script> diff --git a/testing/web-platform/tests/web-animations/responsive/verticalAlign.html b/testing/web-platform/tests/web-animations/responsive/verticalAlign.html new file mode 100644 index 0000000000..af81fa481b --- /dev/null +++ b/testing/web-platform/tests/web-animations/responsive/verticalAlign.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<div id='container'> +<div id='element'></div> +</div> + +<script> +var element = document.getElementById('element'); +var container = document.getElementById('container'); + +test(function() { + element.style.fontSize = '10px'; + var player = element.animate([{verticalAlign: '3em'}, {verticalAlign: '5em'}], 10); + player.pause(); + player.currentTime = 5; + element.style.fontSize = '20px'; + assert_equals(getComputedStyle(element).verticalAlign, '80px'); +}, 'verticalAlign responsive to style changes'); + +</script> diff --git a/testing/web-platform/tests/web-animations/testcommon.js b/testing/web-platform/tests/web-animations/testcommon.js new file mode 100644 index 0000000000..0b318714a5 --- /dev/null +++ b/testing/web-platform/tests/web-animations/testcommon.js @@ -0,0 +1,323 @@ +'use strict'; + +const MS_PER_SEC = 1000; + +// The recommended minimum precision to use for time values[1]. +// +// [1] https://drafts.csswg.org/web-animations/#precision-of-time-values +const TIME_PRECISION = 0.0005; // ms + +// Allow implementations to substitute an alternative method for comparing +// times based on their precision requirements. +if (!window.assert_times_equal) { + window.assert_times_equal = (actual, expected, description) => { + assert_approx_equals(actual, expected, TIME_PRECISION * 2, description); + }; +} + +// Allow implementations to substitute an alternative method for comparing +// times based on their precision requirements. +if (!window.assert_time_greater_than_equal) { + window.assert_time_greater_than_equal = (actual, expected, description) => { + assert_greater_than_equal(actual, expected - 2 * TIME_PRECISION, + description); + }; +} + +// Allow implementations to substitute an alternative method for comparing +// a time value based on its precision requirements with a fixed value. +if (!window.assert_time_equals_literal) { + window.assert_time_equals_literal = (actual, expected, description) => { + if (Math.abs(expected) === Infinity) { + assert_equals(actual, expected, description); + } else { + assert_approx_equals(actual, expected, TIME_PRECISION, description); + } + } +} + +// creates div element, appends it to the document body and +// removes the created element during test cleanup +function createDiv(test, doc) { + return createElement(test, 'div', doc); +} + +// creates element of given tagName, appends it to the document body and +// removes the created element during test cleanup +// if tagName is null or undefined, returns div element +function createElement(test, tagName, doc) { + if (!doc) { + doc = document; + } + const element = doc.createElement(tagName || 'div'); + doc.body.appendChild(element); + test.add_cleanup(() => { + element.remove(); + }); + return element; +} + +// Creates a style element with the specified rules, appends it to the document +// head and removes the created element during test cleanup. +// |rules| is an object. For example: +// { '@keyframes anim': '' , +// '.className': 'animation: anim 100s;' }; +// or +// { '.className1::before': 'content: ""; width: 0px; transition: all 10s;', +// '.className2::before': 'width: 100px;' }; +// The object property name could be a keyframes name, or a selector. +// The object property value is declarations which are property:value pairs +// split by a space. +function createStyle(test, rules, doc) { + if (!doc) { + doc = document; + } + const extraStyle = doc.createElement('style'); + doc.head.appendChild(extraStyle); + if (rules) { + const sheet = extraStyle.sheet; + for (const selector in rules) { + sheet.insertRule(`${selector}{${rules[selector]}}`, + sheet.cssRules.length); + } + } + test.add_cleanup(() => { + extraStyle.remove(); + }); +} + +// Cubic bezier with control points (0, 0), (x1, y1), (x2, y2), and (1, 1). +function cubicBezier(x1, y1, x2, y2) { + const xForT = t => { + const omt = 1-t; + return 3 * omt * omt * t * x1 + 3 * omt * t * t * x2 + t * t * t; + }; + + const yForT = t => { + const omt = 1-t; + return 3 * omt * omt * t * y1 + 3 * omt * t * t * y2 + t * t * t; + }; + + const tForX = x => { + // Binary subdivision. + let mint = 0, maxt = 1; + for (let i = 0; i < 30; ++i) { + const guesst = (mint + maxt) / 2; + const guessx = xForT(guesst); + if (x < guessx) { + maxt = guesst; + } else { + mint = guesst; + } + } + return (mint + maxt) / 2; + }; + + return x => { + if (x == 0) { + return 0; + } + if (x == 1) { + return 1; + } + return yForT(tForX(x)); + }; +} + +function stepEnd(nsteps) { + return x => Math.floor(x * nsteps) / nsteps; +} + +function stepStart(nsteps) { + return x => { + const result = Math.floor(x * nsteps + 1.0) / nsteps; + return (result > 1.0) ? 1.0 : result; + }; +} + +function waitForAnimationFrames(frameCount) { + return new Promise(resolve => { + function handleFrame() { + if (--frameCount <= 0) { + resolve(); + } else { + window.requestAnimationFrame(handleFrame); // wait another frame + } + } + window.requestAnimationFrame(handleFrame); + }); +} + +// Continually calls requestAnimationFrame until |minDelay| has elapsed +// as recorded using document.timeline.currentTime (i.e. frame time not +// wall-clock time). +function waitForAnimationFramesWithDelay(minDelay) { + const startTime = document.timeline.currentTime; + return new Promise(resolve => { + (function handleFrame() { + if (document.timeline.currentTime - startTime >= minDelay) { + resolve(); + } else { + window.requestAnimationFrame(handleFrame); + } + }()); + }); +} + + +// Waits for a requestAnimationFrame callback in the next refresh driver tick. +function waitForNextFrame() { + const timeAtStart = document.timeline.currentTime; + return new Promise(resolve => { + (function handleFrame() { + if (timeAtStart === document.timeline.currentTime) { + window.requestAnimationFrame(handleFrame); + } else { + resolve(); + } + }()); + }); +} + +async function insertFrameAndAwaitLoad(test, iframe, doc) { + const eventWatcher = new EventWatcher(test, iframe, ['load']); + const event_promise = eventWatcher.wait_for('load'); + + doc.body.appendChild(iframe); + test.add_cleanup(() => { doc.body.removeChild(iframe); }); + + await event_promise; +} + +// Returns 'matrix()' or 'matrix3d()' function string generated from an array. +function createMatrixFromArray(array) { + return (array.length == 16 ? 'matrix3d' : 'matrix') + `(${array.join()})`; +} + +// Returns 'matrix3d()' function string equivalent to +// 'rotate3d(x, y, z, radian)'. +function rotate3dToMatrix3d(x, y, z, radian) { + return createMatrixFromArray(rotate3dToMatrix(x, y, z, radian)); +} + +// Returns an array of the 4x4 matrix equivalent to 'rotate3d(x, y, z, radian)'. +// https://drafts.csswg.org/css-transforms-2/#Rotate3dDefined +function rotate3dToMatrix(x, y, z, radian) { + const sc = Math.sin(radian / 2) * Math.cos(radian / 2); + const sq = Math.sin(radian / 2) * Math.sin(radian / 2); + + // Normalize the vector. + const length = Math.sqrt(x*x + y*y + z*z); + x /= length; + y /= length; + z /= length; + + return [ + 1 - 2 * (y*y + z*z) * sq, + 2 * (x * y * sq + z * sc), + 2 * (x * z * sq - y * sc), + 0, + 2 * (x * y * sq - z * sc), + 1 - 2 * (x*x + z*z) * sq, + 2 * (y * z * sq + x * sc), + 0, + 2 * (x * z * sq + y * sc), + 2 * (y * z * sq - x * sc), + 1 - 2 * (x*x + y*y) * sq, + 0, + 0, + 0, + 0, + 1 + ]; +} + +// Compare matrix string like 'matrix(1, 0, 0, 1, 100, 0)' with tolerances. +function assert_matrix_equals(actual, expected, description) { + const matrixRegExp = /^matrix(?:3d)*\((.+)\)/; + assert_regexp_match(actual, matrixRegExp, + 'Actual value is not a matrix') + assert_regexp_match(expected, matrixRegExp, + 'Expected value is not a matrix'); + + const actualMatrixArray = + actual.match(matrixRegExp)[1].split(',').map(Number); + const expectedMatrixArray = + expected.match(matrixRegExp)[1].split(',').map(Number); + + assert_equals(actualMatrixArray.length, expectedMatrixArray.length, + `dimension of the matrix: ${description}`); + for (let i = 0; i < actualMatrixArray.length; i++) { + assert_approx_equals(actualMatrixArray[i], expectedMatrixArray[i], 0.0001, + `expected ${expected} but got ${actual}: ${description}`); + } +} + +// Compare rotate3d vector like '0 1 0 45deg' with tolerances. +function assert_rotate3d_equals(actual, expected, description) { + const rotationRegExp =/^((([+-]?\d+(\.+\d+)?\s){3})?\d+(\.+\d+)?)deg/; + + assert_regexp_match(actual, rotationRegExp, + 'Actual value is not a rotate3d vector') + assert_regexp_match(expected, rotationRegExp, + 'Expected value is not a rotate3d vector'); + + const actualRotationVector = + actual.match(rotationRegExp)[1].split(' ').map(Number); + const expectedRotationVector = + expected.match(rotationRegExp)[1].split(' ').map(Number); + + assert_equals(actualRotationVector.length, expectedRotationVector.length, + `dimension of the matrix: ${description}`); + for (let i = 0; i < actualRotationVector.length; i++) { + assert_approx_equals( + actualRotationVector[i], + expectedRotationVector[i], + 0.0001, + `expected ${expected} but got ${actual}: ${description}`); + } +} + +function assert_phase_at_time(animation, phase, currentTime) { + animation.currentTime = currentTime; + const fillMode = animation.effect.getTiming().fill; + + if (phase === 'active') { + // If the fill mode is 'none', then progress will only be non-null if we + // are in the active phase. + animation.effect.updateTiming({ fill: 'none' }); + assert_not_equals(animation.effect.getComputedTiming().progress, null, + 'Animation effect is in active phase when current time ' + + `is ${currentTime}.`); + } else { + // The easiest way to distinguish between the 'before' phase and the 'after' + // phase is to toggle the fill mode. For example, if the progress is null + // when the fill mode is 'none' but non-null when the fill mode is + // 'backwards' then we are in the before phase. + animation.effect.updateTiming({ fill: 'none' }); + assert_equals(animation.effect.getComputedTiming().progress, null, + `Animation effect is in ${phase} phase when current time ` + + `is ${currentTime} (progress is null with 'none' fill mode)`); + + animation.effect.updateTiming({ + fill: phase === 'before' ? 'backwards' : 'forwards', + }); + assert_not_equals(animation.effect.getComputedTiming().progress, null, + `Animation effect is in ${phase} phase when current ` + + `time is ${currentTime} (progress is non-null with ` + + `appropriate fill mode)`); + } + + // Reset fill mode to avoid side-effects. + animation.effect.updateTiming({ fill: fillMode }); +} + + +// Use with reftest-wait to wait until compositor commits are no longer deferred +// before taking the screenshot. +// crbug.com/1378671 +async function waitForCompositorReady(target) { + const animation = + document.body.animate({ opacity: [ 1, 1 ] }, {duration: 1 }); + return animation.finished; +} 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/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..d570eed5c2 --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/timelines/timelines.html @@ -0,0 +1,112 @@ +<!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'); + +async_test(t => { + const div = createDiv(t); + const animation = div.animate(null, 100 * MS_PER_SEC); + + animation.ready.then(t.step_func(() => { + const readyTimelineTime = document.timeline.currentTime; + requestAnimationFrame(t.step_func_done(() => { + assert_equals(readyTimelineTime, document.timeline.currentTime, + 'There should be a microtask checkpoint'); + })); + })); +}, 'Performs a microtask checkpoint after updating timelins'); + +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> |