diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /testing/web-platform/tests/web-animations/interfaces/Animation | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/web-animations/interfaces/Animation')
18 files changed, 2288 insertions, 0 deletions
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> |