diff options
Diffstat (limited to 'dom/animation/test/mozilla')
40 files changed, 5434 insertions, 0 deletions
diff --git a/dom/animation/test/mozilla/empty.html b/dom/animation/test/mozilla/empty.html new file mode 100644 index 0000000000..739422cbfa --- /dev/null +++ b/dom/animation/test/mozilla/empty.html @@ -0,0 +1,2 @@ +<!DOCTYPE html> +<script src="../testcommon.js"></script> diff --git a/dom/animation/test/mozilla/file_deferred_start.html b/dom/animation/test/mozilla/file_deferred_start.html new file mode 100644 index 0000000000..863fc80fec --- /dev/null +++ b/dom/animation/test/mozilla/file_deferred_start.html @@ -0,0 +1,179 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<script src="/tests/SimpleTest/paint_listener.js"></script> +<style> +@keyframes empty { } +.target { + /* Element needs geometry to be eligible for layerization */ + width: 100px; + height: 100px; + background-color: white; +} +</style> +<body> +<script> +'use strict'; + +function waitForDocLoad() { + return new Promise((resolve, reject) => { + if (document.readyState === 'complete') { + resolve(); + } else { + window.addEventListener('load', resolve); + } + }); +} + +function waitForPaints() { + return new Promise((resolve, reject) => { + waitForAllPaintsFlushed(resolve); + }); +} + +promise_test(async t => { + // Test that empty animations actually start. + // + // Normally we tie the start of animations to when their first frame of + // the animation is rendered. However, for animations that don't actually + // trigger a paint (e.g. because they are empty, or are animating something + // that doesn't render or is offscreen) we want to make sure they still + // start. + // + // Before we start, wait for the document to finish loading, then create + // div element, and wait for painting. This is because during loading we will + // have other paint events taking place which might, by luck, happen to + // trigger animations that otherwise would not have been triggered, leading to + // false positives. + // + // As a result, it's better to wait until we have a more stable state before + // continuing. + await waitForDocLoad(); + + const div = addDiv(t); + + await waitForPaints(); + + div.style.animation = 'empty 1000s'; + const animation = div.getAnimations()[0]; + + let promiseCallbackDone = false; + animation.ready.then(() => { + promiseCallbackDone = true; + }).catch(() => { + assert_unreached('ready promise was rejected'); + }); + + // We need to wait for up to three frames. This is because in some + // cases it can take up to two frames for the initial layout + // to take place. Even after that happens we don't actually resolve the + // ready promise until the following tick. + await waitForAnimationFrames(3); + + assert_true(promiseCallbackDone, + 'ready promise for an empty animation was resolved' + + ' within three animation frames'); +}, 'Animation.ready is resolved for an empty animation'); + +// Test that compositor animations with delays get synced correctly +// +// NOTE: It is important that we DON'T use +// SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh here since that takes +// us through a different code path. +promise_test(async t => { + assert_false(SpecialPowers.DOMWindowUtils.isTestControllingRefreshes, + 'Test should run without the refresh driver being under' + + ' test control'); + + // This test only applies to compositor animations + if (!isOMTAEnabled()) { + return; + } + + const div = addDiv(t, { class: 'target' }); + + // As with the above test, any stray paints can cause this test to produce + // a false negative (that is, pass when it should fail). To avoid this we + // wait for paints and only then do we commence the test. + await waitForPaints(); + + const animation = + div.animate({ transform: [ 'translate(0px)', 'translate(100px)' ] }, + { duration: 400 * MS_PER_SEC, + delay: -200 * MS_PER_SEC }); + + await waitForAnimationReadyToRestyle(animation); + + await waitForPaints(); + + const transformStr = + SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform'); + const translateX = getTranslateXFromTransform(transformStr); + + // If the delay has been applied we should be about half-way through + // the animation. However, if we applied it twice we will be at the + // end of the animation already so check that we are roughly half way + // through. + assert_between_inclusive(translateX, 40, 75, + 'Animation is about half-way through on the compositor'); +}, 'Starting an animation with a delay starts from the correct point'); + +// Test that compositor animations with a playback rate start at the +// appropriate point. +// +// NOTE: As with the previous test, it is important that we DON'T use +// SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh here since that takes +// us through a different code path. +promise_test(async t => { + assert_false(SpecialPowers.DOMWindowUtils.isTestControllingRefreshes, + 'Test should run without the refresh driver being under' + + ' test control'); + + // This test only applies to compositor animations + if (!isOMTAEnabled()) { + return; + } + + const div = addDiv(t, { class: 'target' }); + + // Wait for the document to load and painting (see notes in previous test). + await waitForPaints(); + + const animation = + div.animate({ transform: [ 'translate(0px)', 'translate(100px)' ] }, + 200 * MS_PER_SEC); + animation.currentTime = 100 * MS_PER_SEC; + animation.playbackRate = 0.1; + + await waitForPaints(); + + const transformStr = + SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform'); + const translateX = getTranslateXFromTransform(transformStr); + + // We pass the playback rate to the compositor independently and we have + // tests to ensure that it is correctly applied there. However, if, when + // we resolve the start time of the pending animation, we fail to + // incorporate the playback rate, we will end up starting from the wrong + // point and the current time calculated on the compositor will be wrong. + assert_between_inclusive(translateX, 25, 75, + 'Animation is about half-way through on the compositor'); +}, 'Starting an animation with a playbackRate starts from the correct point'); + +function getTranslateXFromTransform(transformStr) { + const matrixComponents = + transformStr.startsWith('matrix(') + ? transformStr.substring('matrix('.length, transformStr.length-1) + .split(',') + .map(component => Number(component)) + : []; + assert_equals(matrixComponents.length, 6, + 'Got a valid transform matrix on the compositor' + + ' (got: "' + transformStr + '")'); + + return matrixComponents[4]; +} + +done(); +</script> +</body> diff --git a/dom/animation/test/mozilla/file_disable_animations_api_autoremove.html b/dom/animation/test/mozilla/file_disable_animations_api_autoremove.html new file mode 100644 index 0000000000..79cb508467 --- /dev/null +++ b/dom/animation/test/mozilla/file_disable_animations_api_autoremove.html @@ -0,0 +1,69 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<body> +<script> +'use strict'; + +promise_test(async t => { + const div = addDiv(t); + + const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' }); + + // This should be assert_not_own_property but our local copy of testharness.js + // is old. + assert_equals( + animA.replaceState, + undefined, + 'Should not have a replaceState member' + ); + + animA.addEventListener( + 'remove', + t.step_func(() => { + assert_unreached('Should not fire a remove event'); + }) + ); + + // Allow a chance for the remove event to be fired + + await animA.finished; + await waitForNextFrame(); +}, 'Remove events should not be fired if the pref is not set'); + +promise_test(async t => { + const div = addDiv(t); + div.style.opacity = '0.1'; + + const animA = div.animate( + { opacity: 0.2 }, + { duration: 1, fill: 'forwards' } + ); + const animB = div.animate( + { opacity: 0.3, composite: 'add' }, + { duration: 1, fill: 'forwards' } + ); + + await animA.finished; + + assert_approx_equals( + parseFloat(getComputedStyle(div).opacity), + 0.5, + 0.0001, + 'Covered animation should still contribute to effect stack when adding' + ); + + animB.cancel(); + + assert_approx_equals( + parseFloat(getComputedStyle(div).opacity), + 0.2, + 0.0001, + 'Covered animation should still contribute to animated style when replacing' + ); +}, 'Covered animations should still affect style if the pref is not set'); + +done(); +</script> +</body> diff --git a/dom/animation/test/mozilla/file_disable_animations_api_compositing.html b/dom/animation/test/mozilla/file_disable_animations_api_compositing.html new file mode 100644 index 0000000000..6d9ba35dc0 --- /dev/null +++ b/dom/animation/test/mozilla/file_disable_animations_api_compositing.html @@ -0,0 +1,137 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<body> +<script> +'use strict'; + +test(t => { + const anim = addDiv(t).animate( + { marginLeft: ['0px', '10px'] }, + { + duration: 100 * MS_PER_SEC, + iterations: 10, + iterationComposite: 'accumulate', + composite: 'add', + } + ); + assert_false( + 'iterationComposite' in anim.effect, + 'The KeyframeEffect.iterationComposite member is not present' + ); + assert_false( + 'composite' in anim.effect, + 'The KeyframeEffect.composite member is not present' + ); +}, 'The iterationComposite and composite members are not present on Animation' + + ' when the compositing pref is disabled'); + +test(t => { + const div = addDiv(t); + const anim = div.animate( + { marginLeft: ['0px', '10px'] }, + { + duration: 100 * MS_PER_SEC, + iterations: 10, + iterationComposite: 'accumulate', + } + ); + anim.pause(); + anim.currentTime = 200 * MS_PER_SEC; + + assert_equals( + getComputedStyle(div).marginLeft, + '0px', + 'Animated style should NOT accumulate' + ); +}, 'KeyframeEffectOptions.iterationComposite should be ignored if the' + + ' compositing pref is disabled'); + +test(t => { + const div = addDiv(t); + const anim1 = div.animate( + { marginLeft: ['0px', '100px'] }, + { duration: 100 * MS_PER_SEC } + ); + anim1.pause(); + anim1.currentTime = 50 * MS_PER_SEC; + + const anim2 = div.animate( + { marginLeft: ['0px', '100px'] }, + { duration: 100 * MS_PER_SEC, composite: 'add' } + ); + anim2.pause(); + anim2.currentTime = 50 * MS_PER_SEC; + + assert_equals( + getComputedStyle(div).marginLeft, + '50px', + 'Animations should NOT add together' + ); +}, 'KeyframeEffectOptions.composite should be ignored if the' + + ' compositing pref is disabled'); + +test(t => { + const div = addDiv(t); + const anim1 = div.animate({ marginLeft: ['0px', '100px'] }, 100 * MS_PER_SEC); + anim1.pause(); + anim1.currentTime = 50 * MS_PER_SEC; + + const anim2 = div.animate( + [ + { marginLeft: '0px', composite: 'add' }, + { marginLeft: '100px', composite: 'add' }, + ], + 100 * MS_PER_SEC + ); + anim2.pause(); + anim2.currentTime = 50 * MS_PER_SEC; + + assert_equals( + getComputedStyle(div).marginLeft, + '50px', + 'Animations should NOT add together' + ); +}, 'composite member is ignored on keyframes when using array notation'); + +test(t => { + const div = addDiv(t); + const anim1 = div.animate( + { marginLeft: ['0px', '100px'] }, + 100 * MS_PER_SEC + ); + anim1.pause(); + anim1.currentTime = 50 * MS_PER_SEC; + + const anim2 = div.animate( + { marginLeft: ['0px', '100px'], composite: ['add', 'add'] }, + 100 * MS_PER_SEC + ); + anim2.pause(); + anim2.currentTime = 50 * MS_PER_SEC; + + assert_equals( + getComputedStyle(div).marginLeft, + '50px', + 'Animations should NOT add together' + ); +}, 'composite member is ignored on keyframes when using object notation'); + +test(t => { + const anim = addDiv(t).animate( + { marginLeft: ['0px', '10px'] }, + 100 * MS_PER_SEC + ); + + for (let frame of anim.effect.getKeyframes()) { + assert_false( + 'composite' in frame, + 'The BaseComputedKeyframe.composite member is not present' + ); + } +}, 'composite member is hidden from the result of ' + + 'KeyframeEffect::getKeyframes()'); + +done(); +</script> +</body> diff --git a/dom/animation/test/mozilla/file_disable_animations_api_get_animations.html b/dom/animation/test/mozilla/file_disable_animations_api_get_animations.html new file mode 100644 index 0000000000..3d484444a7 --- /dev/null +++ b/dom/animation/test/mozilla/file_disable_animations_api_get_animations.html @@ -0,0 +1,20 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<body> +<script> +'use strict'; + +test(t => { + assert_false('getAnimations' in addDiv(t)); +}, 'Element.getAnimations() is not available when getAnimations pref is' + + ' disabled'); + +test(t => { + assert_false('getAnimations' in document); +}, 'Document.getAnimations() is not available when getAnimations pref is' + + ' disabled'); + +done(); +</script> +</body> diff --git a/dom/animation/test/mozilla/file_disable_animations_api_implicit_keyframes.html b/dom/animation/test/mozilla/file_disable_animations_api_implicit_keyframes.html new file mode 100644 index 0000000000..9cd05e7d40 --- /dev/null +++ b/dom/animation/test/mozilla/file_disable_animations_api_implicit_keyframes.html @@ -0,0 +1,48 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<body> +<script> +'use strict'; + +// Tests for cases we should throw an exception for if implicit keyframes are +// disabled. +var gTests = [ + { desc: "single Keyframe value", + keyframes: { left: "100px" } }, + { desc: "single Keyframe with no offset", + keyframes: [{ left: "100px" }] }, + { desc: "single Keyframe with 0% offset", + keyframes: [{ left: "100px", offset: 0 }] }, + { desc: "single Keyframe with 100% offset", + keyframes: [{ left: "100px", offset: 1 }] }, + { desc: "multiple Keyframes with missing 0% Keyframe", + keyframes: [{ left: "100px", offset: 0.25 }, + { left: "200px", offset: 0.50 }, + { left: "300px", offset: 1.00 }] }, + { desc: "multiple Keyframes with missing 100% Keyframe", + keyframes: [{ left: "100px", offset: 0.00 }, + { left: "200px", offset: 0.50 }, + { left: "300px", offset: 0.75 }] }, + { desc: "multiple Keyframes with missing properties on first Keyframe", + keyframes: [{ left: "100px", offset: 0.0 }, + { left: "200px", top: "200px", offset: 0.5 }, + { left: "300px", top: "300px", offset: 1.0 }] }, + { desc: "multiple Keyframes with missing properties on last Keyframe", + keyframes: [{ left: "100px", top: "200px", offset: 0.0 }, + { left: "200px", top: "200px", offset: 0.5 }, + { left: "300px", offset: 1.0 }] }, +]; + +gTests.forEach(function(subtest) { + test(function(t) { + var div = addDiv(t); + assert_throws("NotSupportedError", function() { + div.animate(subtest.keyframes, 100 * MS_PER_SEC); + }); + }, "Element.animate() throws with " + subtest.desc); +}); + +done(); +</script> +</body> diff --git a/dom/animation/test/mozilla/file_disable_animations_api_timelines.html b/dom/animation/test/mozilla/file_disable_animations_api_timelines.html new file mode 100644 index 0000000000..39fedb299a --- /dev/null +++ b/dom/animation/test/mozilla/file_disable_animations_api_timelines.html @@ -0,0 +1,30 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<body> +<script> +'use strict'; + +test(t => { + assert_false( + window.hasOwnProperty('DocumentTimeline'), + 'DocumentTimeline should not be exposed on the global' + ); + assert_false( + window.hasOwnProperty('AnimationTimeline'), + 'AnimationTimeline should not be exposed on the global' + ); + assert_false( + 'timeline' in document, + 'document should not have a timeline property' + ); + + const anim = addDiv(t).animate(null); + assert_false( + 'timeline' in anim, + 'Animation should not have a timeline property' + ); +}, 'Timeline-related interfaces and members are disabled'); + +done(); +</script> diff --git a/dom/animation/test/mozilla/file_discrete_animations.html b/dom/animation/test/mozilla/file_discrete_animations.html new file mode 100644 index 0000000000..e0de609bc5 --- /dev/null +++ b/dom/animation/test/mozilla/file_discrete_animations.html @@ -0,0 +1,122 @@ +<!doctype html> +<head> +<meta charset=utf-8> +<title>Test Mozilla-specific discrete animatable properties</title> +<script type="application/javascript" src="../testcommon.js"></script> +</head> +<body> +<script> +"use strict"; + +const gMozillaSpecificProperties = { + "-moz-box-align": { + // https://developer.mozilla.org/en/docs/Web/CSS/box-align + from: "center", + to: "stretch" + }, + "-moz-box-direction": { + // https://developer.mozilla.org/en/docs/Web/CSS/box-direction + from: "reverse", + to: "normal" + }, + "-moz-box-ordinal-group": { + // https://developer.mozilla.org/en/docs/Web/CSS/box-ordinal-group + from: "1", + to: "5" + }, + "-moz-box-orient": { + // https://www.w3.org/TR/css-flexbox-1/ + from: "horizontal", + to: "vertical" + }, + "-moz-box-pack": { + // https://www.w3.org/TR/2009/WD-css3-flexbox-20090723/#propdef-box-pack + from: "center", + to: "end" + }, + "-moz-float-edge": { + // https://developer.mozilla.org/en/docs/Web/CSS/-moz-float-edge + from: "margin-box", + to: "content-box" + }, + "-moz-force-broken-image-icon": { + // https://developer.mozilla.org/en/docs/Web/CSS/-moz-force-broken-image-icon + from: "1", + to: "0" + }, + "-moz-text-size-adjust": { + // https://drafts.csswg.org/css-size-adjust/#propdef-text-size-adjust + from: "none", + to: "auto" + }, + "-webkit-text-stroke-width": { + // https://compat.spec.whatwg.org/#propdef--webkit-text-stroke-width + from: "10px", + to: "50px" + } +} + +for (let property in gMozillaSpecificProperties) { + const testData = gMozillaSpecificProperties[property]; + const from = testData.from; + const to = testData.to; + const idlName = propertyToIDL(property); + const keyframes = {}; + keyframes[idlName] = [from, to]; + + test(t => { + const div = addDiv(t); + const animation = div.animate(keyframes, + { duration: 1000, fill: "both" }); + testAnimationSamples(animation, idlName, + [{ time: 0, expected: from.toLowerCase() }, + { time: 499, expected: from.toLowerCase() }, + { time: 500, expected: to.toLowerCase() }, + { time: 1000, expected: to.toLowerCase() }]); + }, property + " should animate between '" + + from + "' and '" + to + "' with linear easing"); + + test(function(t) { + // Easing: http://cubic-bezier.com/#.68,0,1,.01 + // With this curve, we don't reach the 50% point until about 95% of + // the time has expired. + const div = addDiv(t); + const animation = div.animate(keyframes, + { duration: 1000, fill: "both", + easing: "cubic-bezier(0.68,0,1,0.01)" }); + testAnimationSamples(animation, idlName, + [{ time: 0, expected: from.toLowerCase() }, + { time: 940, expected: from.toLowerCase() }, + { time: 960, expected: to.toLowerCase() }]); + }, property + " should animate between '" + + from + "' and '" + to + "' with effect easing"); + + test(function(t) { + // Easing: http://cubic-bezier.com/#.68,0,1,.01 + // With this curve, we don't reach the 50% point until about 95% of + // the time has expired. + keyframes.easing = "cubic-bezier(0.68,0,1,0.01)"; + const div = addDiv(t); + const animation = div.animate(keyframes, + { duration: 1000, fill: "both" }); + testAnimationSamples(animation, idlName, + [{ time: 0, expected: from.toLowerCase() }, + { time: 940, expected: from.toLowerCase() }, + { time: 960, expected: to.toLowerCase() }]); + }, property + " should animate between '" + + from + "' and '" + to + "' with keyframe easing"); +} + +function testAnimationSamples(animation, idlName, testSamples) { + const target = animation.effect.target; + testSamples.forEach(testSample => { + animation.currentTime = testSample.time; + assert_equals(getComputedStyle(target)[idlName], testSample.expected, + "The value should be " + testSample.expected + + " at " + testSample.time + "ms"); + }); +} + +done(); +</script> +</body> diff --git a/dom/animation/test/mozilla/file_restyles.html b/dom/animation/test/mozilla/file_restyles.html new file mode 100644 index 0000000000..3a19c2f4ac --- /dev/null +++ b/dom/animation/test/mozilla/file_restyles.html @@ -0,0 +1,2252 @@ +<!doctype html> +<head> +<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1"> +<meta charset=utf-8> +<title>Tests restyles caused by animations</title> +<script> +const ok = opener.ok.bind(opener); +const is = opener.is.bind(opener); +const todo = opener.todo.bind(opener); +const todo_is = opener.todo_is.bind(opener); +const info = opener.info.bind(opener); +const original_finish = opener.SimpleTest.finish; +const SimpleTest = opener.SimpleTest; +const add_task = opener.add_task; +SimpleTest.finish = function finish() { + self.close(); + original_finish(); +} +</script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<script src="/tests/SimpleTest/paint_listener.js"></script> +<script src="../testcommon.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> +<style> +@keyframes background-position { + 0% { + background-position: -25px center; + } + + 40%, + 100% { + background-position: 36px center; + } +} +@keyframes opacity { + from { opacity: 1; } + to { opacity: 0; } +} +@keyframes opacity-without-end-value { + from { opacity: 0; } +} +@keyframes on-main-thread { + from { z-index: 0; } + to { z-index: 999; } +} +@keyframes rotate { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} +@keyframes move-in { + from { transform: translate(120%, 120%); } + to { transform: translate(0%, 0%); } +} +@keyframes background-color { + from { background-color: rgb(255, 0, 0,); } + to { background-color: rgb(0, 255, 0,); } +} +div { + /* Element needs geometry to be eligible for layerization */ + width: 100px; + height: 100px; + background-color: white; +} +progress:not(.stop)::-moz-progress-bar { + animation: on-main-thread 100s; +} +body { + /* + * set overflow:hidden to avoid accidentally unthrottling animations to update + * the overflow region. + */ + overflow: hidden; +} +</style> +</head> +<body> +<script> +'use strict'; + +// Returns observed animation restyle markers when |funcToMakeRestyleHappen| +// is called. +// NOTE: This function is synchronous version of the above observeStyling(). +// Unlike the above observeStyling, this function takes a callback function, +// |funcToMakeRestyleHappen|, which may be expected to trigger a synchronous +// restyles, and returns any restyle markers produced by calling that function. +function observeAnimSyncStyling(funcToMakeRestyleHappen) { + const docShell = getDocShellForObservingRestylesForWindow(window); + + funcToMakeRestyleHappen(); + + const markers = docShell.popProfileTimelineMarkers(); + docShell.recordProfileTimelineMarkers = false; + return Array.prototype.filter.call(markers, (marker, index) => { + return marker.name == 'Styles' && marker.isAnimationOnly; + }); +} + +function ensureElementRemoval(aElement) { + return new Promise(resolve => { + aElement.remove(); + waitForAllPaintsFlushed(resolve); + }); +} + +function waitForWheelEvent(aTarget) { + return new Promise(resolve => { + // Get the scrollable target element position in this window coordinate + // system to send a wheel event to the element. + const targetRect = aTarget.getBoundingClientRect(); + const centerX = targetRect.left + targetRect.width / 2; + const centerY = targetRect.top + targetRect.height / 2; + + sendWheelAndPaintNoFlush(aTarget, centerX, centerY, + { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaY: targetRect.height }, + resolve); + }); +} + +const omtaEnabled = isOMTAEnabled(); + +function add_task_if_omta_enabled(test) { + if (!omtaEnabled) { + info(test.name + " is skipped because OMTA is disabled"); + return; + } + add_task(test); +} + +// We need to wait for all paints before running tests to avoid contaminations +// from styling of this document itself. +waitForAllPaints(() => { + add_task(async function () { + // Start vsync rate measurement in after a RAF callback. + await waitForNextFrame(); + + const timeAtStart = document.timeline.currentTime; + await waitForAnimationFrames(5); + const vsyncRate = (document.timeline.currentTime - timeAtStart) / 5; + + // In this test we basically observe restyling counts in 5 frames, if it + // takes over 200ms during the 5 frames, this test will fail. So + // "200ms / 5 = 40ms" is a threshold whether the test works as expected or + // not. We'd take 5ms additional tolerance here. + // Note that the 200ms is a period we unthrottle throttled animations that + // at least one of the animating styles produces change hints causing + // overflow, the value is defined in + // KeyframeEffect::OverflowRegionRefreshInterval. + if (vsyncRate > 40 - 5) { + ok(true, `the vsync rate ${vsyncRate} on this machine is too slow to run this test`); + SimpleTest.finish(); + } + }); + + add_task(async function restyling_for_main_thread_animations() { + const div = addDiv(null, { style: 'animation: on-main-thread 100s' }); + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + + ok(!SpecialPowers.wrap(animation).isRunningOnCompositor); + + const markers = await observeStyling(5); + is(markers.length, 5, + 'CSS animations running on the main-thread should update style ' + + 'on the main thread'); + await ensureElementRemoval(div); + }); + + add_task(async function restyling_for_main_thread_animations_progress_bar_pseudo() { + const progress = document.createElement("progress"); + document.body.appendChild(progress); + + await waitForNextFrame(); + await waitForNextFrame(); + + let markers = await observeStyling(5); + is(markers.length, 5, + 'CSS animations running on the main-thread should update style ' + + 'on the main thread on ::-moz-progress-bar'); + progress.classList.add("stop"); + await waitForNextFrame(); + await waitForNextFrame(); + + markers = await observeStyling(5); + is(markers.length, 0, 'Animation is correctly removed'); + await ensureElementRemoval(progress); + }); + + add_task_if_omta_enabled(async function no_restyling_for_compositor_animations() { + const div = addDiv(null, { style: 'animation: opacity 100s' }); + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + ok(SpecialPowers.wrap(animation).isRunningOnCompositor); + + const markers = await observeStyling(5); + is(markers.length, 0, + 'CSS animations running on the compositor should not update style ' + + 'on the main thread'); + await ensureElementRemoval(div); + }); + + add_task_if_omta_enabled(async function no_restyling_for_compositor_transitions() { + const div = addDiv(null, { style: 'transition: opacity 100s; opacity: 0' }); + getComputedStyle(div).opacity; + div.style.opacity = 1; + + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + ok(SpecialPowers.wrap(animation).isRunningOnCompositor); + + const markers = await observeStyling(5); + is(markers.length, 0, + 'CSS transitions running on the compositor should not update style ' + + 'on the main thread'); + await ensureElementRemoval(div); + }); + + add_task_if_omta_enabled(async function no_restyling_when_animation_duration_is_changed() { + const div = addDiv(null, { style: 'animation: opacity 100s' }); + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + ok(SpecialPowers.wrap(animation).isRunningOnCompositor); + + div.animationDuration = '200s'; + + const markers = await observeStyling(5); + is(markers.length, 0, + 'Animations running on the compositor should not update style ' + + 'on the main thread'); + await ensureElementRemoval(div); + }); + + add_task_if_omta_enabled(async function only_one_restyling_after_finish_is_called() { + const div = addDiv(null, { style: 'animation: opacity 100s' }); + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + ok(SpecialPowers.wrap(animation).isRunningOnCompositor); + + animation.finish(); + + let markers = await observeStyling(1); + is(markers.length, 1, + 'Animations running on the compositor should only update style once ' + + 'after finish() is called'); + + markers = await observeStyling(1); + todo_is(markers.length, 0, + 'Bug 1415457: Animations running on the compositor should only ' + + 'update style once after finish() is called'); + + markers = await observeStyling(5); + is(markers.length, 0, + 'Finished animations should never update style after one ' + + 'restyle happened for finish()'); + + await ensureElementRemoval(div); + }); + + add_task(async function no_restyling_mouse_movement_on_finished_transition() { + const div = addDiv(null, { style: 'transition: opacity 1ms; opacity: 0' }); + getComputedStyle(div).opacity; + div.style.opacity = 1; + + const animation = div.getAnimations()[0]; + const initialRect = div.getBoundingClientRect(); + + await animation.finished; + let markers = await observeStyling(1); + is(markers.length, 1, + 'Finished transitions should restyle once after Animation.finished ' + + 'was fulfilled'); + + let mouseX = initialRect.left + initialRect.width / 2; + let mouseY = initialRect.top + initialRect.height / 2; + markers = await observeStyling(5, () => { + // We can't use synthesizeMouse here since synthesizeMouse causes + // layout flush. + synthesizeMouseAtPoint(mouseX++, mouseY++, + { type: 'mousemove' }, window); + }); + + is(markers.length, 0, + 'Finished transitions should never cause restyles when mouse is moved ' + + 'on the transitions'); + await ensureElementRemoval(div); + }); + + add_task(async function no_restyling_mouse_movement_on_finished_animation() { + const div = addDiv(null, { style: 'animation: opacity 1ms' }); + const animation = div.getAnimations()[0]; + + const initialRect = div.getBoundingClientRect(); + + await animation.finished; + let markers = await observeStyling(1); + is(markers.length, 1, + 'Finished animations should restyle once after Animation.finished ' + + 'was fulfilled'); + + let mouseX = initialRect.left + initialRect.width / 2; + let mouseY = initialRect.top + initialRect.height / 2; + markers = await observeStyling(5, () => { + // We can't use synthesizeMouse here since synthesizeMouse causes + // layout flush. + synthesizeMouseAtPoint(mouseX++, mouseY++, + { type: 'mousemove' }, window); + }); + + is(markers.length, 0, + 'Finished animations should never cause restyles when mouse is moved ' + + 'on the animations'); + await ensureElementRemoval(div); + }); + + add_task_if_omta_enabled(async function no_restyling_compositor_animations_out_of_view_element() { + const div = addDiv(null, + { style: 'animation: opacity 100s; transform: translateY(-400px);' }); + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + ok(!SpecialPowers.wrap(animation).isRunningOnCompositor); + + const markers = await observeStyling(5); + + is(markers.length, 0, + 'Animations running on the compositor in an out-of-view element ' + + 'should never cause restyles'); + await ensureElementRemoval(div); + }); + + add_task(async function no_restyling_main_thread_animations_out_of_view_element() { + const div = addDiv(null, + { style: 'animation: on-main-thread 100s; transform: translateY(-400px);' }); + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + const markers = await observeStyling(5); + + is(markers.length, 0, + 'Animations running on the main-thread in an out-of-view element ' + + 'should never cause restyles'); + await ensureElementRemoval(div); + }); + + add_task_if_omta_enabled(async function no_restyling_compositor_animations_in_scrolled_out_element() { + const parentElement = addDiv(null, + { style: 'overflow-y: scroll; height: 20px;' }); + const div = addDiv(null, + { style: 'animation: opacity 100s; position: relative; top: 100px;' }); + parentElement.appendChild(div); + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + + const markers = await observeStyling(5); + + is(markers.length, 0, + 'Animations running on the compositor for elements ' + + 'which are scrolled out should never cause restyles'); + + await ensureElementRemoval(parentElement); + }); + + add_task( + async function no_restyling_missing_keyframe_opacity_animations_on_scrolled_out_element() { + const parentElement = addDiv(null, + { style: 'overflow-y: scroll; height: 20px;' }); + const div = addDiv(null, + { style: 'animation: opacity-without-end-value 100s; ' + + 'position: relative; top: 100px;' }); + parentElement.appendChild(div); + const animation = div.getAnimations()[0]; + await waitForAnimationReadyToRestyle(animation); + + const markers = await observeStyling(5); + + is(markers.length, 0, + 'Opacity animations on scrolled out elements should never cause ' + + 'restyles even if the animation has missing keyframes'); + + await ensureElementRemoval(parentElement); + } + ); + + add_task( + async function restyling_transform_animations_in_scrolled_out_element() { + // Make sure we start from the state right after requestAnimationFrame. + await waitForFrame(); + + const parentElement = addDiv(null, + { style: 'overflow-y: scroll; height: 20px;' }); + const div = addDiv(null, + { style: 'animation: rotate 100s infinite; position: relative; top: 100px;' }); + parentElement.appendChild(div); + const animation = div.getAnimations()[0]; + let timeAtStart = document.timeline.currentTime; + + ok(!animation.isRunningOnCompositor, + 'The transform animation is not running on the compositor'); + + let markers; + let now; + let elapsed; + while (true) { + now = document.timeline.currentTime; + elapsed = (now - timeAtStart); + markers = await observeStyling(1); + if (markers.length) { + break; + } + } + // If the current time has elapsed over 200ms since the animation was + // created, it means that the animation should have already + // unthrottled in this tick, let's see what we observe in this tick's + // restyling process. + // We use toPrecision here and below so 199.99999999999977 will turn into 200. + ok(elapsed.toPrecision(10) >= 200, + 'Transform animation running on the element which is scrolled out ' + + 'should be throttled until 200ms is elapsed. now: ' + + now + ' start time: ' + timeAtStart + ' elapsed:' + elapsed); + + timeAtStart = document.timeline.currentTime; + markers = await observeStyling(1); + now = document.timeline.currentTime; + elapsed = (now - timeAtStart); + + let expectedMarkersLengthValid; + // On the fence of 200 ms, we probably have 1 marker; but if we hit a bad rounding + // we might still have 0. But if it's > 200, we should have 1; and less we should have 0. + if (elapsed.toPrecision(10) == 200) + expectedMarkersLengthValid = markers.length < 2; + else if (elapsed.toPrecision(10) > 200) + expectedMarkersLengthValid = markers.length == 1; + else + expectedMarkersLengthValid = !markers.length; + ok(expectedMarkersLengthValid, + 'Transform animation running on the element which is scrolled out ' + + 'should be unthrottled after around 200ms have elapsed. now: ' + + now + ' start time: ' + timeAtStart + ' elapsed: ' + elapsed); + + await ensureElementRemoval(parentElement); + } + ); + + add_task( + async function restyling_out_of_view_transform_animations_in_another_element() { + // Make sure we start from the state right after requestAnimationFrame. + await waitForFrame(); + + const parentElement = addDiv(null, + { style: 'overflow: hidden;' }); + const div = addDiv(null, + { style: 'animation: move-in 100s infinite;' }); + parentElement.appendChild(div); + const animation = div.getAnimations()[0]; + let timeAtStart = document.timeline.currentTime; + + ok(!animation.isRunningOnCompositor, + 'The transform animation on out of view element ' + + 'is not running on the compositor'); + + // Structure copied from restyling_transform_animations_in_scrolled_out_element + let markers; + let now; + let elapsed; + while (true) { + now = document.timeline.currentTime; + elapsed = (now - timeAtStart); + markers = await observeStyling(1); + if (markers.length) { + break; + } + } + + ok(elapsed.toPrecision(10) >= 200, + 'Transform animation running on out of view element ' + + 'should be throttled until 200ms is elapsed. now: ' + + now + ' start time: ' + timeAtStart + ' elapsed:' + elapsed); + + timeAtStart = document.timeline.currentTime; + markers = await observeStyling(1); + now = document.timeline.currentTime; + elapsed = (now - timeAtStart); + + let expectedMarkersLengthValid; + // On the fence of 200 ms, we probably have 1 marker; but if we hit a bad rounding + // we might still have 0. But if it's > 200, we should have 1; and less we should have 0. + if (elapsed.toPrecision(10) == 200) + expectedMarkersLengthValid = markers.length < 2; + else if (elapsed.toPrecision(10) > 200) + expectedMarkersLengthValid = markers.length == 1; + else + expectedMarkersLengthValid = !markers.length; + ok(expectedMarkersLengthValid, + 'Transform animation running on out of view element ' + + 'should be unthrottled after around 200ms have elapsed. now: ' + + now + ' start time: ' + timeAtStart + ' elapsed: ' + elapsed); + + await ensureElementRemoval(parentElement); + } + ); + + add_task(async function finite_transform_animations_in_out_of_view_element() { + const parentElement = addDiv(null, { style: 'overflow: hidden;' }); + const div = addDiv(null); + const animation = + div.animate({ transform: [ 'translateX(120%)', 'translateX(100%)' ] }, + // This animation will move a bit but + // will remain out-of-view. + 100 * MS_PER_SEC); + parentElement.appendChild(div); + + await waitForAnimationReadyToRestyle(animation); + ok(!SpecialPowers.wrap(animation).isRunningOnCompositor); + + const markers = await observeStyling(20); + is(markers.length, 20, + 'Finite transform animation in out-of-view element should never be ' + + 'throttled'); + + await ensureElementRemoval(parentElement); + }); + + add_task(async function restyling_main_thread_animations_in_scrolled_out_element() { + const parentElement = addDiv(null, + { style: 'overflow-y: scroll; height: 20px;' }); + const div = addDiv(null, + { style: 'animation: on-main-thread 100s; position: relative; top: 20px;' }); + parentElement.appendChild(div); + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + let markers = await observeStyling(5); + + is(markers.length, 0, + 'Animations running on the main-thread for elements ' + + 'which are scrolled out should never cause restyles'); + + await waitForWheelEvent(parentElement); + + // Make sure we are ready to restyle before counting restyles. + await waitForFrame(); + + markers = await observeStyling(5); + is(markers.length, 5, + 'Animations running on the main-thread which were in scrolled out ' + + 'elements should update restyling soon after the element moved in ' + + 'view by scrolling'); + + await ensureElementRemoval(parentElement); + }); + + add_task(async function restyling_main_thread_animations_in_nested_scrolled_out_element() { + const grandParent = addDiv(null, + { style: 'overflow-y: scroll; height: 20px;' }); + const parentElement = addDiv(null, + { style: 'overflow-y: scroll; height: 100px;' }); + const div = addDiv(null, + { style: 'animation: on-main-thread 100s; ' + + 'position: relative; ' + + 'top: 20px;' }); // This element is in-view in the parent, but + // out of view in the grandparent. + grandParent.appendChild(parentElement); + parentElement.appendChild(div); + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + let markers = await observeStyling(5); + + is(markers.length, 0, + 'Animations running on the main-thread which are in nested elements ' + + 'which are scrolled out should never cause restyles'); + + await waitForWheelEvent(grandParent); + + await waitForFrame(); + + markers = await observeStyling(5); + is(markers.length, 5, + 'Animations running on the main-thread which were in nested scrolled ' + + 'out elements should update restyle soon after the element moved ' + + 'in view by scrolling'); + + await ensureElementRemoval(grandParent); + }); + + add_task_if_omta_enabled(async function no_restyling_compositor_animations_in_visibility_hidden_element() { + const div = addDiv(null, + { style: 'animation: opacity 100s; visibility: hidden' }); + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + ok(!SpecialPowers.wrap(animation).isRunningOnCompositor); + + const markers = await observeStyling(5); + + is(markers.length, 0, + 'Animations running on the compositor in visibility hidden element ' + + 'should never cause restyles'); + await ensureElementRemoval(div); + }); + + add_task(async function restyling_main_thread_animations_move_out_of_view_by_scrolling() { + const parentElement = addDiv(null, + { style: 'overflow-y: scroll; height: 200px;' }); + const div = addDiv(null, + { style: 'animation: on-main-thread 100s;' }); + const pad = addDiv(null, + { style: 'height: 400px;' }); + parentElement.appendChild(div); + parentElement.appendChild(pad); + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + + await waitForWheelEvent(parentElement); + + await waitForFrame(); + + const markers = await observeStyling(5); + + // FIXME: We should reduce a redundant restyle here. + ok(markers.length >= 0, + 'Animations running on the main-thread which are in scrolled out ' + + 'elements should throttle restyling'); + + await ensureElementRemoval(parentElement); + }); + + add_task(async function restyling_main_thread_animations_moved_in_view_by_resizing() { + const parentElement = addDiv(null, + { style: 'overflow-y: scroll; height: 20px;' }); + const div = addDiv(null, + { style: 'animation: on-main-thread 100s; position: relative; top: 100px;' }); + parentElement.appendChild(div); + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + + let markers = await observeStyling(5); + is(markers.length, 0, + 'Animations running on the main-thread which is in scrolled out ' + + 'elements should not update restyling'); + + parentElement.style.height = '100px'; + markers = await observeStyling(1); + + is(markers.length, 1, + 'Animations running on the main-thread which was in scrolled out ' + + 'elements should update restyling soon after the element moved in ' + + 'view by resizing'); + + await ensureElementRemoval(parentElement); + }); + + add_task( + async function restyling_animations_on_visibility_changed_element_having_child() { + const div = addDiv(null, + { style: 'animation: on-main-thread 100s;' }); + const childElement = addDiv(null); + div.appendChild(childElement); + + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + + // We don't check the animation causes restyles here since we already + // check it in the first test case. + + div.style.visibility = 'hidden'; + await waitForNextFrame(); + + const markers = await observeStyling(5); + todo_is(markers.length, 0, + 'Animations running on visibility hidden element which ' + + 'has a child whose visiblity is inherited from the element and ' + + 'the element was initially visible'); + + await ensureElementRemoval(div); + } + ); + + add_task( + async function restyling_animations_on_visibility_hidden_element_which_gets_visible() { + const div = addDiv(null, + { style: 'animation: on-main-thread 100s; visibility: hidden' }); + const animation = div.getAnimations()[0]; + + + await waitForAnimationReadyToRestyle(animation); + let markers = await observeStyling(5); + + is(markers.length, 0, + 'Animations running on visibility hidden element should never ' + + 'cause restyles'); + + div.style.visibility = 'visible'; + await waitForNextFrame(); + + markers = await observeStyling(5); + is(markers.length, 5, + 'Animations running that was on visibility hidden element which ' + + 'gets visible should not throttle restyling any more'); + + await ensureElementRemoval(div); + } + ); + + add_task(async function restyling_animations_in_visibility_changed_parent() { + const parentDiv = addDiv(null, { style: 'visibility: hidden' }); + const div = addDiv(null, { style: 'animation: on-main-thread 100s;' }); + parentDiv.appendChild(div); + + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + let markers = await observeStyling(5); + + is(markers.length, 0, + 'Animations running in visibility hidden parent should never cause ' + + 'restyles'); + + parentDiv.style.visibility = 'visible'; + await waitForNextFrame(); + + markers = await observeStyling(5); + is(markers.length, 5, + 'Animations that was in visibility hidden parent should not ' + + 'throttle restyling any more'); + + parentDiv.style.visibility = 'hidden'; + await waitForNextFrame(); + + markers = await observeStyling(5); + is(markers.length, 0, + 'Animations that the parent element became visible should throttle ' + + 'restyling again'); + + await ensureElementRemoval(parentDiv); + }); + + add_task( + async function restyling_animations_on_visibility_hidden_element_with_visibility_changed_children() { + const div = addDiv(null, + { style: 'animation: on-main-thread 100s; visibility: hidden' }); + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + let markers = await observeStyling(5); + + is(markers.length, 0, + 'Animations on visibility hidden element having no visible children ' + + 'should never cause restyles'); + + const childElement = addDiv(null, { style: 'visibility: visible' }); + div.appendChild(childElement); + await waitForNextFrame(); + + markers = await observeStyling(5); + is(markers.length, 5, + 'Animations running on visibility hidden element but the element has ' + + 'a visible child should not throttle restyling'); + + childElement.style.visibility = 'hidden'; + await waitForNextFrame(); + + markers = await observeStyling(5); + todo_is(markers.length, 0, + 'Animations running on visibility hidden element that a child ' + + 'has become invisible should throttle restyling'); + + childElement.style.visibility = 'visible'; + await waitForNextFrame(); + + markers = await observeStyling(5); + is(markers.length, 5, + 'Animations running on visibility hidden element should not throttle ' + + 'restyling after the invisible element changed to visible'); + + childElement.remove(); + await waitForNextFrame(); + + markers = await observeStyling(5); + todo_is(markers.length, 0, + 'Animations running on visibility hidden element should throttle ' + + 'restyling again after all visible descendants were removed'); + + await ensureElementRemoval(div); + } + ); + + add_task( + async function restyling_animations_on_visiblity_hidden_element_having_oof_child() { + const div = addDiv(null, + { style: 'animation: on-main-thread 100s; position: absolute' }); + const childElement = addDiv(null, + { style: 'float: left; visibility: hidden' }); + div.appendChild(childElement); + + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + + // We don't check the animation causes restyles here since we already + // check it in the first test case. + + div.style.visibility = 'hidden'; + await waitForNextFrame(); + + const markers = await observeStyling(5); + is(markers.length, 0, + 'Animations running on visibility hidden element which has an ' + + 'out-of-flow child should throttle restyling'); + + await ensureElementRemoval(div); + } + ); + + add_task( + async function restyling_animations_on_visibility_hidden_element_having_grandchild() { + // element tree: + // + // root(visibility:hidden) + // / \ + // childA childB + // / \ / \ + // AA AB BA BB + + const div = addDiv(null, + { style: 'animation: on-main-thread 100s; visibility: hidden' }); + + const childA = addDiv(null); + div.appendChild(childA); + const childB = addDiv(null); + div.appendChild(childB); + + const grandchildAA = addDiv(null); + childA.appendChild(grandchildAA); + const grandchildAB = addDiv(null); + childA.appendChild(grandchildAB); + + const grandchildBA = addDiv(null); + childB.appendChild(grandchildBA); + const grandchildBB = addDiv(null); + childB.appendChild(grandchildBB); + + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + let markers = await observeStyling(5); + is(markers.length, 0, + 'Animations on visibility hidden element having no visible ' + + 'descendants should never cause restyles'); + + childA.style.visibility = 'visible'; + grandchildAA.style.visibility = 'visible'; + grandchildAB.style.visibility = 'visible'; + await waitForNextFrame(); + + markers = await observeStyling(5); + is(markers.length, 5, + 'Animations running on visibility hidden element but the element has ' + + 'visible children should not throttle restyling'); + + // Make childA hidden again but both of grandchildAA and grandchildAB are + // still visible. + childA.style.visibility = 'hidden'; + await waitForNextFrame(); + + markers = await observeStyling(5); + is(markers.length, 5, + 'Animations running on visibility hidden element that a child has ' + + 'become invisible again but there are still visible children should ' + + 'not throttle restyling'); + + // Make grandchildAA hidden but grandchildAB is still visible. + grandchildAA.style.visibility = 'hidden'; + await waitForNextFrame(); + + markers = await observeStyling(5); + is(markers.length, 5, + 'Animations running on visibility hidden element that a grandchild ' + + 'become invisible again but another grandchild is still visible ' + + 'should not throttle restyling'); + + + // Make childB and grandchildBA visible. + childB.style.visibility = 'visible'; + grandchildBA.style.visibility = 'visible'; + await waitForNextFrame(); + + markers = await observeStyling(5); + is(markers.length, 5, + 'Animations running on visibility hidden element but the element has ' + + 'visible descendants should not throttle restyling'); + + // Make childB hidden but grandchildAB and grandchildBA are still visible. + childB.style.visibility = 'hidden'; + await waitForNextFrame(); + + markers = await observeStyling(5); + is(markers.length, 5, + 'Animations running on visibility hidden element but the element has ' + + 'visible grandchildren should not throttle restyling'); + + // Make grandchildAB hidden but grandchildBA is still visible. + grandchildAB.style.visibility = 'hidden'; + await waitForNextFrame(); + + markers = await observeStyling(5); + is(markers.length, 5, + 'Animations running on visibility hidden element but the element has ' + + 'a visible grandchild should not throttle restyling'); + + // Make grandchildBA hidden. Now all descedants are invisible. + grandchildBA.style.visibility = 'hidden'; + await waitForNextFrame(); + + markers = await observeStyling(5); + todo_is(markers.length, 0, + 'Animations on visibility hidden element that all descendants have ' + + 'become invisible again should never cause restyles'); + + // Make childB visible. + childB.style.visibility = 'visible'; + await waitForNextFrame(); + + markers = await observeStyling(5); + is(markers.length, 5, + 'Animations on visibility hidden element that has a visible child ' + + 'should never cause restyles'); + + // Make childB invisible again + childB.style.visibility = 'hidden'; + await waitForNextFrame(); + + markers = await observeStyling(5); + todo_is(markers.length, 0, + 'Animations on visibility hidden element that the visible child ' + + 'has become invisible again should never cause restyles'); + + await ensureElementRemoval(div); + } + ); + + add_task_if_omta_enabled(async function no_restyling_compositor_animations_after_pause_is_called() { + const div = addDiv(null, { style: 'animation: opacity 100s' }); + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + ok(SpecialPowers.wrap(animation).isRunningOnCompositor); + + animation.pause(); + + await animation.ready; + + let markers = await observeStyling(1); + is(markers.length, 1, + 'Animations running on the compositor should restyle once after ' + + 'Animation.pause() was called'); + + markers = await observeStyling(5); + is(markers.length, 0, + 'Paused animations running on the compositor should never cause ' + + 'restyles'); + await ensureElementRemoval(div); + }); + + add_task(async function no_restyling_main_thread_animations_after_pause_is_called() { + const div = addDiv(null, { style: 'animation: on-main-thread 100s' }); + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + + animation.pause(); + + await animation.ready; + + let markers = await observeStyling(1); + is(markers.length, 1, + 'Animations running on the main-thread should restyle once after ' + + 'Animation.pause() was called'); + + markers = await observeStyling(5); + is(markers.length, 0, + 'Paused animations running on the main-thread should never cause ' + + 'restyles'); + await ensureElementRemoval(div); + }); + + add_task_if_omta_enabled(async function only_one_restyling_when_current_time_is_set_to_middle_of_duration() { + const div = addDiv(null, { style: 'animation: opacity 100s' }); + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + + animation.currentTime = 50 * MS_PER_SEC; + + const markers = await observeStyling(5); + is(markers.length, 1, + 'Bug 1235478: Animations running on the compositor should only once ' + + 'update style when currentTime is set to middle of duration time'); + await ensureElementRemoval(div); + }); + + add_task_if_omta_enabled(async function change_duration_and_currenttime() { + const div = addDiv(null); + const animation = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation); + ok(SpecialPowers.wrap(animation).isRunningOnCompositor); + + // Set currentTime to a time longer than duration. + animation.currentTime = 500 * MS_PER_SEC; + + // Now the animation immediately get back from compositor. + ok(!SpecialPowers.wrap(animation).isRunningOnCompositor); + + // Extend the duration. + animation.effect.updateTiming({ duration: 800 * MS_PER_SEC }); + const markers = await observeStyling(5); + is(markers.length, 1, + 'Animations running on the compositor should update style ' + + 'when duration is made longer than the current time'); + + await ensureElementRemoval(div); + }); + + add_task(async function script_animation_on_display_none_element() { + const div = addDiv(null); + const animation = div.animate({ zIndex: [ '0', '999' ] }, + 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation); + + div.style.display = 'none'; + + // We need to wait a frame to apply display:none style. + await waitForNextFrame(); + + is(animation.playState, 'running', + 'Script animations keep running even when the target element has ' + + '"display: none" style'); + + ok(!SpecialPowers.wrap(animation).isRunningOnCompositor, + 'Script animations on "display:none" element should not run on the ' + + 'compositor'); + + let markers = await observeStyling(5); + is(markers.length, 0, + 'Script animations on "display: none" element should not update styles'); + + div.style.display = ''; + + markers = await observeStyling(5); + is(markers.length, 5, + 'Script animations restored from "display: none" state should update ' + + 'styles'); + + await ensureElementRemoval(div); + }); + + add_task_if_omta_enabled(async function compositable_script_animation_on_display_none_element() { + const div = addDiv(null); + const animation = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation); + + div.style.display = 'none'; + + // We need to wait a frame to apply display:none style. + await waitForNextFrame(); + + is(animation.playState, 'running', + 'Opacity script animations keep running even when the target element ' + + 'has "display: none" style'); + + ok(!SpecialPowers.wrap(animation).isRunningOnCompositor, + 'Opacity script animations on "display:none" element should not ' + + 'run on the compositor'); + + let markers = await observeStyling(5); + is(markers.length, 0, + 'Opacity script animations on "display: none" element should not ' + + 'update styles'); + + div.style.display = ''; + + markers = await observeStyling(1); + is(markers.length, 1, + 'Script animations restored from "display: none" state should update ' + + 'styles soon'); + + ok(SpecialPowers.wrap(animation).isRunningOnCompositor, + 'Opacity script animations restored from "display: none" should be ' + + 'run on the compositor in the next frame'); + + await ensureElementRemoval(div); + }); + + add_task(async function restyling_for_empty_keyframes() { + const div = addDiv(null); + const animation = div.animate({ }, 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation); + let markers = await observeStyling(5); + + is(markers.length, 0, + 'Animations with no keyframes should not cause restyles'); + + animation.effect.setKeyframes({ zIndex: ['0', '999'] }); + markers = await observeStyling(5); + + is(markers.length, 5, + 'Setting valid keyframes should cause regular animation restyles to ' + + 'occur'); + + animation.effect.setKeyframes({ }); + markers = await observeStyling(5); + + is(markers.length, 1, + 'Setting an empty set of keyframes should trigger a single restyle ' + + 'to remove the previous animated style'); + + await ensureElementRemoval(div); + }); + + add_task_if_omta_enabled(async function no_restyling_when_animation_style_when_re_setting_same_animation_property() { + const div = addDiv(null, { style: 'animation: opacity 100s' }); + const animation = div.getAnimations()[0]; + await waitForAnimationReadyToRestyle(animation); + ok(SpecialPowers.wrap(animation).isRunningOnCompositor); + // Apply the same animation style + div.style.animation = 'opacity 100s'; + const markers = await observeStyling(5); + is(markers.length, 0, + 'Applying same animation style ' + + 'should never cause restyles'); + await ensureElementRemoval(div); + }); + + add_task(async function necessary_update_should_be_invoked() { + const div = addDiv(null, { style: 'animation: on-main-thread 100s' }); + const animation = div.getAnimations()[0]; + await waitForAnimationReadyToRestyle(animation); + await waitForAnimationFrames(5); + // Apply another animation style + div.style.animation = 'on-main-thread 110s'; + const markers = await observeStyling(1); + // There should be two restyles. + // 1) Animation-only restyle for before applying the new animation style + // 2) Animation-only restyle for after applying the new animation style + is(markers.length, 2, + 'Applying animation style with different duration ' + + 'should restyle twice'); + await ensureElementRemoval(div); + }); + + add_task_if_omta_enabled( + async function changing_cascading_result_for_main_thread_animation() { + const div = addDiv(null, { style: 'on-main-thread: blue' }); + const animation = div.animate({ opacity: [0, 1], + zIndex: ['0', '999'] }, + 100 * MS_PER_SEC); + await waitForAnimationReadyToRestyle(animation); + ok(SpecialPowers.wrap(animation).isRunningOnCompositor, + 'The opacity animation is running on the compositor'); + // Make the z-index style as !important to cause an update + // to the cascade. + // Bug 1300982: The z-index animation should be no longer + // running on the main thread. + div.style.setProperty('z-index', '0', 'important'); + const markers = await observeStyling(5); + todo_is(markers.length, 0, + 'Changing cascading result for the property running on the main ' + + 'thread does not cause synchronization layer of opacity animation ' + + 'running on the compositor'); + await ensureElementRemoval(div); + } + ); + + add_task_if_omta_enabled( + async function animation_visibility_and_opacity() { + const div = addDiv(null); + const animation1 = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC); + const animation2 = div.animate({ visibility: ['hidden', 'visible'] }, 100 * MS_PER_SEC); + await waitForAnimationReadyToRestyle(animation1); + await waitForAnimationReadyToRestyle(animation2); + const markers = await observeStyling(5); + is(markers.length, 5, 'The animation should not be throttled'); + await ensureElementRemoval(div); + } + ); + + add_task(async function restyling_for_animation_on_orphaned_element() { + const div = addDiv(null); + const animation = div.animate({ marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation); + + div.remove(); + + let markers = await observeStyling(5); + is(markers.length, 0, + 'Animation on orphaned element should not cause restyles'); + + document.body.appendChild(div); + + markers = await observeStyling(5); + is(markers.length, 5, + 'Animation on re-attached to the document begins to update style'); + + await ensureElementRemoval(div); + }); + + add_task_if_omta_enabled( + // Tests that if we remove an element from the document whose animation + // cascade needs recalculating, that it is correctly updated when it is + // re-attached to the document. + async function restyling_for_opacity_animation_on_re_attached_element() { + const div = addDiv(null, { style: 'opacity: 1 ! important' }); + const animation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation); + ok(!SpecialPowers.wrap(animation).isRunningOnCompositor, + 'The opacity animation overridden by an !important rule is NOT ' + + 'running on the compositor'); + + // Drop the !important rule to update the cascade. + div.style.setProperty('opacity', '1', ''); + + div.remove(); + + const markers = await observeStyling(5); + is(markers.length, 0, + 'Opacity animation on orphaned element should not cause restyles'); + + document.body.appendChild(div); + + // Need a frame to give the animation a chance to be sent to the + // compositor. + await waitForNextFrame(); + + ok(SpecialPowers.wrap(animation).isRunningOnCompositor, + 'The opacity animation which is no longer overridden by the ' + + '!important rule begins running on the compositor even if the ' + + '!important rule had been dropped before the target element was ' + + 'removed'); + + await ensureElementRemoval(div); + } + ); + + add_task( + async function no_throttling_additive_animations_out_of_view_element() { + const div = addDiv(null, { style: 'transform: translateY(-400px);' }); + const animation = + div.animate([{ visibility: 'visible' }], + { duration: 100 * MS_PER_SEC, composite: 'add' }); + + await waitForAnimationReadyToRestyle(animation); + + const markers = await observeStyling(5); + + is(markers.length, 5, + 'Additive animation has no keyframe whose offset is 0 or 1 in an ' + + 'out-of-view element should not be throttled'); + await ensureElementRemoval(div); + } + ); + + // Tests that missing keyframes animations don't throttle at all. + add_task(async function no_throttling_animations_out_of_view_element() { + const div = addDiv(null, { style: 'transform: translateY(-400px);' }); + const animation = + div.animate([{ visibility: 'visible' }], 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation); + + const markers = await observeStyling(5); + + is(markers.length, 5, + 'Discrete animation has has no keyframe whose offset is 0 or 1 in an ' + + 'out-of-view element should not be throttled'); + await ensureElementRemoval(div); + }); + + // Tests that missing keyframes animation on scrolled out element that the + // animation is not able to be throttled. + add_task( + async function no_throttling_missing_keyframe_animations_out_of_view_element() { + const div = + addDiv(null, { style: 'transform: translateY(-400px);' + + 'visibility: collapse;' }); + const animation = + div.animate([{ visibility: 'visible' }], 100 * MS_PER_SEC); + await waitForAnimationReadyToRestyle(animation); + + const markers = await observeStyling(5); + is(markers.length, 5, + 'visibility animation has no keyframe whose offset is 0 or 1 in an ' + + 'out-of-view element and produces change hint other than paint-only ' + + 'change hint should not be throttled'); + await ensureElementRemoval(div); + } + ); + + // Counter part of the above test. + add_task(async function no_restyling_discrete_animations_out_of_view_element() { + const div = addDiv(null, { style: 'transform: translateY(-400px);' }); + const animation = + div.animate({ visibility: ['visible', 'hidden'] }, 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation); + + const markers = await observeStyling(5); + + is(markers.length, 0, + 'Discrete animation running on the main-thread in an out-of-view ' + + 'element should never cause restyles'); + await ensureElementRemoval(div); + }); + + add_task(async function no_restyling_while_computed_timing_is_not_changed() { + const div = addDiv(null); + const animation = div.animate({ zIndex: [ '0', '999' ] }, + { duration: 100 * MS_PER_SEC, + easing: 'step-end' }); + + await waitForAnimationReadyToRestyle(animation); + + const markers = await observeStyling(5); + + is(markers.length, 0, + 'Animation running on the main-thread while computed timing is not ' + + 'changed should never cause restyles'); + await ensureElementRemoval(div); + }); + + add_task(async function no_throttling_animations_in_view_svg() { + const div = addDiv(null, { style: 'overflow: scroll;' + + 'height: 100px; width: 100px;' }); + const svg = addSVGElement(div, 'svg', { viewBox: '-10 -10 0.1 0.1', + width: '50px', + height: '50px' }); + const rect = addSVGElement(svg, 'rect', { x: '-10', + y: '-10', + width: '10', + height: '10', + fill: 'red' }); + const animation = rect.animate({ fill: ['blue', 'lime'] }, 100 * MS_PER_SEC); + await waitForAnimationReadyToRestyle(animation); + + const markers = await observeStyling(5); + is(markers.length, 5, + 'CSS animations on an in-view svg element with post-transform should ' + + 'not be throttled.'); + + await ensureElementRemoval(div); + }); + + add_task(async function no_throttling_animations_in_transformed_parent() { + const div = addDiv(null, { style: 'overflow: scroll;' + + 'transform: translateX(50px);' }); + const svg = addSVGElement(div, 'svg', { viewBox: '0 0 1250 1250', + width: '40px', + height: '40px' }); + const rect = addSVGElement(svg, 'rect', { x: '0', + y: '0', + width: '1250', + height: '1250', + fill: 'red' }); + const animation = rect.animate({ fill: ['blue', 'lime'] }, 100 * MS_PER_SEC); + await waitForAnimationReadyToRestyle(animation); + + const markers = await observeStyling(5); + is(markers.length, 5, + 'CSS animations on an in-view svg element which is inside transformed ' + + 'parent should not be throttled.'); + + await ensureElementRemoval(div); + }); + + add_task(async function throttling_animations_out_of_view_svg() { + const div = addDiv(null, { style: 'overflow: scroll;' + + 'height: 100px; width: 100px;' }); + const svg = addSVGElement(div, 'svg', { viewBox: '-10 -10 0.1 0.1', + width: '50px', + height: '50px' }); + const rect = addSVGElement(svg, 'rect', { width: '10', + height: '10', + fill: 'red' }); + + const animation = rect.animate({ fill: ['blue', 'lime'] }, 100 * MS_PER_SEC); + await waitForAnimationReadyToRestyle(animation); + + const markers = await observeStyling(5); + is(markers.length, 0, + 'CSS animations on an out-of-view svg element with post-transform ' + + 'should be throttled.'); + + await ensureElementRemoval(div); + }); + + add_task(async function no_throttling_animations_in_view_css_transform() { + const scrollDiv = addDiv(null, { style: 'overflow: scroll; ' + + 'height: 100px; width: 100px;' }); + const targetDiv = addDiv(null, + { style: 'animation: on-main-thread 100s;' + + 'transform: translate(-50px, -50px);' }); + scrollDiv.appendChild(targetDiv); + + const animation = targetDiv.getAnimations()[0]; + await waitForAnimationReadyToRestyle(animation); + + const markers = await observeStyling(5); + is(markers.length, 5, + 'CSS animation on an in-view element with pre-transform should not ' + + 'be throttled.'); + + await ensureElementRemoval(scrollDiv); + }); + + add_task(async function throttling_animations_out_of_view_css_transform() { + const scrollDiv = addDiv(null, { style: 'overflow: scroll;' + + 'height: 100px; width: 100px;' }); + const targetDiv = addDiv(null, + { style: 'animation: on-main-thread 100s;' + + 'transform: translate(100px, 100px);' }); + scrollDiv.appendChild(targetDiv); + + const animation = targetDiv.getAnimations()[0]; + await waitForAnimationReadyToRestyle(animation); + + const markers = await observeStyling(5); + is(markers.length, 0, + 'CSS animation on an out-of-view element with pre-transform should be ' + + 'throttled.'); + + await ensureElementRemoval(scrollDiv); + }); + + add_task( + async function throttling_animations_in_out_of_view_position_absolute_element() { + const parentDiv = addDiv(null, + { style: 'position: absolute; top: -1000px;' }); + const targetDiv = addDiv(null, + { style: 'animation: on-main-thread 100s;' }); + parentDiv.appendChild(targetDiv); + + const animation = targetDiv.getAnimations()[0]; + await waitForAnimationReadyToRestyle(animation); + + const markers = await observeStyling(5); + is(markers.length, 0, + 'CSS animation in an out-of-view position absolute element should ' + + 'be throttled'); + + await ensureElementRemoval(parentDiv); + } + ); + + add_task( + async function throttling_animations_on_out_of_view_position_absolute_element() { + const div = addDiv(null, + { style: 'animation: on-main-thread 100s; ' + + 'position: absolute; top: -1000px;' }); + + const animation = div.getAnimations()[0]; + await waitForAnimationReadyToRestyle(animation); + + const markers = await observeStyling(5); + is(markers.length, 0, + 'CSS animation on an out-of-view position absolute element should ' + + 'be throttled'); + + await ensureElementRemoval(div); + } + ); + + add_task( + async function throttling_animations_in_out_of_view_position_fixed_element() { + const parentDiv = addDiv(null, + { style: 'position: fixed; top: -1000px;' }); + const targetDiv = addDiv(null, + { style: 'animation: on-main-thread 100s;' }); + parentDiv.appendChild(targetDiv); + + const animation = targetDiv.getAnimations()[0]; + await waitForAnimationReadyToRestyle(animation); + + const markers = await observeStyling(5); + is(markers.length, 0, + 'CSS animation on an out-of-view position:fixed element should be ' + + 'throttled'); + + await ensureElementRemoval(parentDiv); + } + ); + + add_task( + async function throttling_animations_on_out_of_view_position_fixed_element() { + const div = addDiv(null, + { style: 'animation: on-main-thread 100s; ' + + 'position: fixed; top: -1000px;' }); + + const animation = div.getAnimations()[0]; + await waitForAnimationReadyToRestyle(animation); + + const markers = await observeStyling(5); + is(markers.length, 0, + 'CSS animation on an out-of-view position:fixed element should be ' + + 'throttled'); + + await ensureElementRemoval(div); + } + ); + + add_task( + async function no_throttling_animations_in_view_position_fixed_element() { + const iframe = document.createElement('iframe'); + iframe.setAttribute('srcdoc', '<div id="target"></div>'); + document.documentElement.appendChild(iframe); + + await new Promise(resolve => { + iframe.addEventListener('load', () => { + resolve(); + }); + }); + + const target = iframe.contentDocument.getElementById('target'); + target.style= 'position: fixed; top: 20px; width: 100px; height: 100px;'; + + const animation = target.animate({ zIndex: [ '0', '999' ] }, + { duration: 100 * MS_PER_SEC, + iterations: Infinity }); + await waitForAnimationReadyToRestyle(animation); + + const markers = await observeStylingInTargetWindow(iframe.contentWindow, 5); + is(markers.length, 5, + 'CSS animation on an in-view position:fixed element should NOT be ' + + 'throttled'); + + await ensureElementRemoval(iframe); + } + ); + + add_task( + async function throttling_position_absolute_animations_in_collapsed_iframe() { + const iframe = document.createElement('iframe'); + iframe.setAttribute('srcdoc', '<div id="target"></div>'); + iframe.style.height = '0px'; + document.documentElement.appendChild(iframe); + + await new Promise(resolve => { + iframe.addEventListener('load', () => { + resolve(); + }); + }); + + const target = iframe.contentDocument.getElementById("target"); + target.style= 'position: absolute; top: 50%; width: 100px; height: 100px'; + + const animation = target.animate({ opacity: [0, 1] }, + { duration: 100 * MS_PER_SEC, + iterations: Infinity }); + await waitForAnimationReadyToRestyle(animation); + + const markers = await observeStylingInTargetWindow(iframe.contentWindow, 5); + is(markers.length, 0, + 'Animation on position:absolute element in collapsed iframe should ' + + 'be throttled'); + + await ensureElementRemoval(iframe); + } + ); + + add_task( + async function position_absolute_animations_in_collapsed_element() { + const parent = addDiv(null, { style: 'overflow: scroll; height: 0px;' }); + const target = addDiv(null, + { style: 'animation: on-main-thread 100s infinite;' + + 'position: absolute; top: 50%;' + + 'width: 100px; height: 100px;' }); + parent.appendChild(target); + + const animation = target.getAnimations()[0]; + await waitForAnimationReadyToRestyle(animation); + + const markers = await observeStyling(5); + is(markers.length, 5, + 'Animation on position:absolute element in collapsed element ' + + 'should not be throttled'); + + await ensureElementRemoval(parent); + } + ); + + add_task( + async function throttling_position_absolute_animations_in_collapsed_element() { + const parent = addDiv(null, { style: 'overflow: scroll; height: 0px;' }); + const target = addDiv(null, + { style: 'animation: on-main-thread 100s infinite;' + + 'position: absolute; top: 50%;' }); + parent.appendChild(target); + + const animation = target.getAnimations()[0]; + await waitForAnimationReadyToRestyle(animation); + + const markers = await observeStyling(5); + todo_is(markers.length, 0, + 'Animation on collapsed position:absolute element in collapsed ' + + 'element should be throttled'); + + await ensureElementRemoval(parent); + } + ); + + add_task_if_omta_enabled( + async function no_restyling_for_compositor_animation_on_unrelated_style_change() { + const div = addDiv(null); + const animation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation); + ok(SpecialPowers.wrap(animation).isRunningOnCompositor, + 'The opacity animation is running on the compositor'); + + div.style.setProperty('color', 'blue', ''); + const markers = await observeStyling(5); + is(markers.length, 0, + 'The opacity animation keeps running on the compositor when ' + + 'color style is changed'); + await ensureElementRemoval(div); + } + ); + + add_task( + async function no_overflow_transform_animations_in_scrollable_element() { + const parentElement = addDiv(null, + { style: 'overflow-y: scroll; height: 100px;' }); + const div = addDiv(null); + const animation = + div.animate({ transform: [ 'translateY(10px)', 'translateY(10px)' ] }, + 100 * MS_PER_SEC); + parentElement.appendChild(div); + + await waitForAnimationReadyToRestyle(animation); + ok(SpecialPowers.wrap(animation).isRunningOnCompositor); + + const markers = await observeStyling(20); + is(markers.length, 0, + 'No-overflow transform animations running on the compositor should ' + + 'never update style on the main thread'); + + await ensureElementRemoval(parentElement); + } + ); + + add_task(async function no_flush_on_getAnimations() { + const div = addDiv(null); + const animation = + div.animate({ opacity: [ '0', '1' ] }, 100 * MS_PER_SEC); + await waitForAnimationReadyToRestyle(animation); + + ok(SpecialPowers.wrap(animation).isRunningOnCompositor); + + const markers = observeAnimSyncStyling(() => { + is(div.getAnimations().length, 1, 'There should be one animation'); + }); + is(markers.length, 0, + 'Element.getAnimations() should not flush throttled animation style'); + + await ensureElementRemoval(div); + }); + + add_task(async function restyling_for_throttled_animation_on_getAnimations() { + const div = addDiv(null, { style: 'animation: opacity 100s' }); + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + ok(SpecialPowers.wrap(animation).isRunningOnCompositor); + + const markers = observeAnimSyncStyling(() => { + div.style.animationDuration = '0s'; + is(div.getAnimations().length, 0, 'There should be no animation'); + }); + + is(markers.length, 1, // For discarding the throttled animation. + 'Element.getAnimations() should flush throttled animation style so ' + + 'that the throttled animation is discarded'); + + await ensureElementRemoval(div); + }); + + add_task( + async function no_restyling_for_throttled_animation_on_querying_play_state() { + const div = addDiv(null, { style: 'animation: opacity 100s' }); + const animation = div.getAnimations()[0]; + const sibling = addDiv(null); + + await waitForAnimationReadyToRestyle(animation); + ok(SpecialPowers.wrap(animation).isRunningOnCompositor); + + const markers = observeAnimSyncStyling(() => { + sibling.style.opacity = '0.5'; + is(animation.playState, 'running', + 'Animation.playState should be running'); + }); + is(markers.length, 0, + 'Animation.playState should not flush throttled animation in the ' + + 'case where there are only style changes that don\'t affect the ' + + 'throttled animation'); + + await ensureElementRemoval(div); + await ensureElementRemoval(sibling); + } + ); + + add_task( + async function restyling_for_throttled_animation_on_querying_play_state() { + const div = addDiv(null, { style: 'animation: opacity 100s' }); + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + ok(SpecialPowers.wrap(animation).isRunningOnCompositor); + + const markers = observeAnimSyncStyling(() => { + div.style.animationPlayState = 'paused'; + is(animation.playState, 'paused', + 'Animation.playState should be reflected by pending style'); + }); + + is(markers.length, 1, + 'Animation.playState should flush throttled animation style that ' + + 'affects the throttled animation'); + + await ensureElementRemoval(div); + } + ); + + add_task( + async function no_restyling_for_throttled_transition_on_querying_play_state() { + const div = addDiv(null, { style: 'transition: opacity 100s; opacity: 0' }); + const sibling = addDiv(null); + + getComputedStyle(div).opacity; + div.style.opacity = 1; + + const transition = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(transition); + ok(SpecialPowers.wrap(transition).isRunningOnCompositor); + + const markers = observeAnimSyncStyling(() => { + sibling.style.opacity = '0.5'; + is(transition.playState, 'running', + 'Animation.playState should be running'); + }); + + is(markers.length, 0, + 'Animation.playState should not flush throttled transition in the ' + + 'case where there are only style changes that don\'t affect the ' + + 'throttled animation'); + + await ensureElementRemoval(div); + await ensureElementRemoval(sibling); + } + ); + + add_task( + async function restyling_for_throttled_transition_on_querying_play_state() { + const div = addDiv(null, { style: 'transition: opacity 100s; opacity: 0' }); + getComputedStyle(div).opacity; + div.style.opacity = '1'; + + const transition = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(transition); + ok(SpecialPowers.wrap(transition).isRunningOnCompositor); + + const markers = observeAnimSyncStyling(() => { + div.style.transitionProperty = 'none'; + is(transition.playState, 'idle', + 'Animation.playState should be reflected by pending style change ' + + 'which cancel the transition'); + }); + + is(markers.length, 1, + 'Animation.playState should flush throttled transition style that ' + + 'affects the throttled animation'); + + await ensureElementRemoval(div); + } + ); + + add_task(async function restyling_visibility_animations_on_in_view_element() { + const div = addDiv(null); + const animation = + div.animate({ visibility: ['hidden', 'visible'] }, 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation); + + const markers = await observeStyling(5); + + is(markers.length, 5, + 'Visibility animation running on the main-thread on in-view element ' + + 'should not be throttled'); + await ensureElementRemoval(div); + }); + + add_task(async function restyling_outline_offset_animations_on_invisible_element() { + const div = addDiv(null, + { style: 'visibility: hidden; ' + + 'outline-style: solid; ' + + 'outline-width: 1px;' }); + const animation = + div.animate({ outlineOffset: [ '0px', '10px' ] }, + { duration: 100 * MS_PER_SEC, + iterations: Infinity }); + + await waitForAnimationReadyToRestyle(animation); + + const markers = await observeStyling(5); + + is(markers.length, 0, + 'Outline offset animation running on the main-thread on invisible ' + + 'element should be throttled'); + await ensureElementRemoval(div); + }); + + add_task(async function restyling_transform_animations_on_invisible_element() { + const div = addDiv(null, { style: 'visibility: hidden;' }); + + const animation = + div.animate({ transform: [ 'none', 'rotate(360deg)' ] }, + { duration: 100 * MS_PER_SEC, + iterations: Infinity }); + + await waitForAnimationReadyToRestyle(animation); + + ok(!SpecialPowers.wrap(animation).isRunningOnCompositor); + + const markers = await observeStyling(5); + + is(markers.length, 0, + 'Transform animations on visibility hidden element should be throttled'); + await ensureElementRemoval(div); + }); + + add_task(async function restyling_transform_animations_on_invisible_element() { + const div = addDiv(null, { style: 'visibility: hidden;' }); + + const animation = + div.animate([ { transform: 'rotate(360deg)' } ], + { duration: 100 * MS_PER_SEC, + iterations: Infinity }); + + await waitForAnimationReadyToRestyle(animation); + + ok(!SpecialPowers.wrap(animation).isRunningOnCompositor); + + const markers = await observeStyling(5); + + is(markers.length, 0, + 'Transform animations without 100% keyframe on visibility hidden ' + + 'element should be throttled'); + await ensureElementRemoval(div); + }); + + add_task(async function restyling_translate_animations_on_invisible_element() { + const div = addDiv(null, { style: 'visibility: hidden;' }); + + const animation = + div.animate([ { translate: '100px' } ], + { duration: 100 * MS_PER_SEC, + iterations: Infinity }); + + await waitForAnimationReadyToRestyle(animation); + + ok(!SpecialPowers.wrap(animation).isRunningOnCompositor); + + const markers = await observeStyling(5); + + is(markers.length, 0, + 'Translate animations without 100% keyframe on visibility hidden ' + + 'element should be throttled'); + await ensureElementRemoval(div); + }); + + add_task(async function restyling_rotate_animations_on_invisible_element() { + const div = addDiv(null, { style: 'visibility: hidden;' }); + + const animation = + div.animate([ { rotate: '45deg' } ], + { duration: 100 * MS_PER_SEC, + iterations: Infinity }); + + await waitForAnimationReadyToRestyle(animation); + + ok(!SpecialPowers.wrap(animation).isRunningOnCompositor); + + const markers = await observeStyling(5); + + is(markers.length, 0, + 'Rotate animations without 100% keyframe on visibility hidden ' + + 'element should be throttled'); + await ensureElementRemoval(div); + }); + + add_task(async function restyling_scale_animations_on_invisible_element() { + const div = addDiv(null, { style: 'visibility: hidden;' }); + + const animation = + div.animate([ { scale: '2 2' } ], + { duration: 100 * MS_PER_SEC, + iterations: Infinity }); + + await waitForAnimationReadyToRestyle(animation); + + ok(!SpecialPowers.wrap(animation).isRunningOnCompositor); + + const markers = await observeStyling(5); + + is(markers.length, 0, + 'Scale animations without 100% keyframe on visibility hidden ' + + 'element should be throttled'); + await ensureElementRemoval(div); + }); + + add_task( + async function restyling_transform_animations_having_abs_pos_child_on_invisible_element() { + const div = addDiv(null, { style: 'visibility: hidden;' }); + const child = addDiv(null, { style: 'position: absolute; top: 100px;' }); + div.appendChild(child); + + const animation = + div.animate({ transform: [ 'none', 'rotate(360deg)' ] }, + { duration: 100 * MS_PER_SEC, + iterations: Infinity }); + + await waitForAnimationReadyToRestyle(animation); + + ok(!SpecialPowers.wrap(animation).isRunningOnCompositor); + + const markers = await observeStyling(5); + + is(markers.length, 0, + 'Transform animation having an absolutely positioned child on ' + + 'visibility hidden element should be throttled'); + await ensureElementRemoval(div); + }); + + add_task(async function no_restyling_animations_in_out_of_view_iframe() { + const div = addDiv(null, { style: 'overflow-y: scroll; height: 100px;' }); + + const iframe = document.createElement('iframe'); + iframe.setAttribute( + 'srcdoc', + '<div style="height: 100px;"></div><div id="target"></div>'); + div.appendChild(iframe); + + await new Promise(resolve => { + iframe.addEventListener('load', () => { + resolve(); + }); + }); + + const target = iframe.contentDocument.getElementById("target"); + target.style= 'width: 100px; height: 100px;'; + + const animation = target.animate({ zIndex: [ '0', '999' ] }, + 100 * MS_PER_SEC); + await waitForAnimationReadyToRestyle(animation); + + const markers = await observeStylingInTargetWindow(iframe.contentWindow, 5); + is(markers.length, 0, + 'Animation in out-of-view iframe should be throttled'); + + await ensureElementRemoval(div); + }); + + // Tests that transform animations are not able to run on the compositor due + // to layout restrictions (e.g. animations on a large size frame) doesn't + // flush layout at all. + add_task(async function flush_layout_for_transform_animations() { + // Set layout.animation.prerender.partial to disallow transform animations + // on large frames to be sent to the compositor. + await SpecialPowers.pushPrefEnv({ + set: [['layout.animation.prerender.partial', false]] }); + const div = addDiv(null, { style: 'width: 10000px; height: 10000px;' }); + + const animation = div.animate([ { transform: 'rotate(360deg)', } ], + { duration: 100 * MS_PER_SEC, + // Set step-end to skip further restyles. + easing: 'step-end' }); + + const FLUSH_LAYOUT = SpecialPowers.DOMWindowUtils.FLUSH_LAYOUT; + const isWebRender = + SpecialPowers.DOMWindowUtils.layerManagerType.startsWith('WebRender'); + ok(SpecialPowers.DOMWindowUtils.needsFlush(FLUSH_LAYOUT), + 'Flush layout is needed for the appended div'); + await waitForAnimationReadyToRestyle(animation); + + ok(!SpecialPowers.wrap(animation).isRunningOnCompositor); + ok(!SpecialPowers.DOMWindowUtils.needsFlush(FLUSH_LAYOUT), + 'No further flush layout needed'); + + await ensureElementRemoval(div); + }); + + add_task(async function partial_prerendered_transform_animations() { + await SpecialPowers.pushPrefEnv({ + set: [['layout.animation.prerender.partial', true]] }); + const div = addDiv(null, { style: 'width: 10000px; height: 10000px;' }); + + const animation = div.animate( + // Use the same value both for `from` and `to` to avoid jank on the + // compositor. + { transform: ['rotate(0deg)', 'rotate(0deg)'] }, + 100 * MS_PER_SEC + ); + + await waitForAnimationReadyToRestyle(animation); + + const markers = await observeStyling(5); + is(markers.length, 0, + 'Transform animation with partial pre-rendered should never cause ' + + 'restyles'); + + await ensureElementRemoval(div); + }); + + add_task(async function restyling_on_create_animation() { + const div = addDiv(); + const docShell = getDocShellForObservingRestylesForWindow(window); + + const animationA = div.animate( + { transform: ['none', 'rotate(360deg)'] }, + 100 * MS_PER_SEC + ); + const animationB = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC); + const animationC = div.animate( + { color: ['blue', 'green'] }, + 100 * MS_PER_SEC + ); + const animationD = div.animate( + { width: ['100px', '200px'] }, + 100 * MS_PER_SEC + ); + const animationE = div.animate( + { height: ['100px', '200px'] }, + 100 * MS_PER_SEC + ); + + const markers = docShell + .popProfileTimelineMarkers() + .filter(marker => marker.name === 'Styles' && !marker.isAnimationOnly); + docShell.recordProfileTimelineMarkers = false; + + is(markers.length, 0, 'Creating animations should not flush styles'); + + await ensureElementRemoval(div); + }); + + add_task(async function out_of_view_background_position() { + const div = addDiv(null, { + style: ` + background-image: linear-gradient(90deg, rgb(224, 224, 224), rgb(241, 241, 241) 30%, rgb(224, 224, 224) 60%); + background-size: 80px; + animation: background-position 100s infinite; + transform: translateY(-400px); + `, + }) + + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + + const markers = await observeStyling(5); + + is(markers.length, 0, 'background-position animations can be throttled'); + await ensureElementRemoval(div); + }); + + add_task_if_omta_enabled(async function no_restyling_animations_in_opacity_zero_element() { + const div = addDiv(null, { style: 'animation: on-main-thread 100s infinite; opacity: 0' }); + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + const markers = await observeStyling(5); + is(markers.length, 0, + 'Animations running on the main thread in opacity: 0 element ' + + 'should never cause restyles'); + await ensureElementRemoval(div); + }); + + add_task_if_omta_enabled(async function no_restyling_compositor_animations_in_opacity_zero_descendant() { + const container = addDiv(null, { style: 'opacity: 0' }); + const child = addDiv(null, { style: 'animation: background-color 100s infinite;' }); + container.appendChild(child); + + const animation = child.getAnimations()[0]; + await waitForAnimationReadyToRestyle(animation); + ok(!SpecialPowers.wrap(animation).isRunningOnCompositor); + + const markers = await observeStyling(5); + + is(markers.length, 0, + 'Animations running on the compositor in opacity zero descendant element ' + + 'should never cause restyles'); + await ensureElementRemoval(container); + }); + + add_task_if_omta_enabled(async function no_restyling_compositor_animations_in_opacity_zero_descendant_abspos() { + const container = addDiv(null, { style: 'opacity: 0' }); + const child = addDiv(null, { style: 'position: abspos; animation: background-color 100s infinite;' }); + container.appendChild(child); + + const animation = child.getAnimations()[0]; + await waitForAnimationReadyToRestyle(animation); + ok(!SpecialPowers.wrap(animation).isRunningOnCompositor); + + const markers = await observeStyling(5); + + is(markers.length, 0, + 'Animations running on the compositor in opacity zero abspos descendant element ' + + 'should never cause restyles'); + await ensureElementRemoval(container); + }); + + add_task_if_omta_enabled(async function restyling_main_thread_animations_in_opacity_zero_descendant_after_root_opacity_animation() { + const container = addDiv(null, { style: 'opacity: 0' }); + + const child = addDiv(null, { style: 'animation: on-main-thread 100s infinite;' }); + container.appendChild(child); + + // Animate the container from 1 to zero opacity and ensure the child animation is throttled then. + const containerAnimation = container.animate({ opacity: [ '1', '0' ] }, 100); + await containerAnimation.finished; + + const animation = child.getAnimations()[0]; + await waitForAnimationReadyToRestyle(animation); + + const markers = await observeStyling(5); + + is(markers.length, 0, + 'Animations running on the compositor in opacity zero descendant element ' + + 'should never cause restyles after root animation has finished'); + await ensureElementRemoval(container); + }); + + add_task_if_omta_enabled(async function restyling_main_thread_animations_in_opacity_zero_descendant_during_root_opacity_animation() { + const container = addDiv(null, { style: 'opacity: 0; animation: opacity 100s infinite' }); + + const child = addDiv(null, { style: 'animation: on-main-thread 100s infinite;' }); + container.appendChild(child); + + const animation = child.getAnimations()[0]; + await waitForAnimationReadyToRestyle(animation); + + const markers = await observeStyling(5); + + is(markers.length, 5, + 'Animations in opacity zero descendant element ' + + 'should not be throttled if root is animating opacity'); + await ensureElementRemoval(container); + }); + + add_task_if_omta_enabled(async function transparent_background_color_animations() { + const div = addDiv(null); + const animation = + div.animate({ backgroundColor: [ 'rgb(0, 200, 0, 0)', + 'rgb(200, 0, 0, 0.1)' ] }, + { duration: 100 * MS_PER_SEC, + // An easing function producing zero in the first half of + // the duration. + easing: 'cubic-bezier(1, 0, 1, 0)' }); + await waitForAnimationReadyToRestyle(animation); + + ok(SpecialPowers.wrap(animation).isRunningOnCompositor); + + const markers = await observeStyling(5); + is(markers.length, 0, + 'transparent background-color animation should not update styles on ' + + 'the main thread'); + + await ensureElementRemoval(div); + }); + + add_task_if_omta_enabled(async function transform_animation_on_collapsed_element() { + const iframe = document.createElement('iframe'); + document.body.appendChild(iframe); + + // Load a cross origin iframe. + const targetURL = SimpleTest.getTestFileURL("empty.html") + .replace(window.location.origin, "http://example.com/"); + iframe.src = targetURL; + await new Promise(resolve => { + iframe.onload = resolve; + }); + + await SpecialPowers.spawn(iframe, [MS_PER_SEC], async (MS_PER_SEC) => { + // Create a flex item with "preserve-3d" having an abs-pos child inside + // a grid container. + // These styles make the the flex item size (0x0). + const gridContainer = content.document.createElement("div"); + gridContainer.style.display = "grid"; + gridContainer.style.placeItems = "center"; + + const target = content.document.createElement("div"); + target.style.display = "flex"; + target.style.transformStyle = "preserve-3d"; + gridContainer.appendChild(target); + + const child = content.document.createElement("div"); + child.style.position = "absolute"; + child.style.transform = "rotateY(0deg)"; + child.style.width = "100px"; + child.style.height = "100px"; + child.style.backgroundColor = "green"; + target.appendChild(child); + + content.document.body.appendChild(gridContainer); + + const animation = + target.animate({ transform: [ "rotateY(0deg)", "rotateY(360deg)" ] }, + { duration: 100 * MS_PER_SEC, + id: "test", + easing: 'step-end' }); + await content.wrappedJSObject.waitForAnimationReadyToRestyle(animation); + ok(SpecialPowers.wrap(animation).isRunningOnCompositor, + 'transform animation on a collapsed element should run on the ' + + 'compositor'); + + const markers = await content.wrappedJSObject.observeStyling(5); + is(markers.length, 0, + 'transform animation on a collapsed element animation should not ' + + 'update styles on the main thread'); + }); + + await ensureElementRemoval(iframe); + }); +}); + +</script> +</body> diff --git a/dom/animation/test/mozilla/file_transition_finish_on_compositor.html b/dom/animation/test/mozilla/file_transition_finish_on_compositor.html new file mode 100644 index 0000000000..4912d05dd1 --- /dev/null +++ b/dom/animation/test/mozilla/file_transition_finish_on_compositor.html @@ -0,0 +1,67 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<script src="/tests/SimpleTest/paint_listener.js"></script> +<style> +div { + /* Element needs geometry to be eligible for layerization */ + width: 100px; + height: 100px; + background-color: white; +} +</style> +<body> +<script> +'use strict'; + +function waitForPaints() { + return new Promise(function(resolve, reject) { + waitForAllPaintsFlushed(resolve); + }); +} + +promise_test(t => { + // This test only applies to compositor animations + if (!isOMTAEnabled()) { + return; + } + + var div = addDiv(t, { style: 'transition: transform 50ms; ' + + 'transform: translateX(0px)' }); + getComputedStyle(div).transform; + + div.style.transform = 'translateX(100px)'; + + var timeBeforeStart = window.performance.now(); + return waitForPaints().then(() => { + // If it took over 50ms to paint the transition, we have no luck + // to test it. This situation will happen if GC runs while waiting for the + // paint. + if (window.performance.now() - timeBeforeStart >= 50) { + return; + } + + var transform = + SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform'); + assert_not_equals(transform, '', + 'The transition style is applied on the compositor'); + + // Generate artificial busyness on the main thread for 100ms. + var timeAtStart = window.performance.now(); + while (window.performance.now() - timeAtStart < 100) {} + + // Now the transition on the compositor should finish but stay at the final + // position because there was no chance to pull the transition back from + // the compositor. + transform = + SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform'); + assert_equals(transform, 'matrix(1, 0, 0, 1, 100, 0)', + 'The final transition style is still applied on the ' + + 'compositor'); + }); +}, 'Transition on the compositor keeps the final style while the main thread ' + + 'is busy even if the transition finished on the compositor'); + +done(); +</script> +</body> diff --git a/dom/animation/test/mozilla/test_cascade.html b/dom/animation/test/mozilla/test_cascade.html new file mode 100644 index 0000000000..4bdb07530e --- /dev/null +++ b/dom/animation/test/mozilla/test_cascade.html @@ -0,0 +1,37 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../testcommon.js"></script> +<style> +@keyframes margin-left { + from { margin-left: 20px; } + to { margin-left: 80px; } +} +</style> +<body> +<div id="log"></div> +<script> +'use strict'; + +test(function(t) { + var div = addDiv(t, { style: 'transition: margin-left 100s; ' + + 'margin-left: 80px' }); + var cs = getComputedStyle(div); + + assert_equals(cs.marginLeft, '80px', 'initial margin-left'); + + div.style.marginLeft = "20px"; + assert_equals(cs.marginLeft, '80px', 'margin-left transition at 0s'); + + div.style.animation = "margin-left 2s"; + assert_equals(cs.marginLeft, '20px', + 'margin-left animation overrides transition at 0s'); + + div.style.animation = "none"; + assert_equals(cs.marginLeft, '80px', + 'margin-left animation stops overriding transition at 0s'); +}, 'Animation overrides/stops overriding transition immediately'); + +</script> +</body> diff --git a/dom/animation/test/mozilla/test_cubic_bezier_limits.html b/dom/animation/test/mozilla/test_cubic_bezier_limits.html new file mode 100644 index 0000000000..bdbc78654f --- /dev/null +++ b/dom/animation/test/mozilla/test_cubic_bezier_limits.html @@ -0,0 +1,168 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../testcommon.js"></script> +<body> +<style> +@keyframes anim { + to { margin-left: 100px; } +} + +.transition-div { + margin-left: 100px; +} +</style> +<div id="log"></div> +<script> +'use strict'; + +// We clamp +infinity or -inifinity value in floating point to +// maximum floating point value or -maxinum floating point value. +const max_float = '3.40282e38'; + +test(function(t) { + var div = addDiv(t); + var anim = div.animate({ }, 100 * MS_PER_SEC); + + anim.effect.updateTiming({ easing: 'cubic-bezier(0, 1e+39, 0, 0)' }); + assert_equals(anim.effect.getComputedTiming().easing, + 'cubic-bezier(0, ' + max_float + ', 0, 0)', + 'y1 control point for effect easing is out of upper boundary'); + + anim.effect.updateTiming({ easing: 'cubic-bezier(0, 0, 0, 1e+39)' }); + assert_equals(anim.effect.getComputedTiming().easing, + 'cubic-bezier(0, 0, 0, ' + max_float + ')', + 'y2 control point for effect easing is out of upper boundary'); + + anim.effect.updateTiming({ easing: 'cubic-bezier(0, -1e+39, 0, 0)' }); + assert_equals(anim.effect.getComputedTiming().easing, + 'cubic-bezier(0, ' + '-' + max_float + ', 0, 0)', + 'y1 control point for effect easing is out of lower boundary'); + + anim.effect.updateTiming({ easing: 'cubic-bezier(0, 0, 0, -1e+39)' }); + assert_equals(anim.effect.getComputedTiming().easing, + 'cubic-bezier(0, 0, 0, ' + '-' + max_float + ')', + 'y2 control point for effect easing is out of lower boundary'); + +}, 'Clamp y1 and y2 control point out of boundaries for effect easing' ); + +test(function(t) { + var div = addDiv(t); + var anim = div.animate({ }, 100 * MS_PER_SEC); + + anim.effect.setKeyframes([ { easing: 'cubic-bezier(0, 1e+39, 0, 0)' }]); + assert_equals(anim.effect.getKeyframes()[0].easing, + 'cubic-bezier(0, ' + max_float + ', 0, 0)', + 'y1 control point for keyframe easing is out of upper boundary'); + + anim.effect.setKeyframes([ { easing: 'cubic-bezier(0, 0, 0, 1e+39)' }]); + assert_equals(anim.effect.getKeyframes()[0].easing, + 'cubic-bezier(0, 0, 0, ' + max_float + ')', + 'y2 control point for keyframe easing is out of upper boundary'); + + anim.effect.setKeyframes([ { easing: 'cubic-bezier(0, -1e+39, 0, 0)' }]); + assert_equals(anim.effect.getKeyframes()[0].easing, + 'cubic-bezier(0, ' + '-' + max_float + ', 0, 0)', + 'y1 control point for keyframe easing is out of lower boundary'); + + anim.effect.setKeyframes([ { easing: 'cubic-bezier(0, 0, 0, -1e+39)' }]); + assert_equals(anim.effect.getKeyframes()[0].easing, + 'cubic-bezier(0, 0, 0, ' + '-' + max_float + ')', + 'y2 control point for keyframe easing is out of lower boundary'); + +}, 'Clamp y1 and y2 control point out of boundaries for keyframe easing' ); + +test(function(t) { + var div = addDiv(t); + + div.style.animation = 'anim 100s cubic-bezier(0, 1e+39, 0, 0)'; + + assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing, + 'cubic-bezier(0, ' + max_float + ', 0, 0)', + 'y1 control point for CSS animation is out of upper boundary'); + + div.style.animation = 'anim 100s cubic-bezier(0, 0, 0, 1e+39)'; + assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing, + 'cubic-bezier(0, 0, 0, ' + max_float + ')', + 'y2 control point for CSS animation is out of upper boundary'); + + div.style.animation = 'anim 100s cubic-bezier(0, -1e+39, 0, 0)'; + assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing, + 'cubic-bezier(0, ' + '-' + max_float + ', 0, 0)', + 'y1 control point for CSS animation is out of lower boundary'); + + div.style.animation = 'anim 100s cubic-bezier(0, 0, 0, -1e+39)'; + assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing, + 'cubic-bezier(0, 0, 0, ' + '-' + max_float + ')', + 'y2 control point for CSS animation is out of lower boundary'); + +}, 'Clamp y1 and y2 control point out of boundaries for CSS animation' ); + +test(function(t) { + var div = addDiv(t, {'class': 'transition-div'}); + + div.style.transition = 'margin-left 100s cubic-bezier(0, 1e+39, 0, 0)'; + flushComputedStyle(div); + div.style.marginLeft = '0px'; + assert_equals(div.getAnimations()[0].effect.getTiming().easing, + 'cubic-bezier(0, ' + max_float + ', 0, 0)', + 'y1 control point for CSS transition on upper boundary'); + div.style.transition = ''; + div.style.marginLeft = ''; + + div.style.transition = 'margin-left 100s cubic-bezier(0, 0, 0, 1e+39)'; + flushComputedStyle(div); + div.style.marginLeft = '0px'; + assert_equals(div.getAnimations()[0].effect.getTiming().easing, + 'cubic-bezier(0, 0, 0, ' + max_float + ')', + 'y2 control point for CSS transition on upper boundary'); + div.style.transition = ''; + div.style.marginLeft = ''; + + div.style.transition = 'margin-left 100s cubic-bezier(0, -1e+39, 0, 0)'; + flushComputedStyle(div); + div.style.marginLeft = '0px'; + assert_equals(div.getAnimations()[0].effect.getTiming().easing, + 'cubic-bezier(0, ' + '-' + max_float + ', 0, 0)', + 'y1 control point for CSS transition on lower boundary'); + div.style.transition = ''; + div.style.marginLeft = ''; + + div.style.transition = 'margin-left 100s cubic-bezier(0, 0, 0, -1e+39)'; + flushComputedStyle(div); + div.style.marginLeft = '0px'; + assert_equals(div.getAnimations()[0].effect.getTiming().easing, + 'cubic-bezier(0, 0, 0, ' + '-' + max_float + ')', + 'y2 control point for CSS transition on lower boundary'); + +}, 'Clamp y1 and y2 control point out of boundaries for CSS transition' ); + +test(function(t) { + var div = addDiv(t); + var anim = div.animate({ }, { duration: 100 * MS_PER_SEC, fill: 'forwards' }); + + anim.pause(); + // The positive steepest function on both edges. + anim.effect.updateTiming({ easing: 'cubic-bezier(0, 1e+39, 0, 1e+39)' }); + assert_equals(anim.effect.getComputedTiming().progress, 0.0, + 'progress on lower edge for the highest value of y1 and y2 control points'); + + anim.finish(); + assert_equals(anim.effect.getComputedTiming().progress, 1.0, + 'progress on upper edge for the highest value of y1 and y2 control points'); + + // The negative steepest function on both edges. + anim.effect.updateTiming({ easing: 'cubic-bezier(0, -1e+39, 0, -1e+39)' }); + anim.currentTime = 0; + assert_equals(anim.effect.getComputedTiming().progress, 0.0, + 'progress on lower edge for the lowest value of y1 and y2 control points'); + + anim.finish(); + assert_equals(anim.effect.getComputedTiming().progress, 1.0, + 'progress on lower edge for the lowest value of y1 and y2 control points'); + +}, 'Calculated values on both edges'); + +</script> +</body> diff --git a/dom/animation/test/mozilla/test_deferred_start.html b/dom/animation/test/mozilla/test_deferred_start.html new file mode 100644 index 0000000000..7d0a15b1f7 --- /dev/null +++ b/dom/animation/test/mozilla/test_deferred_start.html @@ -0,0 +1,21 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { + set: [ + ["dom.animations-api.core.enabled", true], + ["dom.animations-api.getAnimations.enabled", true], + ["dom.animations-api.timelines.enabled", true], + ], + }, + function() { + window.open("file_deferred_start.html"); + } +); +</script> diff --git a/dom/animation/test/mozilla/test_disable_animations_api_autoremove.html b/dom/animation/test/mozilla/test_disable_animations_api_autoremove.html new file mode 100644 index 0000000000..56e6362273 --- /dev/null +++ b/dom/animation/test/mozilla/test_disable_animations_api_autoremove.html @@ -0,0 +1,15 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({ explicit_done: true }); +SpecialPowers.pushPrefEnv( + { set: [['dom.animations-api.autoremove.enabled', false]] }, + function() { + window.open('file_disable_animations_api_autoremove.html'); + } +); +</script> diff --git a/dom/animation/test/mozilla/test_disable_animations_api_compositing.html b/dom/animation/test/mozilla/test_disable_animations_api_compositing.html new file mode 100644 index 0000000000..94216ea62d --- /dev/null +++ b/dom/animation/test/mozilla/test_disable_animations_api_compositing.html @@ -0,0 +1,14 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.compositing.enabled", false]]}, + function() { + window.open("file_disable_animations_api_compositing.html"); + }); +</script> diff --git a/dom/animation/test/mozilla/test_disable_animations_api_get_animations.html b/dom/animation/test/mozilla/test_disable_animations_api_get_animations.html new file mode 100644 index 0000000000..a7253439b7 --- /dev/null +++ b/dom/animation/test/mozilla/test_disable_animations_api_get_animations.html @@ -0,0 +1,14 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.getAnimations.enabled", false]]}, + function() { + window.open("file_disable_animations_api_get_animations.html"); + }); +</script> diff --git a/dom/animation/test/mozilla/test_disable_animations_api_implicit_keyframes.html b/dom/animation/test/mozilla/test_disable_animations_api_implicit_keyframes.html new file mode 100644 index 0000000000..aaebf1f00a --- /dev/null +++ b/dom/animation/test/mozilla/test_disable_animations_api_implicit_keyframes.html @@ -0,0 +1,14 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.implicit-keyframes.enabled", false]]}, + function() { + window.open("file_disable_animations_api_implicit_keyframes.html"); + }); +</script> diff --git a/dom/animation/test/mozilla/test_disable_animations_api_timelines.html b/dom/animation/test/mozilla/test_disable_animations_api_timelines.html new file mode 100644 index 0000000000..a20adf4ea2 --- /dev/null +++ b/dom/animation/test/mozilla/test_disable_animations_api_timelines.html @@ -0,0 +1,16 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; + +setup({ explicit_done: true }); +SpecialPowers.pushPrefEnv( + { set: [['dom.animations-api.timelines.enabled', false]] }, + function() { + window.open('file_disable_animations_api_timelines.html'); + } +); +</script> diff --git a/dom/animation/test/mozilla/test_disabled_properties.html b/dom/animation/test/mozilla/test_disabled_properties.html new file mode 100644 index 0000000000..2244143ceb --- /dev/null +++ b/dom/animation/test/mozilla/test_disabled_properties.html @@ -0,0 +1,73 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../testcommon.js"></script> +<body> +<div id="log"></div> +<script> +'use strict'; + +function waitForSetPref(pref, value) { + return SpecialPowers.pushPrefEnv({ 'set': [[pref, value]] }); +} + +/* + * These tests rely on the fact that the overflow-clip-box property is + * disabled by the layout.css.overflow-clip-box.enabled pref. If we ever remove + * that pref we will need to substitute some other pref:property combination. + */ + +promise_test(function(t) { + return waitForSetPref('layout.css.overflow-clip-box.enabled', true).then(() => { + var anim = addDiv(t).animate({ overflowClipBoxBlock: [ 'padding-box', 'content-box' ]}); + assert_equals(anim.effect.getKeyframes().length, 2, + 'A property-indexed keyframe specifying only enabled' + + ' properties produces keyframes'); + return waitForSetPref('layout.css.overflow-clip-box.enabled', false); + }).then(() => { + var anim = addDiv(t).animate({ overflowClipBoxBlock: [ 'padding-box', 'content-box' ]}); + assert_equals(anim.effect.getKeyframes().length, 0, + 'A property-indexed keyframe specifying only disabled' + + ' properties produces no keyframes'); + }); +}, 'Specifying a disabled property using a property-indexed keyframe'); + +promise_test(function(t) { + var createAnim = () => { + var anim = addDiv(t).animate([ { overflowClipBoxBlock: 'padding-box' }, + { overflowClipBoxBlock: 'content-box' } ]); + assert_equals(anim.effect.getKeyframes().length, 2, + 'Animation specified using a keyframe sequence should' + + ' return the same number of keyframes regardless of' + + ' whether or not the specified properties are disabled'); + return anim; + }; + + var assert_has_property = (anim, index, descr, property) => { + assert_true( + anim.effect.getKeyframes()[index].hasOwnProperty(property), + `${descr} should have the '${property}' property`); + }; + var assert_does_not_have_property = (anim, index, descr, property) => { + assert_false( + anim.effect.getKeyframes()[index].hasOwnProperty(property), + `${descr} should NOT have the '${property}' property`); + }; + + return waitForSetPref('layout.css.overflow-clip-box.enabled', true).then(() => { + var anim = createAnim(); + assert_has_property(anim, 0, 'Initial keyframe', 'overflowClipBoxBlock'); + assert_has_property(anim, 1, 'Final keyframe', 'overflowClipBoxBlock'); + return waitForSetPref('layout.css.overflow-clip-box.enabled', false); + }).then(() => { + var anim = createAnim(); + assert_does_not_have_property(anim, 0, 'Initial keyframe', + 'overflowClipBoxBlock'); + assert_does_not_have_property(anim, 1, 'Final keyframe', + 'overflowClipBoxBlock'); + }); +}, 'Specifying a disabled property using a keyframe sequence'); + +</script> +</body> diff --git a/dom/animation/test/mozilla/test_discrete_animations.html b/dom/animation/test/mozilla/test_discrete_animations.html new file mode 100644 index 0000000000..d4826a74bd --- /dev/null +++ b/dom/animation/test/mozilla/test_discrete_animations.html @@ -0,0 +1,16 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [ + ["layout.css.osx-font-smoothing.enabled", true], + ] }, + function() { + window.open("file_discrete_animations.html"); + }); +</script> diff --git a/dom/animation/test/mozilla/test_distance_of_basic_shape.html b/dom/animation/test/mozilla/test_distance_of_basic_shape.html new file mode 100644 index 0000000000..65e403bf06 --- /dev/null +++ b/dom/animation/test/mozilla/test_distance_of_basic_shape.html @@ -0,0 +1,91 @@ +<!doctype html> +<meta charset=utf-8> +<script src='/resources/testharness.js'></script> +<script src='/resources/testharnessreport.js'></script> +<script src='../testcommon.js'></script> +<div id='log'></div> +<script type='text/javascript'> +'use strict'; + +// We don't have an official spec to define the distance between two basic +// shapes, but we still need this for DevTools, so Gecko and Servo backends use +// the similar rules to define the distance. If there is a spec for it, we have +// to update this test file. +// See https://github.com/w3c/csswg-drafts/issues/662. + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'clip-path', 'none', 'none'); + assert_equals(dist, 0, 'none and none'); +}, 'none and none'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'clip-path', 'circle(10px)', 'circle(20px)'); + assert_equals(dist, 10, 'circle(10px) and circle(20px)'); +}, 'circles'); + +test(function(t) { + var target = addDiv(t); + var circle1 = 'circle(calc(10px + 10px) at 20px 10px)'; + var circle2 = 'circle(30px at 10px 10px)'; + var dist = getDistance(target, 'clip-path', circle1, circle2); + assert_equals(dist, + Math.sqrt(10 * 10 + 10 * 10), + circle1 + ' and ' + circle2); +}, 'circles with positions'); + +test(function(t) { + var target = addDiv(t); + var ellipse1 = 'ellipse(20px calc(10px + 10px))'; + var ellipse2 = 'ellipse(30px 30px)'; + var dist = getDistance(target, 'clip-path', ellipse1, ellipse2); + assert_equals(dist, + Math.sqrt(10 * 10 + 10 * 10), + ellipse1 + ' and ' + ellipse2); +}, 'ellipses'); + +test(function(t) { + var target = addDiv(t); + var polygon1 = 'polygon(50px 0px, 100px 50px, 50px 100px, 0px 50px)'; + var polygon2 = 'polygon(40px 0px, 100px 70px, 10px 100px, 0px 70px)'; + var dist = getDistance(target, 'clip-path', polygon1, polygon2); + assert_equals(dist, + Math.sqrt(10 * 10 + 20 * 20 + 40 * 40 + 20 * 20), + polygon1 + ' and ' + polygon2); +}, 'polygons'); + +test(function(t) { + var target = addDiv(t); + var inset1 = 'inset(5px 5px 5px 5px round 40px 30px 20px 5px)'; + var inset2 = 'inset(10px 5px round 50px 60px)'; + var dist = getDistance(target, 'clip-path', inset1, inset2); + + // if we have only two parameter in inset(), the first one means + // top and bottom edges, and the second one means left and right edges. + // and the definitions of inset is inset(top, right, bottom, left). Besides, + // the "round" part uses the shorthand of border radius for each corner, so + // each corner is a pair of (x, y). We are computing the distance between: + // 1. inset(5px 5px 5px 5px + // round (40px 40px) (30px 30px) (20px 20px) (5px 5px)) + // 2. inset(10px 5px 10px 5px + // round (50px 50px) (60px 60px) (50px 50px) (60px 60px)) + // That is why we need to multiply 2 for each border-radius corner. + assert_equals(dist, + Math.sqrt(5 * 5 + 5 * 5 + + (50 - 40) * (50 - 40) * 2 + + (60 - 30) * (60 - 30) * 2 + + (50 - 20) * (50 - 20) * 2 + + (60 - 5) * (60 - 5) * 2), + inset1 + ' and ' + inset2); +}, 'insets'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'clip-path', + 'circle(20px)', 'ellipse(10px 20px)'); + assert_equals(dist, 0, 'circle(20px) and ellipse(10px 20px)'); +}, 'Mismatched basic shapes'); + +</script> +</html> diff --git a/dom/animation/test/mozilla/test_distance_of_filter.html b/dom/animation/test/mozilla/test_distance_of_filter.html new file mode 100644 index 0000000000..33f772d983 --- /dev/null +++ b/dom/animation/test/mozilla/test_distance_of_filter.html @@ -0,0 +1,248 @@ +<!doctype html> +<meta charset=utf-8> +<script src='/resources/testharness.js'></script> +<script src='/resources/testharnessreport.js'></script> +<script src='../testcommon.js'></script> +<div id='log'></div> +<script type='text/javascript'> +'use strict'; + +const EPSILON = 1e-6; + +// We don't have an official spec to define the distance between two filter +// lists, but we still need this for DevTools, so Gecko and Servo backends use +// the similar rules to define the distance. If there is a spec for it, we have +// to update this test file. +// See https://github.com/w3c/fxtf-drafts/issues/91. + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', 'none', 'none'); + assert_equals(dist, 0, 'none and none'); +}, 'none and none'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', 'blur(10px)', 'none'); + // The default value of blur is 0px. + assert_equals(dist, 10, 'blur(10px) and none'); +}, 'blur and none'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', 'blur(10px)', 'blur(1px)'); + assert_equals(dist, 9, 'blur(10px) and blur(1px)'); +}, 'blurs'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', 'brightness(75%)', 'none'); + // The default value of brightness is 100%. + assert_equals(dist, (1 - 0.75), 'brightness(75%) and none'); +}, 'brightness and none'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', + 'brightness(50%)', 'brightness(175%)'); + assert_equals(dist, (1.75 - 0.5), 'brightness(50%) and brightness(175%)'); +}, 'brightnesses'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', 'contrast(75%)', 'none'); + // The default value of contrast is 100%. + assert_equals(dist, (1 - 0.75), 'contrast(75%) and none'); +}, 'contrast and none'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', 'contrast(50%)', 'contrast(175%)'); + assert_equals(dist, (1.75 - 0.5), 'contrast(50%) and contrast(175%)'); +}, 'contrasts'); + +test(function(t) { + var target = addDiv(t); + var filter1 = 'drop-shadow(10px 10px 10px blue)'; + var filter2 = 'none'; + var dist = getDistance(target, 'filter', filter1, filter2); + // The rgba of Blue is rgba(0, 0, 255, 1.0) = rgba(0%, 0%, 100%, 100%). + // So we are try to compute the distance of + // 1. drop-shadow(10, 10, 10, rgba(0, 0, 1.0, 1.0)). + // 2. drop-shadow( 0, 0, 0, rgba(0, 0, 0, 0)). + assert_equals(dist, + Math.sqrt(10 * 10 * 3 + (1 * 1 + 1 * 1)), + filter1 + ' and ' + filter2); +}, 'drop-shadow and none'); + +test(function(t) { + var target = addDiv(t); + var filter1 = 'drop-shadow(10px 10px 10px blue)'; + var filter2 = 'drop-shadow(5px 5px 1px yellow)'; + var dist = getDistance(target, 'filter', filter1, filter2); + // Blue: rgba(0, 0, 255, 1.0) = rgba( 0%, 0%, 100%, 100%). + // Yellow: rgba(255, 255, 0, 1.0) = rgba(100%, 100%, 0%, 100%). + assert_equals(dist, + Math.sqrt(5 * 5 * 2 + 9 * 9 + (1 * 1 * 3)), + filter1 + ' and ' + filter2); +}, 'drop-shadows'); + +test(function(t) { + var target = addDiv(t); + var filter1 = 'drop-shadow(10px 10px 10px)'; + var filter2 = 'drop-shadow(5px 5px 1px yellow)'; + var dist = getDistance(target, 'filter', filter1, filter2); + // Yellow: rgba(255, 255, 0, 1.0) = rgba(100%, 100%, 0%, 100%) + // Transparent: rgba(0, 0, 0, 0) = rgba( 0%, 0%, 0%, 0%) + // Distance involving `currentcolor` is calculated as distance + // from `transparent` + assert_equals(dist, + Math.sqrt(5 * 5 * 2 + 9 * 9 + (1 * 1 * 3)), + filter1 + ' and ' + filter2); +}, 'drop-shadows with color and non-color'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', 'grayscale(25%)', 'none'); + // The default value of grayscale is 0%. + assert_equals(dist, 0.25, 'grayscale(25%) and none'); +}, 'grayscale and none'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', 'grayscale(50%)', 'grayscale(75%)'); + assert_equals(dist, 0.25, 'grayscale(50%) and grayscale(75%)'); +}, 'grayscales'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', 'grayscale(75%)', 'grayscale(175%)'); + assert_equals(dist, 0.25, 'distance of grayscale(75%) and grayscale(175%)'); +}, 'grayscales where one has a value larger than 1.0'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', 'hue-rotate(180deg)', 'none'); + // The default value of hue-rotate is 0deg. + assert_approx_equals(dist, Math.PI, EPSILON, 'hue-rotate(180deg) and none'); +}, 'hue-rotate and none'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', + 'hue-rotate(720deg)', 'hue-rotate(-180deg)'); + assert_approx_equals(dist, 5 * Math.PI, EPSILON, + 'hue-rotate(720deg) and hue-rotate(-180deg)'); +}, 'hue-rotates'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', 'invert(25%)', 'none'); + // The default value of invert is 0%. + assert_equals(dist, 0.25, 'invert(25%) and none'); +}, 'invert and none'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', 'invert(50%)', 'invert(75%)'); + assert_equals(dist, 0.25, 'invert(50%) and invert(75%)'); +}, 'inverts'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', 'invert(75%)', 'invert(175%)'); + assert_equals(dist, 0.25, 'invert(75%) and invert(175%)'); +}, 'inverts where one has a value larger than 1.0'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', 'opacity(75%)', 'none'); + // The default value of opacity is 100%. + assert_equals(dist, (1 - 0.75), 'opacity(75%) and none'); +}, 'opacity and none'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', 'opacity(50%)', 'opacity(75%)'); + assert_equals(dist, 0.25, 'opacity(50%) and opacity(75%)'); +}, 'opacities'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', 'opacity(75%)', 'opacity(175%)'); + assert_equals(dist, 0.25, 'opacity(75%) and opacity(175%)'); +}, 'opacities where one has a value larger than 1.0'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', 'saturate(75%)', 'none'); + // The default value of saturate is 100%. + assert_equals(dist, (1 - 0.75), 'saturate(75%) and none'); +}, 'saturate and none'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', 'saturate(50%)', 'saturate(175%)'); + assert_equals(dist, (1.75 - 0.5), 'saturate(50%) and saturate(175%)'); +}, 'saturates'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', 'sepia(25%)', 'none'); + // The default value of sepia is 0%. + assert_equals(dist, 0.25, 'sepia(25%) and none'); +}, 'sepia and none'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', 'sepia(50%)', 'sepia(75%)'); + assert_equals(dist, 0.25, 'sepia(50%) and sepia(75%)'); +}, 'sepias'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', 'sepia(75%)', 'sepia(175%)'); + assert_equals(dist, 0.25, 'sepia(75%) and sepia(175%)'); +}, 'sepias where one has a value larger than 1.0'); + +test(function(t) { + var target = addDiv(t); + var filter1 = 'grayscale(50%) opacity(100%) blur(5px)'; + // none filter: 'grayscale(0) opacity(1) blur(0px)' + var filter2 = 'none'; + var dist = getDistance(target, 'filter', filter1, filter2); + assert_equals(dist, + Math.sqrt(0.5 * 0.5 + 5 * 5), + filter1 + ' and ' + filter2); +}, 'Filter list and none'); + +test(function(t) { + var target = addDiv(t); + var filter1 = 'grayscale(50%) opacity(100%) blur(5px)'; + var filter2 = 'grayscale(100%) opacity(50%) blur(1px)'; + var dist = getDistance(target, 'filter', filter1, filter2); + assert_equals(dist, + Math.sqrt(0.5 * 0.5 + 0.5 * 0.5 + 4 * 4), + filter1 + ' and ' + filter2); +}, 'Filter lists'); + +test(function(t) { + var target = addDiv(t); + var filter1 = 'grayscale(50%) opacity(100%) blur(5px)'; + var filter2 = 'grayscale(100%) opacity(50%) blur(1px) sepia(50%)'; + var dist = getDistance(target, 'filter', filter1, filter2); + assert_equals(dist, + Math.sqrt(0.5 * 0.5 + 0.5 * 0.5 + 4 * 4 + 0.5 * 0.5), + filter1 + ' and ' + filter2); +}, 'Filter lists where one has extra functions'); + +test(function(t) { + var target = addDiv(t); + var filter1 = 'grayscale(50%) opacity(100%)'; + var filter2 = 'opacity(100%) grayscale(50%)'; + var dist = getDistance(target, 'filter', filter1, filter2); + assert_equals(dist, 0, filter1 + ' and ' + filter2); +}, 'Mismatched filter lists'); + +</script> +</html> diff --git a/dom/animation/test/mozilla/test_distance_of_path_function.html b/dom/animation/test/mozilla/test_distance_of_path_function.html new file mode 100644 index 0000000000..af6592c892 --- /dev/null +++ b/dom/animation/test/mozilla/test_distance_of_path_function.html @@ -0,0 +1,140 @@ +<!doctype html> +<meta charset=utf-8> +<script src='/resources/testharness.js'></script> +<script src='/resources/testharnessreport.js'></script> +<script src='../testcommon.js'></script> +<div id="log"></div> +<script type='text/javascript'> +'use strict'; + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'offset-path', 'none', 'none'); + assert_equals(dist, 0, 'none and none'); +}, 'none and none'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'offset-path', 'path("M 10 10")', 'none'); + assert_equals(dist, 0, 'path("M 10 10") and none'); +}, 'Path and none'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'offset-path', + 'path("M 10 10 H 10")', + 'path("M 10 10 H 10 H 10")'); + assert_equals(dist, 0, 'path("M 10 10 H 10") and ' + + 'path("M 10 10 H 10 H 10")'); +}, 'Mismatched path functions'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'offset-path', + 'path("M 10 10")', + 'path("M 20 20")'); + assert_equals(dist, + Math.sqrt(10 * 10 * 2), + 'path("M 10 10") and path("M 30 30")'); +}, 'The moveto commands'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'offset-path', + 'path("M 0 0 L 10 10")', + 'path("M 0 0 L 20 20")'); + assert_equals(dist, + Math.sqrt(10 * 10 * 2), + 'path("M 0 0 L 10 10") and path("M 0 0 L 20 20")'); +}, 'The lineto commands'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'offset-path', + 'path("M 0 0 H 10")', + 'path("M 0 0 H 20")'); + assert_equals(dist, 10, 'path("M 0 0 H 10") and path("M 0 0 H 20")'); +}, 'The horizontal lineto commands'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'offset-path', + 'path("M 0 0 V 10")', + 'path("M 0 0 V 20")'); + assert_equals(dist, 10, 'path("M 0 0 V 10") and path("M 0 0 V 20")'); +}, 'The vertical lineto commands'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'offset-path', + 'path("M 0 0 C 10 10 20 20 30 30")', + 'path("M 0 0 C 20 20 40 40 0 0")'); + assert_equals(dist, + Math.sqrt(10 * 10 * 2 + 20 * 20 * 2 + 30 * 30 * 2), + 'path("M 0 0 C 10 10 20 20 30 30") and ' + + 'path("M 0 0 C 20 20 40 40 0 0")'); +}, 'The cubic Bézier curve commands'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'offset-path', + 'path("M 0 0 S 20 20 30 30")', + 'path("M 0 0 S 40 40 0 0")'); + assert_equals(dist, + Math.sqrt(20 * 20 * 2 + 30 * 30 * 2), + 'path("M 0 0 S 20 20 30 30") and ' + + 'path("M 0 0 S 40 40 0 0")'); +}, 'The smooth cubic Bézier curve commands'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'offset-path', + 'path("M 0 0 Q 10 10 30 30")', + 'path("M 0 0 Q 20 20 0 0")'); + assert_equals(dist, + Math.sqrt(10 * 10 * 2 + 30 * 30 * 2), + 'path("M 0 0 Q 10 10 30 30") and ' + + 'path("M 0 0 Q 20 20 0 0")'); +}, 'The quadratic cubic Bézier curve commands'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'offset-path', + 'path("M 0 0 T 30 30")', + 'path("M 0 0 T 0 0")'); + assert_equals(dist, + Math.sqrt(30 * 30 * 2), + 'path("M 0 0 T 30 30") and ' + + 'path("M 0 0 T 0 0")'); +}, 'The smooth quadratic cubic Bézier curve commands'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'offset-path', + 'path("M 0 0 A 5 5 10 0 1 30 30")', + 'path("M 0 0 A 4 4 5 0 0 20 20")'); + assert_equals(dist, + Math.sqrt(1 * 1 * 2 + // radii + 5 * 5 + // angle + 1 * 1 + // flag + 10 * 10 * 2), + 'path("M 0 0 A 5 5 10 0 1 30 30") and ' + + 'path("M 0 0 A 4 4 5 0 0 20 20")'); +}, 'The elliptical arc curve commands'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'offset-path', + 'path("m 10 20 h 30 v 60 h 10 v -10 l 110 60")', + // == 'path("M 10 20 H 40 V 80 H 50 V 70 L 160 130")' + 'path("M 130 140 H 120 V 160 H 130 V 150 L 200 170")'); + assert_equals(dist, + Math.sqrt(120 * 120 * 2 + + 80 * 80 * 4 + + 40 * 40 * 2), + 'path("m 10 20 h 30 v 60 h 10 v -10 l 110 60") and ' + + 'path("M 130 140 H 120 V 160 H 130 V 150 L 200 170")'); +}, 'The distance of paths with absolute and relative coordinates'); + +</script> +</html> diff --git a/dom/animation/test/mozilla/test_distance_of_transform.html b/dom/animation/test/mozilla/test_distance_of_transform.html new file mode 100644 index 0000000000..96ff1eb66d --- /dev/null +++ b/dom/animation/test/mozilla/test_distance_of_transform.html @@ -0,0 +1,404 @@ +<!doctype html> +<meta charset=utf-8> +<script src='/resources/testharness.js'></script> +<script src='/resources/testharnessreport.js'></script> +<script src='../testcommon.js'></script> +<div id='log'></div> +<script type='text/javascript'> +'use strict'; + +// We don't have an official spec to define the distance between two transform +// lists, but we still need this for DevTools, so Gecko and Servo backend use +// the similar rules to define the distance. If there is a spec for it, we have +// to update this test file. + +const EPSILON = 0.00001; + +// |v| should be a unit vector (i.e. having length 1) +function getQuaternion(v, angle) { + return [ + v[0] * Math.sin(angle / 2.0), + v[1] * Math.sin(angle / 2.0), + v[2] * Math.sin(angle / 2.0), + Math.cos(angle / 2.0) + ]; +} + +function computeRotateDistance(q1, q2) { + const dot = q1.reduce((sum, e, i) => sum + e * q2[i], 0); + return Math.acos(Math.min(Math.max(dot, -1.0), 1.0)) * 2.0; +} + +function createMatrixFromArray(array) { + return (array.length === 16 ? 'matrix3d' : 'matrix') + `(${array.join()})`; +} + +function rotate3dToMatrix(x, y, z, radian) { + var sc = Math.sin(radian / 2) * Math.cos(radian / 2); + var sq = Math.sin(radian / 2) * Math.sin(radian / 2); + + // Normalize the vector. + var length = Math.sqrt(x*x + y*y + z*z); + x /= length; + y /= length; + z /= length; + + return [ + 1 - 2 * (y*y + z*z) * sq, + 2 * (x * y * sq + z * sc), + 2 * (x * z * sq - y * sc), + 0, + 2 * (x * y * sq - z * sc), + 1 - 2 * (x*x + z*z) * sq, + 2 * (y * z * sq + x * sc), + 0, + 2 * (x * z * sq + y * sc), + 2 * (y * z * sq - x * sc), + 1 - 2 * (x*x + y*y) * sq, + 0, + 0, + 0, + 0, + 1 + ]; +} + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'transform', 'none', 'none'); + assert_equals(dist, 0, 'distance of translate'); +}, 'Test distance of none and none'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'transform', 'translate(100px)', 'none'); + assert_equals(dist, 100, 'distance of translate'); +}, 'Test distance of translate function and none'); + +test(function(t) { + var target = addDiv(t); + var dist = + getDistance(target, 'transform', 'translate(100px)', 'translate(200px)'); + assert_equals(dist, 200 - 100, 'distance of translate'); +}, 'Test distance of translate functions'); + +test(function(t) { + var target = addDiv(t); + var dist = + getDistance(target, 'transform', 'translate3d(100px, 0, 50px)', 'none'); + assert_equals(dist, Math.sqrt(100 * 100 + 50 * 50), + 'distance of translate3d'); +}, 'Test distance of translate3d function and none'); + +test(function(t) { + var target = addDiv(t); + var dist = + getDistance(target, 'transform', + 'translate3d(100px, 0, 50px)', + 'translate3d(200px, 80px, 0)'); + assert_equals(dist, Math.sqrt(100 * 100 + 80 * 80 + 50 * 50), + 'distance of translate'); +}, 'Test distance of translate3d functions'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'transform', 'scale(1.5)', 'none'); + assert_equals(dist, Math.sqrt(0.5 * 0.5 + 0.5 * 0.5), 'distance of scale'); +}, 'Test distance of scale function and none'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'transform', 'scale(1.5)', 'scale(2.0)'); + assert_equals(dist, Math.sqrt(0.5 * 0.5 + 0.5 * 0.5), 'distance of scale'); +}, 'Test distance of scale functions'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'transform', + 'scale3d(1.5, 1.5, 1.5)', + 'none'); + assert_equals(dist, + Math.sqrt(0.5 * 0.5 + 0.5 * 0.5 + 0.5 * 0.5), + 'distance of scale3d'); +}, 'Test distance of scale3d function and none'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'transform', + 'scale3d(1.5, 1.5, 1.5)', + 'scale3d(2.0, 2.0, 1.0)'); + assert_equals(dist, + Math.sqrt(0.5 * 0.5 + 0.5 * 0.5 + 0.5 * 0.5), + 'distance of scale3d'); +}, 'Test distance of scale3d functions'); + +test(function(t) { + var target = addDiv(t); + var dist = + getDistance(target, 'transform', 'rotate(45deg)', 'rotate(90deg)'); + assert_approx_equals(dist, Math.PI / 2.0 - Math.PI / 4.0, EPSILON, 'distance of rotate'); +}, 'Test distance of rotate functions'); + +test(function(t) { + var target = addDiv(t); + var dist = + getDistance(target, 'transform', 'rotate(45deg)', 'none'); + assert_approx_equals(dist, Math.PI / 4.0, EPSILON, 'distance of rotate'); +}, 'Test distance of rotate function and none'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'transform', + 'rotate3d(0, 1, 0, 90deg)', + 'none'); + assert_approx_equals(dist, Math.PI / 2, EPSILON, 'distance of rotate3d'); +}, 'Test distance of rotate3d function and none'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'transform', + 'rotate3d(0, 0, 1, 90deg)', + 'rotate3d(1, 0, 0, 90deg)'); + let q1 = getQuaternion([0, 0, 1], Math.PI / 2.0); + let q2 = getQuaternion([1, 0, 0], Math.PI / 2.0); + assert_approx_equals(dist, computeRotateDistance(q1, q2), EPSILON, 'distance of rotate3d'); +}, 'Test distance of rotate3d functions'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'transform', + 'rotate3d(0, 0, 1, 90deg)', + 'rotate3d(0, 0, 0, 90deg)'); + assert_approx_equals(dist, Math.PI / 2, EPSILON, 'distance of rotate3d'); +}, 'Test distance of rotate3d functions whose direction vector cannot be ' + + 'normalized'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'transform', 'skew(1rad, 0.5rad)', 'none'); + assert_approx_equals(dist, Math.sqrt(1 * 1 + 0.5 * 0.5), EPSILON, 'distance of skew'); +}, 'Test distance of skew function and none'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'transform', + 'skew(1rad, 0.5rad)', + 'skew(-1rad, 0)'); + assert_approx_equals(dist, Math.sqrt(2 * 2 + 0.5 * 0.5), EPSILON, 'distance of skew'); +}, 'Test distance of skew functions'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'transform', + 'perspective(128px)', + 'none'); + assert_equals(dist, Infinity, 'distance of perspective'); +}, 'Test distance of perspective function and none'); + +test(function(t) { + var target = addDiv(t); + // perspective(0) is treated as perspective(inf) because perspective length + // should be greater than or equal to zero. + var dist = getDistance(target, 'transform', + 'perspective(128px)', + 'perspective(0)'); + assert_equals(dist, 128, 'distance of perspective'); +}, 'Test distance of perspective function and an invalid perspective'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'transform', + 'perspective(128px)', + 'perspective(1024px)'); + assert_equals(dist, 1024 - 128, 'distance of perspective'); +}, 'Test distance of perspective functions'); + +test(function(t) { + var target = addDiv(t); + var sin_30 = Math.sin(Math.PI / 6); + var cos_30 = Math.cos(Math.PI / 6); + // matrix => translate(100, 0) rotate(30deg). + var matrix = createMatrixFromArray([ cos_30, sin_30, + -sin_30, cos_30, + 100, 0 ]); + var dist = getDistance(target, 'transform', matrix, 'none'); + assert_approx_equals(dist, + Math.sqrt(100 * 100 + (Math.PI / 6) * (Math.PI / 6)), + EPSILON, + 'distance of matrix'); +}, 'Test distance of matrix function and none'); + +test(function(t) { + var target = addDiv(t); + var sin_30 = Math.sin(Math.PI / 6); + var cos_30 = Math.cos(Math.PI / 6); + // matrix1 => translate(100, 0) rotate(30deg). + var matrix1 = createMatrixFromArray([ cos_30, sin_30, + -sin_30, cos_30, + 100, 0 ]); + // matrix2 => translate(0, 100) scale(0.5). + var matrix2 = createMatrixFromArray([ 0.5, 0, 0, 0.5, 0, 100 ]); + var dist = getDistance(target, 'transform', matrix1, matrix2); + assert_approx_equals(dist, + Math.sqrt(100 * 100 + 100 * 100 + // translate + (Math.PI / 6) * (Math.PI / 6) + // rotate + 0.5 * 0.5 + 0.5 * 0.5), // scale + EPSILON, + 'distance of matrix'); +}, 'Test distance of matrix functions'); + +test(function(t) { + var target = addDiv(t); + var matrix = createMatrixFromArray(rotate3dToMatrix(0, 1, 0, Math.PI / 6)); + var dist = getDistance(target, 'transform', matrix, 'none'); + assert_approx_equals(dist, Math.PI / 6, EPSILON, 'distance of matrix3d'); +}, 'Test distance of matrix3d function and none'); + +test(function(t) { + var target = addDiv(t); + // matrix1 => rotate3d(0, 1, 0, 30deg). + var matrix1 = createMatrixFromArray(rotate3dToMatrix(0, 1, 0, Math.PI / 6)); + // matrix1 => translate3d(100, 0, 0) scale3d(0.5, 0.5, 0.5). + var matrix2 = createMatrixFromArray([ 0.5, 0, 0, 0, + 0, 0.5, 0, 0, + 0, 0, 0.5, 0, + 100, 0, 0, 1 ]); + var dist = getDistance(target, 'transform', matrix1, matrix2); + assert_approx_equals(dist, + Math.sqrt(100 * 100 + // translate + 0.5 * 0.5 * 3 + // scale + (Math.PI / 6) * (Math.PI / 6)), // rotate + EPSILON, + 'distance of matrix'); +}, 'Test distance of matrix3d functions'); + +test(function(t) { + var target = addDiv(t); + var cos_180 = Math.cos(Math.PI); + var sin_180 = Math.sin(Math.PI); + // matrix1 => translate3d(100px, 50px, -10px) skew(45deg). + var matrix1 = createMatrixFromArray([ 1, 0, 0, 0, + Math.tan(Math.PI/4.0), 1, 0, 0, + 0, 0, 1, 0, + 100, 50, -10, 1]); + // matrix2 => translate3d(1000px, 0, 0) rotate3d(1, 0, 0, 180deg). + var matrix2 = createMatrixFromArray([ 1, 0, 0, 0, + 0, cos_180, sin_180, 0, + 0, -sin_180, cos_180, 0, + 1000, 0, 0, 1 ]); + var dist = getDistance(target, 'transform', matrix1, matrix2); + assert_approx_equals(dist, + Math.sqrt(900 * 900 + 50 * 50 + 10 * 10 + // translate + Math.PI * Math.PI + // rotate + (Math.PI / 4) * (Math.PI / 4)), // skew angle + EPSILON, + 'distance of matrix'); +}, 'Test distance of matrix3d functions with skew factors'); + +test(function(t) { + var target = addDiv(t); + var dist = + getDistance(target, 'transform', + 'rotate(180deg) translate(1000px)', + 'rotate(360deg) translate(0px)'); + assert_approx_equals(dist, Math.sqrt(1000 * 1000 + Math.PI * Math.PI), EPSILON, + 'distance of transform lists'); +}, 'Test distance of transform lists'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'transform', + 'translate(100px) rotate(180deg)', + 'translate(50px) rotate(90deg) scale(5) skew(1rad)'); + assert_approx_equals(dist, + Math.sqrt(50 * 50 + + Math.PI / 2 * Math.PI / 2 + + 4 * 4 * 2 + + 1 * 1), + EPSILON, + 'distance of transform lists'); +}, 'Test distance of transform lists where one has extra items'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'transform', + 'translate(1000px) rotate3d(1, 0, 0, 180deg)', + 'translate(1000px) scale3d(2.5, 0.5, 1)'); + assert_equals(dist, Math.sqrt(Math.PI * Math.PI + 1.5 * 1.5 + 0.5 * 0.5), + 'distance of transform lists'); +}, 'Test distance of mismatched transform lists'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'transform', + 'translate(100px) skew(1rad)', + 'translate(1000px) rotate3d(0, 1, 0, -2rad)'); + assert_approx_equals(dist, + Math.sqrt(900 * 900 + 1 * 1 + 2 * 2), + EPSILON, + 'distance of transform lists'); +}, 'Test distance of mismatched transform lists with skew function'); + + +// Individual transforms +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'translate', '50px', 'none'); + assert_equals(dist, Math.sqrt(50 * 50), 'distance of 2D translate and none'); +}, 'Test distance of 2D translate property with none'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'translate', '10px 30px', '50px'); + assert_equals(dist, Math.sqrt(40 * 40 + 30 * 30), 'distance of 2D translate'); +}, 'Test distance of 2D translate property'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'translate', '10px 30px 50px', '50px'); + assert_equals(dist, Math.sqrt(40 * 40 + 30 * 30 + 50 * 50), + 'distance of 3D translate'); +}, 'Test distance of 3D translate property'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'scale', '2', 'none'); + assert_equals(dist, Math.sqrt(1 + 1), 'distance of 2D scale and none'); +}, 'Test distance of 2D scale property with none'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'scale', '3', '1 1'); + assert_equals(dist, Math.sqrt(2 * 2 + 2 * 2), 'distance of 2D scale'); +}, 'Test distance of 2D scale property'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'scale', '3 2 2', '1 1'); + assert_equals(dist, Math.sqrt(2 * 2 + 1 * 1 + 1 * 1), + 'distance of 3D scale'); +}, 'Test distance of 3D scale property'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'rotate', '180deg', 'none'); + assert_equals(dist, Math.PI, 'distance of 2D rotate and none'); +}, 'Test distance of 2D rotate property with none'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'rotate', '180deg', '90deg'); + assert_equals(dist, Math.PI / 2.0, 'distance of 2D rotate'); +}, 'Test distance of 2D rotate property'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'rotate', 'z 90deg', 'x 90deg'); + let q1 = getQuaternion([0, 0, 1], Math.PI / 2.0); + let q2 = getQuaternion([1, 0, 0], Math.PI / 2.0); + assert_approx_equals(dist, computeRotateDistance(q1, q2), EPSILON, + 'distance of 3D rotate'); +}, 'Test distance of 3D rotate property'); + +</script> +</html> diff --git a/dom/animation/test/mozilla/test_document_timeline_origin_time_range.html b/dom/animation/test/mozilla/test_document_timeline_origin_time_range.html new file mode 100644 index 0000000000..b2aeef8a77 --- /dev/null +++ b/dom/animation/test/mozilla/test_document_timeline_origin_time_range.html @@ -0,0 +1,32 @@ +<!doctype html> +<meta charset=utf-8> +<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'; + +// If the originTime parameter passed to the DocumentTimeline exceeds +// the range of the internal storage type (a signed 64-bit integer number +// of ticks--a platform-dependent unit) then we should throw. +// Infinity isn't allowed as an origin time value and clamping to just +// inside the allowed range will just mean we overflow elsewhere. + +test(function(t) { + assert_throws({ name: 'TypeError'}, + function() { + new DocumentTimeline({ originTime: Number.MAX_SAFE_INTEGER }); + }); +}, 'Calculated current time is positive infinity'); + +test(function(t) { + assert_throws({ name: 'TypeError'}, + function() { + new DocumentTimeline({ originTime: -1 * Number.MAX_SAFE_INTEGER }); + }); +}, 'Calculated current time is negative infinity'); + +</script> +</body> diff --git a/dom/animation/test/mozilla/test_event_listener_leaks.html b/dom/animation/test/mozilla/test_event_listener_leaks.html new file mode 100644 index 0000000000..bcfadaf9e9 --- /dev/null +++ b/dom/animation/test/mozilla/test_event_listener_leaks.html @@ -0,0 +1,43 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1450271 - Test Animation event listener leak conditions</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/dom/events/test/event_leak_utils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script class="testbody" type="text/javascript"> +// Manipulate Animation. Its important here that we create a +// listener callback from the DOM objects back to the frame's global +// in order to exercise the leak condition. +async function useAnimation(contentWindow) { + let div = contentWindow.document.createElement("div"); + contentWindow.document.body.appendChild(div); + let animation = div.animate({}, 100 * 1000); + is(animation.playState, "running", "animation should be running"); + animation.onfinish = _ => { + contentWindow.finishCount += 1; + }; +} + +async function runTest() { + try { + await checkForEventListenerLeaks("Animation", useAnimation); + } catch (e) { + ok(false, e); + } finally { + SimpleTest.finish(); + } +} + +SimpleTest.waitForExplicitFinish(); +addEventListener("load", runTest, { once: true }); +</script> +</pre> +</body> +</html> diff --git a/dom/animation/test/mozilla/test_get_animations_on_scroll_animations.html b/dom/animation/test/mozilla/test_get_animations_on_scroll_animations.html new file mode 100644 index 0000000000..fb4a21598b --- /dev/null +++ b/dom/animation/test/mozilla/test_get_animations_on_scroll_animations.html @@ -0,0 +1,71 @@ +<!doctype html> +<head> +<meta charset=utf-8> +<title>Test getAnimations() which doesn't return scroll animations</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../testcommon.js"></script> +<style> + @keyframes animWidth { + from { width: 100px; } + to { width: 200px } + } + @keyframes animTop { + to { top: 100px } + } + .fill-vh { + width: 100px; + height: 100vh; + } +</style> +</head> +<body> +<div id="log"></div> +<script> +"use strict"; + +// FIXME: getAnimations() doesn't return scroll-animations. We should fix this +// in Bug 1676795. + +test(function(t) { + const div = addDiv(t, + { style: "width: 10px; height: 100px; " + + "animation: animWidth 100s scroll(), animTop 200s;" }); + + // Sanity check to make sure the scroll animation is there. + addDiv(t, { class: "fill-vh" }); + const scroller = document.scrollingElement; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = maxScroll; + assert_equals(getComputedStyle(div).width, "200px", + "The scroll animation is there"); + + const animations = div.getAnimations(); + assert_equals(animations.length, 1, + 'getAnimations() should not include scroll animations'); + assert_equals(animations[0].animationName, "animTop", + 'getAmimations() should not return scroll animations'); +}, 'Element.getAnimation() should not include scroll animations'); + +test(function(t) { + const div = addDiv(t, + { style: "width: 10px; height: 100px; " + + "animation: animWidth 100s scroll(), animTop 100s;" }); + + // Sanity check to make sure the scroll animation is there. + addDiv(t, { class: "fill-vh" }); + const scroller = document.scrollingElement; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = maxScroll; + assert_equals(getComputedStyle(div).width, "200px", + "The scroll animation is there"); + + const animations = document.getAnimations(); + assert_equals(animations.length, 1, + 'getAnimations() should not include scroll animations'); + assert_equals(animations[0].animationName, "animTop", + 'getAmimations() should not return scroll animations'); +}, 'Document.getAnimation() should not include scroll animations'); + +</script> +</body> diff --git a/dom/animation/test/mozilla/test_hide_and_show.html b/dom/animation/test/mozilla/test_hide_and_show.html new file mode 100644 index 0000000000..f36543bb1e --- /dev/null +++ b/dom/animation/test/mozilla/test_hide_and_show.html @@ -0,0 +1,198 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../testcommon.js"></script> +<style> +@keyframes move { + 100% { + transform: translateX(100px); + } +} + +div.pseudo::before { + animation: move 0.01s; + content: 'content'; +} + +</style> +<body> +<div id="log"></div> +<script> +'use strict'; + +test(function(t) { + var div = addDiv(t, { style: 'animation: move 100s infinite' }); + assert_equals(div.getAnimations().length, 1, + 'display:initial element has animations'); + + div.style.display = 'none'; + assert_equals(div.getAnimations().length, 0, + 'display:none element has no animations'); +}, 'Animation stops playing when the element style display is set to "none"'); + +test(function(t) { + var parentElement = addDiv(t); + var div = addDiv(t, { style: 'animation: move 100s infinite' }); + parentElement.appendChild(div); + assert_equals(div.getAnimations().length, 1, + 'display:initial element has animations'); + + parentElement.style.display = 'none'; + assert_equals(div.getAnimations().length, 0, + 'Element in display:none subtree has no animations'); +}, 'Animation stops playing when its parent element style display is set ' + + 'to "none"'); + +test(function(t) { + var div = addDiv(t, { style: 'animation: move 100s infinite' }); + assert_equals(div.getAnimations().length, 1, + 'display:initial element has animations'); + + div.style.display = 'none'; + assert_equals(div.getAnimations().length, 0, + 'display:none element has no animations'); + + div.style.display = ''; + assert_equals(div.getAnimations().length, 1, + 'Element which is no longer display:none has animations ' + + 'again'); +}, 'Animation starts playing when the element gets shown from ' + + '"display:none" state'); + +test(function(t) { + var parentElement = addDiv(t); + var div = addDiv(t, { style: 'animation: move 100s infinite' }); + parentElement.appendChild(div); + assert_equals(div.getAnimations().length, 1, + 'display:initial element has animations'); + + parentElement.style.display = 'none'; + assert_equals(div.getAnimations().length, 0, + 'Element in display:none subtree has no animations'); + + parentElement.style.display = ''; + assert_equals(div.getAnimations().length, 1, + 'Element which is no longer in display:none subtree has ' + + 'animations again'); +}, 'Animation starts playing when its parent element is shown from ' + + '"display:none" state'); + +test(function(t) { + var div = addDiv(t, { style: 'animation: move 100s forwards' }); + assert_equals(div.getAnimations().length, 1, + 'display:initial element has animations'); + + var animation = div.getAnimations()[0]; + animation.finish(); + assert_equals(div.getAnimations().length, 1, + 'Element has finished animation if the animation ' + + 'fill-mode is forwards'); + + div.style.display = 'none'; + assert_equals(animation.playState, 'idle', + 'The animation.playState should be idle'); + + assert_equals(div.getAnimations().length, 0, + 'display:none element has no animations'); + + div.style.display = ''; + assert_equals(div.getAnimations().length, 1, + 'Element which is no longer display:none has animations ' + + 'again'); + assert_not_equals(div.getAnimations()[0], animation, + 'Restarted animation is a newly-generated animation'); + +}, 'Animation which has already finished starts playing when the element ' + + 'gets shown from "display:none" state'); + +test(function(t) { + var parentElement = addDiv(t); + var div = addDiv(t, { style: 'animation: move 100s forwards' }); + parentElement.appendChild(div); + assert_equals(div.getAnimations().length, 1, + 'display:initial element has animations'); + + var animation = div.getAnimations()[0]; + animation.finish(); + assert_equals(div.getAnimations().length, 1, + 'Element has finished animation if the animation ' + + 'fill-mode is forwards'); + + parentElement.style.display = 'none'; + assert_equals(animation.playState, 'idle', + 'The animation.playState should be idle'); + assert_equals(div.getAnimations().length, 0, + 'Element in display:none subtree has no animations'); + + parentElement.style.display = ''; + assert_equals(div.getAnimations().length, 1, + 'Element which is no longer in display:none subtree has ' + + 'animations again'); + + assert_not_equals(div.getAnimations()[0], animation, + 'Restarted animation is a newly-generated animation'); + +}, 'Animation with fill:forwards which has already finished starts playing ' + + 'when its parent element is shown from "display:none" state'); + +test(function(t) { + var parentElement = addDiv(t); + var div = addDiv(t, { style: 'animation: move 100s' }); + parentElement.appendChild(div); + assert_equals(div.getAnimations().length, 1, + 'display:initial element has animations'); + + var animation = div.getAnimations()[0]; + animation.finish(); + assert_equals(div.getAnimations().length, 0, + 'Element does not have finished animations'); + + parentElement.style.display = 'none'; + assert_equals(animation.playState, 'idle', + 'The animation.playState should be idle'); + assert_equals(div.getAnimations().length, 0, + 'Element in display:none subtree has no animations'); + + parentElement.style.display = ''; + assert_equals(div.getAnimations().length, 1, + 'Element which is no longer in display:none subtree has ' + + 'animations again'); + + assert_not_equals(div.getAnimations()[0], animation, + 'Restarted animation is a newly-generated animation'); + +}, 'CSS Animation which has already finished starts playing when its parent ' + + 'element is shown from "display:none" state'); + +promise_test(function(t) { + var div = addDiv(t, { 'class': 'pseudo' }); + var eventWatcher = new EventWatcher(t, div, 'animationend'); + + assert_equals(document.getAnimations().length, 1, + 'CSS animation on pseudo element'); + + return eventWatcher.wait_for('animationend').then(function() { + assert_equals(document.getAnimations().length, 0, + 'No CSS animation on pseudo element after the animation ' + + 'finished'); + + // Remove the class which generated this pseudo element. + div.classList.remove('pseudo'); + + // We need to wait for two frames to process re-framing. + // The callback of 'animationend' is processed just before rAF callbacks, + // and rAF callbacks are processed before re-framing process, so waiting for + // one rAF callback is not sufficient. + return waitForAnimationFrames(2); + }).then(function() { + // Add the class again to re-generate pseudo element. + div.classList.add('pseudo'); + assert_equals(document.getAnimations().length, 1, + 'A new CSS animation on pseudo element'); + }); +}, 'CSS animation on pseudo element restarts after the pseudo element that ' + + 'had a finished CSS animation is re-generated'); + +</script> +</body> diff --git a/dom/animation/test/mozilla/test_mainthread_synchronization_pref.html b/dom/animation/test/mozilla/test_mainthread_synchronization_pref.html new file mode 100644 index 0000000000..3653fd9536 --- /dev/null +++ b/dom/animation/test/mozilla/test_mainthread_synchronization_pref.html @@ -0,0 +1,42 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../testcommon.js"></script> +<style> +.compositable { + /* Element needs geometry to be eligible for layerization */ + width: 100px; + height: 100px; + background-color: white; +} +</style> +<body> +<div id="log"></div> +<script> +'use strict'; + +promise_test(async t => { + await SpecialPowers.pushPrefEnv({ + set: [[ 'dom.animations.mainthread-synchronization-with-geometric-animations', false ]] + }); + + const elemA = addDiv(t, { class: 'compositable' }); + const elemB = addDiv(t, { class: 'compositable' }); + + const animA = elemA.animate({ transform: [ 'translate(0px)', + 'translate(100px)' ] }, + 100 * MS_PER_SEC); + const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + + await waitForPaints(); + + assert_true(SpecialPowers.wrap(animA).isRunningOnCompositor, + 'Transform animation should not synchronize with margin-left animation ' + + 'created within the same tick with disabling the corresponding pref'); +}, 'Transform animation should not synchronize with margin-left animation ' + + 'created within the same tick with disabling the corresponding pref'); + +</script> +</body> diff --git a/dom/animation/test/mozilla/test_moz_prefixed_properties.html b/dom/animation/test/mozilla/test_moz_prefixed_properties.html new file mode 100644 index 0000000000..f65d05134d --- /dev/null +++ b/dom/animation/test/mozilla/test_moz_prefixed_properties.html @@ -0,0 +1,93 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test animations of all properties that have -moz prefix</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="../testcommon.js"></script> + <script src="../property_database.js"></script> +</head> +<body> +<div id="log"></div> +<script> +"use strict"; + +const testcases = [ + { + property: "-moz-box-align" + }, + { + property: "-moz-box-direction" + }, + { + property: "-moz-box-ordinal-group" + }, + { + property: "-moz-box-orient", + }, + { + property: "-moz-box-pack" + }, + { + property: "-moz-float-edge" + }, + { + property: "-moz-force-broken-image-icon" + }, + { + property: "-moz-orient" + }, + { + property: "-moz-osx-font-smoothing", + pref: "layout.css.osx-font-smoothing.enabled" + }, + { + property: "-moz-text-size-adjust" + }, + { + property: "-moz-user-focus" + }, + { + property: "-moz-user-input" + }, + { + property: "-moz-user-modify" + }, + { + property: "user-select" + }, + { + property: "-moz-window-dragging" + }, +]; + +testcases.forEach(testcase => { + if (testcase.pref && !IsCSSPropertyPrefEnabled(testcase.pref)) { + return; + } + + const property = gCSSProperties[testcase.property]; + const values = property.initial_values.concat(property.other_values); + values.forEach(value => { + test(function(t) { + const container = addDiv(t); + const target = document.createElement("div"); + container.appendChild(target); + + container.style[property.domProp] = value; + + const animation = + target.animate({ [property.domProp]: [value, "inherit"] }, + { duration: 1000, delay: -500 } ); + + const expectedValue = getComputedStyle(container)[property.domProp]; + assert_equals(getComputedStyle(target)[property.domProp], expectedValue, + `Computed style shoud be "${ expectedValue }"`); + }, `Test inherit value for "${ testcase.property }" ` + + `(Parent element style is "${ value }")`); + }); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/animation/test/mozilla/test_pending_animation_tracker.html b/dom/animation/test/mozilla/test_pending_animation_tracker.html new file mode 100644 index 0000000000..022efa7bcf --- /dev/null +++ b/dom/animation/test/mozilla/test_pending_animation_tracker.html @@ -0,0 +1,134 @@ +<!doctype html> +<head> +<meta charset=utf-8> +<title>Test animations in PendingAnimationTracker</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../testcommon.js"></script> +</head> +<body> +<div id="log"></div> +<script> +"use strict"; + +promise_test(function waitForLoad() { + return new Promise(resolve => { + window.addEventListener("load", resolve, { once: true }); + }); +}); + +promise_test(async t => { + // See below, but we should ensure we are in a rAF callback before proceeding + // or else we will get inconsistent results. + await waitForNextFrame(); + + const target = addDiv(t); + const anim = target.animate(null, 100 * MS_PER_SEC); + assert_true(SpecialPowers.DOMWindowUtils.isAnimationInPendingTracker(anim), + 'The animation should be tracked by tracker'); + + anim.effect = null; + await waitForNextFrame(); + + assert_false(SpecialPowers.DOMWindowUtils.isAnimationInPendingTracker(anim), + 'The animation should NOT be tracked by the tracker'); +}, 'An animation whose effect is made null while pending is subsequently' + + ' removed from the tracker'); + +test(t => { + const target = addDiv(t); + const anim = target.animate(null, 100 * MS_PER_SEC); + assert_true(SpecialPowers.DOMWindowUtils.isAnimationInPendingTracker(anim), + 'The animation should be tracked by tracker'); + + const newEffect = new KeyframeEffect(target, null); + anim.effect = newEffect; + + assert_true(SpecialPowers.DOMWindowUtils.isAnimationInPendingTracker(anim), + 'The animation should be still tracked by tracker'); +}, 'Setting another effect keeps the pending animation in the tracker'); + +test(t => { + const effect = new KeyframeEffect(null, null); + const anim = new Animation(effect); + anim.play(); + assert_false(SpecialPowers.DOMWindowUtils.isAnimationInPendingTracker(anim), + 'The orphaned animation should NOT be tracked by tracker'); + + const target = addDiv(t); + const newEffect = new KeyframeEffect(target, null); + anim.effect = newEffect; + + assert_true(SpecialPowers.DOMWindowUtils.isAnimationInPendingTracker(anim), + 'The animation should be now tracked by tracker'); +}, 'Setting effect having target element starts being tracked by the ' + + 'tracker'); + +test(t => { + const target = addDiv(t); + const anim = target.animate(null, 100 * MS_PER_SEC); + assert_true(SpecialPowers.DOMWindowUtils.isAnimationInPendingTracker(anim), + 'The animation should be tracked by tracker'); + + anim.cancel(); + + assert_false(SpecialPowers.DOMWindowUtils.isAnimationInPendingTracker(anim), + 'The animation should NOT be tracked by the tracker'); +}, 'Calling cancel() removes the animation from the tracker'); + +promise_test(async t => { + // Before proceeding this test, make sure following code is _NOT_ processed + // between paint and refresh driver's tick. Otherwise, waitForNextFrame below + // doesn't ensure that a paint process happens which means that there is + // no chance to call TriggerPendingAnimationsOnNextTick to discard the + // animation from the pending animation tracker. + await waitForNextFrame(); + + const target = addDiv(t); + const anim = target.animate(null, 100 * MS_PER_SEC); + + assert_true(SpecialPowers.DOMWindowUtils.isAnimationInPendingTracker(anim), + 'The animation should be tracked by tracker'); + + target.remove(); + + assert_true(SpecialPowers.DOMWindowUtils.isAnimationInPendingTracker(anim), + 'The animation is still being tracked by the tracker'); + + await waitForNextFrame(); + assert_false(SpecialPowers.DOMWindowUtils.isAnimationInPendingTracker(anim), + 'The animation should NOT be tracked by the tracker in the ' + + 'next frame'); +}, 'Removing target element from the document removes the animation from ' + + 'the tracker in the next tick'); + +test(t => { + const target = addDiv(t); + const anotherTarget = addDiv(t); + const anim = target.animate(null, 100 * MS_PER_SEC); + assert_true(SpecialPowers.DOMWindowUtils.isAnimationInPendingTracker(anim), + 'The animation should be tracked by tracker'); + + anim.effect.target = anotherTarget; + + assert_true(SpecialPowers.DOMWindowUtils.isAnimationInPendingTracker(anim), + 'The animation should be still tracked by tracker'); +}, 'Setting another target keeps the pending animation in the tracker'); + +test(t => { + const effect = new KeyframeEffect(null, null); + const anim = new Animation(effect); + anim.play(); + assert_false(SpecialPowers.DOMWindowUtils.isAnimationInPendingTracker(anim), + 'The orphaned animation should NOT be tracked by tracker'); + + const target = addDiv(t); + anim.effect.target = target; + + assert_true(SpecialPowers.DOMWindowUtils.isAnimationInPendingTracker(anim), + 'The animation should be now tracked by tracker'); +}, 'Setting target element to the orphaned animation starts being tracked ' + + 'by the tracker'); + +</script> +</body> diff --git a/dom/animation/test/mozilla/test_restyles.html b/dom/animation/test/mozilla/test_restyles.html new file mode 100644 index 0000000000..bc1ab70c74 --- /dev/null +++ b/dom/animation/test/mozilla/test_restyles.html @@ -0,0 +1,22 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<div id='log'></div> +<script> +'use strict'; +SimpleTest.waitForExplicitFinish(); +SimpleTest.expectAssertions(0, 1); // bug 1332970 +SpecialPowers.pushPrefEnv( + { + set: [ + ['layout.reflow.synthMouseMove', false], + ['privacy.reduceTimerPrecision', false], + ], + }, + function() { + window.open('file_restyles.html'); + } +); +</script> +</html> diff --git a/dom/animation/test/mozilla/test_restyling_xhr_doc.html b/dom/animation/test/mozilla/test_restyling_xhr_doc.html new file mode 100644 index 0000000000..67b6ac8845 --- /dev/null +++ b/dom/animation/test/mozilla/test_restyling_xhr_doc.html @@ -0,0 +1,106 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; + +// This test supplements the web-platform-tests in: +// +// web-animations/interfaces/Animatable/animate-no-browsing-context.html +// +// Specifically, it covers the case where we have a running animation +// targetting an element in a document without a browsing context. +// +// Currently the behavior in this case is not well-defined. For example, +// if we were to simply take an element from such a document, and do: +// +// const xdoc = xhr.responseXML; +// const div = xdoc.getElementById('test'); +// div.style.opacity = '0'; +// alert(getComputedStyle(div).opacity); +// +// We'd get '0' in Firefox and Edge, but an empty string in Chrome. +// +// However, if instead of using the style attribute, we set style in a <style> +// element in *either* the document we're calling from *or* the XHR doc and +// do the same we get '1' in Firefox and Edge, but an empty string in Chrome. +// +// That is, no browser appears to apply styles to elements in a document without +// a browsing context unless the styles are defined using the style attribute, +// and even then Chrome does not. +// +// There is some prose in CSSOM which says, +// +// Note: This means that even if obj is in a different document (e.g. one +// fetched via XMLHttpRequest) it will still use the style rules associated +// with the document that is associated with the global object on which +// getComputedStyle() was invoked to compute the CSS declaration block.[1] +// +// However, this text has been around since at least 2013 and does not appear +// to be implemented. +// +// As a result, it's not really possible to write a cross-browser test for the +// behavior for animations in this context since it's not clear what the result +// should be. That said, we still want to exercise this particular code path so +// we make this case a Mozilla-specific test. The other similar tests cases for +// which the behavior is well-defined are covered by web-platform-tests. +// +// [1] https://drafts.csswg.org/cssom/#extensions-to-the-window-interface + +function getXHRDoc(t) { + return new Promise(resolve => { + const xhr = new XMLHttpRequest(); + xhr.open('GET', 'xhr_doc.html'); + 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 => { + let anim; + return getXHRDoc(t).then(xhrdoc => { + const div = xhrdoc.getElementById('test'); + anim = div.animate({ opacity: [ 0, 1 ] }, 1000); + // Give the animation an active timeline and kick-start it. + anim.timeline = document.timeline; + anim.startTime = document.timeline.currentTime; + assert_equals(anim.playState, 'running', + 'The animation should be running'); + // Gecko currently skips applying animation styles to elements in documents + // without browsing contexts. + assert_not_equals(getComputedStyle(div).opacity, '0', + 'Style should NOT be updated'); + }); +}, 'Forcing an animation targetting an element in a document without a' + + ' browsing context to play does not cause style to update'); + +promise_test(t => { + let anim; + return getXHRDoc(t).then(xhrdoc => { + const div = addDiv(t); + anim = div.animate({ opacity: [ 0, 1 ] }, 1000); + assert_equals(getComputedStyle(div).opacity, '0', + 'Style should be updated'); + // Trigger an animation restyle to be queued + anim.currentTime = 0.1; + // Adopt node into XHR doc + xhrdoc.body.appendChild(div); + // We should skip applying animation styles to elements in documents + // without a pres shell. + assert_equals(getComputedStyle(div).opacity, '1', + 'Style should NOT be updated'); + }); +}, 'Moving an element with a pending animation restyle to a document without' + + ' a browsing context resets animation style'); + +</script> diff --git a/dom/animation/test/mozilla/test_set_easing.html b/dom/animation/test/mozilla/test_set_easing.html new file mode 100644 index 0000000000..55c77f0e8f --- /dev/null +++ b/dom/animation/test/mozilla/test_set_easing.html @@ -0,0 +1,36 @@ +<!doctype html> +<head> +<meta charset=utf-8> +<title>Test setting easing in sandbox</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../testcommon.js"></script> +</head> +<body> +<div id="log"></div> +<script> +"use strict"; + +test(function(t) { + const div = document.createElement("div"); + document.body.appendChild(div); + div.animate({ opacity: [0, 1] }, 100000 ); + + const contentScript = function() { + try { + document.getAnimations()[0].effect.updateTiming({ easing: 'linear' }); + assert_true(true, 'Setting easing should not throw in sandbox'); + } catch (e) { + assert_unreached('Setting easing threw ' + e); + } + }; + + const sandbox = new SpecialPowers.Cu.Sandbox(window); + sandbox.importFunction(document, "document"); + sandbox.importFunction(assert_true, "assert_true"); + sandbox.importFunction(assert_unreached, "assert_unreached"); + SpecialPowers.Cu.evalInSandbox(`(${contentScript.toString()})()`, sandbox); +}, 'Setting easing should not throw any exceptions in sandbox'); + +</script> +</body> diff --git a/dom/animation/test/mozilla/test_style_after_finished_on_compositor.html b/dom/animation/test/mozilla/test_style_after_finished_on_compositor.html new file mode 100644 index 0000000000..bccae9e0d5 --- /dev/null +++ b/dom/animation/test/mozilla/test_style_after_finished_on_compositor.html @@ -0,0 +1,138 @@ +<!doctype html> +<head> +<meta charset=utf-8> +<title>Test for styles after finished on the compositor</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../testcommon.js"></script> +<style> +.compositor { + /* Element needs geometry to be eligible for layerization */ + width: 100px; + height: 100px; + background-color: green; +} +</style> +</head> +<body> +<div id="log"></div> +<script> +"use strict"; + +promise_test(async t => { + const div = addDiv(t, { 'class': 'compositor' }); + const anim = div.animate([ { offset: 0, opacity: 1 }, + { offset: 1, opacity: 0 } ], + { delay: 10, + duration: 100 }); + + await anim.finished; + + await waitForNextFrame(); + + const opacity = SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity'); + + assert_equals(opacity, '', 'No opacity animation runs on the compositor'); +}, 'Opacity animation with positive delay is removed from compositor when ' + + 'finished'); + +promise_test(async t => { + const div = addDiv(t, { 'class': 'compositor' }); + const anim = div.animate([ { offset: 0, opacity: 1 }, + { offset: 0.9, opacity: 1 }, + { offset: 1, opacity: 0 } ], + { duration: 100 }); + + await anim.finished; + + await waitForNextFrame(); + + const opacity = SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity'); + + assert_equals(opacity, '', 'No opacity animation runs on the compositor'); +}, 'Opacity animation initially opacity: 1 is removed from compositor when ' + + 'finished'); + +promise_test(async t => { + const div = addDiv(t, { 'class': 'compositor' }); + const anim = div.animate([ { offset: 0, opacity: 0 }, + { offset: 0.5, opacity: 1 }, + { offset: 0.51, opacity: 1 }, + { offset: 1, opacity: 0 } ], + { delay: 10, duration: 100 }); + + await waitForAnimationFrames(2); + + // Setting the current time at the offset generating opacity: 1. + anim.currentTime = 60; + + await anim.finished; + + await waitForNextFrame(); + + const opacity = SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity'); + + assert_equals(opacity, '', 'No opacity animation runs on the compositor'); +}, 'Opacity animation is removed from compositor even when it only visits ' + + 'exactly the point where the opacity: 1 value was set'); + +promise_test(async t => { + const div = addDiv(t, { 'class': 'compositor' }); + const anim = div.animate([ { offset: 0, transform: 'none' }, + { offset: 1, transform: 'translateX(100px)' } ], + { delay: 10, + duration: 100 }); + + await anim.finished; + + await waitForNextFrame(); + + const transform = SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform'); + + assert_equals(transform, '', 'No transform animation runs on the compositor'); +}, 'Transform animation with positive delay is removed from compositor when ' + + 'finished'); + +promise_test(async t => { + const div = addDiv(t, { 'class': 'compositor' }); + const anim = div.animate([ { offset: 0, transform: 'none' }, + { offset: 0.9, transform: 'none' }, + { offset: 1, transform: 'translateX(100px)' } ], + { duration: 100 }); + + await anim.finished; + + await waitForNextFrame(); + + const transform = SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform'); + + assert_equals(transform, '', 'No transform animation runs on the compositor'); +}, 'Transform animation initially transform: none is removed from compositor ' + + 'when finished'); + + +promise_test(async t => { + const div = addDiv(t, { 'class': 'compositor' }); + const anim = div.animate([ { offset: 0, transform: 'translateX(100px)' }, + { offset: 0.5, transform: 'none' }, + { offset: 0.9, transform: 'none' }, + { offset: 1, transform: 'translateX(100px)' } ], + { delay: 10, duration: 100 }); + + await waitForAnimationFrames(2); + + // Setting the current time at the offset generating transform: none. + anim.currentTime = 60; + + await anim.finished; + + await waitForNextFrame(); + + const transform = SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform'); + + assert_equals(transform, '', 'No transform animation runs on the compositor'); +}, 'Transform animation is removed from compositor even when it only visits ' + + 'exactly the point where the transform: none value was set'); + +</script> +</body> diff --git a/dom/animation/test/mozilla/test_transform_limits.html b/dom/animation/test/mozilla/test_transform_limits.html new file mode 100644 index 0000000000..92d1b7e1ec --- /dev/null +++ b/dom/animation/test/mozilla/test_transform_limits.html @@ -0,0 +1,56 @@ +<!doctype html> +<meta charset=utf-8> +<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'; + +// We clamp +infinity or -inifinity value in floating point to +// maximum floating point value or -maximum floating point value. +const MAX_FLOAT = 3.40282e+38; + +test(function(t) { + var div = addDiv(t); + div.style = "width: 1px; height: 1px;"; + var anim = div.animate([ { transform: 'scale(1)' }, + { transform: 'scale(3.5e+38)'}, + { transform: 'scale(3)' } ], 100 * MS_PER_SEC); + + anim.pause(); + anim.currentTime = 50 * MS_PER_SEC; + assert_equals(getComputedStyle(div).transform, + 'matrix(' + MAX_FLOAT + ', 0, 0, ' + MAX_FLOAT + ', 0, 0)'); +}, 'Test that the parameter of transform scale is clamped' ); + +test(function(t) { + var div = addDiv(t); + div.style = "width: 1px; height: 1px;"; + var anim = div.animate([ { transform: 'translate(1px)' }, + { transform: 'translate(3.5e+38px)'}, + { transform: 'translate(3px)' } ], 100 * MS_PER_SEC); + + anim.pause(); + anim.currentTime = 50 * MS_PER_SEC; + assert_equals(getComputedStyle(div).transform, + 'matrix(1, 0, 0, 1, ' + MAX_FLOAT + ', 0)'); +}, 'Test that the parameter of transform translate is clamped' ); + +test(function(t) { + var div = addDiv(t); + div.style = "width: 1px; height: 1px;"; + var anim = div.animate([ { transform: 'matrix(0.5, 0, 0, 0.5, 0, 0)' }, + { transform: 'matrix(2, 0, 0, 2, 3.5e+38, 0)'}, + { transform: 'matrix(0, 2, 0, -2, 0, 0)' } ], + 100 * MS_PER_SEC); + + anim.pause(); + anim.currentTime = 50 * MS_PER_SEC; + assert_equals(getComputedStyle(div).transform, + 'matrix(2, 0, 0, 2, ' + MAX_FLOAT + ', 0)'); +}, 'Test that the parameter of transform matrix is clamped' ); + +</script> +</body> diff --git a/dom/animation/test/mozilla/test_transition_finish_on_compositor.html b/dom/animation/test/mozilla/test_transition_finish_on_compositor.html new file mode 100644 index 0000000000..46a154b9af --- /dev/null +++ b/dom/animation/test/mozilla/test_transition_finish_on_compositor.html @@ -0,0 +1,22 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +// This test appears like it might get racey and cause a timeout with too low of a +// precision, so we hardcode it to something reasonable. +SpecialPowers.pushPrefEnv( + { + set: [ + ['privacy.reduceTimerPrecision', true], + ['privacy.resistFingerprinting.reduceTimerPrecision.microseconds', 2000], + ], + }, + function() { + window.open('file_transition_finish_on_compositor.html'); + } +); +</script> diff --git a/dom/animation/test/mozilla/test_underlying_discrete_value.html b/dom/animation/test/mozilla/test_underlying_discrete_value.html new file mode 100644 index 0000000000..3961305df3 --- /dev/null +++ b/dom/animation/test/mozilla/test_underlying_discrete_value.html @@ -0,0 +1,188 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../testcommon.js"></script> +<body> +<div id="log"></div> +<script> +"use strict"; + +// Tests that we correctly extract the underlying value when the animation +// type is 'discrete'. +const discreteTests = [ + { + stylesheet: { + "@keyframes keyframes": + "from { align-content: flex-start; } to { align-content: flex-end; } " + }, + expectedKeyframes: [ + { computedOffset: 0, alignContent: "flex-start" }, + { computedOffset: 1, alignContent: "flex-end" } + ], + explanation: "Test for fully-specified keyframes" + }, + { + stylesheet: { + "@keyframes keyframes": "from { align-content: flex-start; }" + }, + expectedKeyframes: [ + { computedOffset: 0, alignContent: "flex-start" }, + { computedOffset: 1, alignContent: "normal" } + ], + explanation: "Test for 0% keyframe only", + }, + { + stylesheet: { + "@keyframes keyframes": "to { align-content: flex-end; }" + }, + expectedKeyframes: [ + { computedOffset: 0, alignContent: "normal" }, + { computedOffset: 1, alignContent: "flex-end" } + ], + explanation: "Test for 100% keyframe only", + }, + { + stylesheet: { + "@keyframes keyframes": "50% { align-content: center; }", + "#target": "align-content: space-between;" + }, + expectedKeyframes: [ + { computedOffset: 0, alignContent: "space-between" }, + { computedOffset: 0.5, alignContent: "center" }, + { computedOffset: 1, alignContent: "space-between" } + ], + explanation: "Test for no 0%/100% keyframes " + + "and specified style on target element" + }, + { + stylesheet: { + "@keyframes keyframes": "50% { align-content: center; }" + }, + attributes: { + style: "align-content: space-between" + }, + expectedKeyframes: [ + { computedOffset: 0, alignContent: "space-between" }, + { computedOffset: 0.5, alignContent: "center" }, + { computedOffset: 1, alignContent: "space-between" } + ], + explanation: "Test for no 0%/100% keyframes " + + "and specified style on target element using style attribute" + }, + { + stylesheet: { + "@keyframes keyframes": "50% { align-content: center; }", + "#target": "align-content: inherit;" + }, + expectedKeyframes: [ + { computedOffset: 0, alignContent: "normal" }, + { computedOffset: 0.5, alignContent: "center" }, + { computedOffset: 1, alignContent: "normal" } + ], + explanation: "Test for no 0%/100% keyframes " + + "and 'inherit' specified on target element", + }, + { + stylesheet: { + "@keyframes keyframes": "50% { align-content: center; }", + ".target": "align-content: space-between;" + }, + attributes: { + class: "target" + }, + expectedKeyframes: [ + { computedOffset: 0, alignContent: "space-between" }, + { computedOffset: 0.5, alignContent: "center" }, + { computedOffset: 1, alignContent: "space-between" } + ], + explanation: "Test for no 0%/100% keyframes " + + "and specified style on target element using class selector" + }, + { + stylesheet: { + "@keyframes keyframes": "50% { align-content: center; }", + "div": "align-content: space-between;" + }, + expectedKeyframes: [ + { computedOffset: 0, alignContent: "space-between" }, + { computedOffset: 0.5, alignContent: "center" }, + { computedOffset: 1, alignContent: "space-between" } + ], + explanation: "Test for no 0%/100% keyframes " + + "and specified style on target element using type selector" + }, + { + stylesheet: { + "@keyframes keyframes": "50% { align-content: center; }", + "div": "align-content: space-between;", + ".target": "align-content: flex-start;", + "#target": "align-content: flex-end;" + }, + attributes: { + class: "target" + }, + expectedKeyframes: [ + { computedOffset: 0, alignContent: "flex-end" }, + { computedOffset: 0.5, alignContent: "center" }, + { computedOffset: 1, alignContent: "flex-end" } + ], + explanation: "Test for no 0%/100% keyframes " + + "and specified style on target element " + + "using ID selector that overrides class selector" + }, + { + stylesheet: { + "@keyframes keyframes": "50% { align-content: center; }", + "div": "align-content: space-between !important;", + ".target": "align-content: flex-start;", + "#target": "align-content: flex-end;" + }, + attributes: { + class: "target" + }, + expectedKeyframes: [ + { computedOffset: 0, alignContent: "space-between" }, + { computedOffset: 0.5, alignContent: "center" }, + { computedOffset: 1, alignContent: "space-between" } + ], + explanation: "Test for no 0%/100% keyframes " + + "and specified style on target element " + + "using important type selector that overrides other rules" + }, +]; + +discreteTests.forEach(testcase => { + test(t => { + if (testcase.skip) { + return; + } + addStyle(t, testcase.stylesheet); + + const div = addDiv(t, { "id": "target" }); + if (testcase.attributes) { + for (let attributeName in testcase.attributes) { + div.setAttribute(attributeName, testcase.attributes[attributeName]); + } + } + div.style.animation = "keyframes 100s"; + + const keyframes = div.getAnimations()[0].effect.getKeyframes(); + const expectedKeyframes = testcase.expectedKeyframes; + assert_equals(keyframes.length, expectedKeyframes.length, + `keyframes.length should be ${ expectedKeyframes.length }`); + + keyframes.forEach((keyframe, index) => { + const expectedKeyframe = expectedKeyframes[index]; + assert_equals(keyframe.computedOffset, expectedKeyframe.computedOffset, + `computedOffset of keyframes[${ index }] should be ` + + `${ expectedKeyframe.computedOffset }`); + assert_equals(keyframe.alignContent, expectedKeyframe.alignContent, + `alignContent of keyframes[${ index }] should be ` + + `${ expectedKeyframe.alignContent }`); + }); + }, testcase.explanation); +}); + +</script> +</body> diff --git a/dom/animation/test/mozilla/test_unstyled.html b/dom/animation/test/mozilla/test_unstyled.html new file mode 100644 index 0000000000..4724979c11 --- /dev/null +++ b/dom/animation/test/mozilla/test_unstyled.html @@ -0,0 +1,54 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../testcommon.js"></script> +<style> +div.pseudo::before { + animation: animation 1s; + content: 'content'; +} +@keyframes animation { + to { opacity: 0 } +} +</style> +<body> +<div id="log"></div> +<script> +'use strict'; + +// Tests for cases where we may not have style data for an element + +promise_test(async t => { + // Get a CSSPseudoElement + const div = addDiv(t, { class: 'pseudo' }); + const cssAnim = document.getAnimations()[0]; + const pseudoElem = cssAnim.effect.target; + + // Drop pseudo from styles and flush styles + div.classList.remove('pseudo'); + getComputedStyle(div, '::before').content; + + // Try animating the pseudo's content attribute + const contentAnim = pseudoElem.animate( + { content: ['none', '"content"'] }, + { duration: 100 * MS_PER_SEC, fill: 'both' } + ); + + // Check that the initial value is as expected + await contentAnim.ready; + assert_equals(getComputedStyle(div, '::before').content, 'none'); + + contentAnim.finish(); + + // Animating an obsolete pseudo element should NOT cause the pseudo element + // to be re-generated. That behavior might change in which case this test + // will need to be updated. The most important part of this test, however, + // is simply checking that nothing explodes if we try to animate such a + // pseudo element. + + assert_equals(getComputedStyle(div, '::before').content, 'none'); +}, 'Animation on an obsolete pseudo element produces expected results'); + +</script> +</body> diff --git a/dom/animation/test/mozilla/xhr_doc.html b/dom/animation/test/mozilla/xhr_doc.html new file mode 100644 index 0000000000..b9fa57e3f5 --- /dev/null +++ b/dom/animation/test/mozilla/xhr_doc.html @@ -0,0 +1,2 @@ +<!doctype html> +<div id=test></div> |