diff options
Diffstat (limited to 'testing/web-platform/tests/web-animations/interfaces')
39 files changed, 5944 insertions, 0 deletions
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> |