import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo

This commit is contained in:
Roy Tam 2018-01-19 03:59:58 +08:00
commit dcd9973243
150858 changed files with 23884658 additions and 0 deletions

View file

@ -0,0 +1,19 @@
<!doctype html>
<html>
<head>
<meta charset=utf-8>
<script>
Element.prototype.animate = function() {
throw 'Called animate() as defined in content document';
}
// Bug 1211783: Use KeyframeEffect (not KeyframeEffectReadOnly) here
for (var obj of [KeyframeEffectReadOnly, Animation]) {
obj = function() {
throw 'Called overridden ' + String(obj) + ' constructor';
};
}
</script>
<body>
<div id="target"></div>
</body>
</html>

View file

@ -0,0 +1,31 @@
<!doctype html>
<head>
<meta charset=utf-8>
<script type="application/javascript" src="../testharness.js"></script>
<script type="application/javascript" src="../testharnessreport.js"></script>
<script type="application/javascript" src="../testcommon.js"></script>
</head>
<body>
<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1045994"
target="_blank">Mozilla Bug 1045994</a>
<div id="log"></div>
<iframe id="iframe"
src="http://example.org/tests/dom/animation/test/chrome/file_animate_xrays.html"></iframe>
<script>
'use strict';
var win = document.getElementById('iframe').contentWindow;
async_test(function(t) {
window.addEventListener('load', t.step_func(function() {
var target = win.document.getElementById('target');
var anim = target.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
// In the x-ray case, the frames object will be given an opaque wrapper
// so it won't be possible to fetch any frames from it.
assert_equals(anim.effect.getKeyframes().length, 0);
t.done();
}));
}, 'Calling animate() across x-rays');
</script>
</body>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,957 @@
<!doctype html>
<head>
<meta charset=utf-8>
<title>Bug 1196114 - Test metadata related to which animation properties
are running on the compositor</title>
<script type="application/javascript" src="../testharness.js"></script>
<script type="application/javascript" src="../testharnessreport.js"></script>
<script type="application/javascript" src="../testcommon.js"></script>
<style>
.compositable {
/* Element needs geometry to be eligible for layerization */
width: 100px;
height: 100px;
background-color: white;
}
@keyframes fade {
from { opacity: 1 }
to { opacity: 0 }
}
</style>
</head>
<body>
<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1196114"
target="_blank">Mozilla Bug 1196114</a>
<div id="log"></div>
<script>
'use strict';
// This is used for obtaining localized strings.
var gStringBundle;
W3CTest.runner.requestLongerTimeout(2);
SpecialPowers.pushPrefEnv({ "set": [
["general.useragent.locale", "en-US"],
// Need to set devPixelsPerPx explicitly to gain
// consistent pixel values in warning messages
// regardless of platform DPIs.
["layout.css.devPixelsPerPx", 1],
] },
start);
function compare_property_state(a, b) {
if (a.property > b.property) {
return -1;
} else if (a.property < b.property) {
return 1;
}
if (a.runningOnCompositor != b.runningOnCompositor) {
return a.runningOnCompositor ? 1 : -1;
}
return a.warning > b.warning ? -1 : 1;
}
function assert_animation_property_state_equals(actual, expected) {
assert_equals(actual.length, expected.length, 'Number of properties');
var sortedActual = actual.sort(compare_property_state);
var sortedExpected = expected.sort(compare_property_state);
for (var i = 0; i < sortedActual.length; i++) {
assert_equals(sortedActual[i].property,
sortedExpected[i].property,
'CSS property name should match');
assert_equals(sortedActual[i].runningOnCompositor,
sortedExpected[i].runningOnCompositor,
'runningOnCompositor property should match');
if (sortedExpected[i].warning instanceof RegExp) {
assert_regexp_match(sortedActual[i].warning,
sortedExpected[i].warning,
'warning message should match');
} else if (sortedExpected[i].warning) {
assert_equals(sortedActual[i].warning,
gStringBundle.GetStringFromName(sortedExpected[i].warning),
'warning message should match');
}
}
}
// Check that the animation is running on compositor and
// warning property is not set for the CSS property regardless
// expected values.
function assert_property_state_on_compositor(actual, expected) {
assert_equals(actual.length, expected.length);
var sortedActual = actual.sort(compare_property_state);
var sortedExpected = expected.sort(compare_property_state);
for (var i = 0; i < sortedActual.length; i++) {
assert_equals(sortedActual[i].property,
sortedExpected[i].property,
'CSS property name should match');
assert_true(sortedActual[i].runningOnCompositor,
'runningOnCompositor property should be true on ' +
sortedActual[i].property);
assert_not_exists(sortedActual[i], 'warning',
'warning property should not be set');
}
}
var gAnimationsTests = [
{
desc: 'animations on compositor',
frames: {
opacity: [0, 1]
},
expected: [
{
property: 'opacity',
runningOnCompositor: true
}
]
},
{
desc: 'animations on main thread',
frames: {
backgroundColor: ['white', 'red']
},
expected: [
{
property: 'background-color',
runningOnCompositor: false
}
]
},
{
desc: 'animations on both threads',
frames: {
backgroundColor: ['white', 'red'],
transform: ['translate(0px)', 'translate(100px)']
},
expected: [
{
property: 'background-color',
runningOnCompositor: false
},
{
property: 'transform',
runningOnCompositor: true
}
]
},
{
desc: 'two animation properties on compositor thread',
frames: {
opacity: [0, 1],
transform: ['translate(0px)', 'translate(100px)']
},
expected: [
{
property: 'opacity',
runningOnCompositor: true
},
{
property: 'transform',
runningOnCompositor: true
}
]
},
{
desc: 'opacity on compositor with animation of geometric properties',
frames: {
width: ['100px', '200px'],
opacity: [0, 1]
},
expected: [
{
property: 'width',
runningOnCompositor: false
},
{
property: 'opacity',
runningOnCompositor: true
}
]
},
];
// Test cases that check results of adding/removing a 'width' property on the
// same animation object.
var gAnimationWithGeometricKeyframeTests = [
{
desc: 'transform',
frames: {
transform: ['translate(0px)', 'translate(100px)']
},
expected: {
withoutGeometric: [
{
property: 'transform',
runningOnCompositor: true
}
],
withGeometric: [
{
property: 'width',
runningOnCompositor: false
},
{
property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
}
]
}
},
{
desc: 'opacity and transform',
frames: {
opacity: [0, 1],
transform: ['translate(0px)', 'translate(100px)']
},
expected: {
withoutGeometric: [
{
property: 'opacity',
runningOnCompositor: true
},
{
property: 'transform',
runningOnCompositor: true
}
],
withGeometric: [
{
property: 'width',
runningOnCompositor: false
},
{
property: 'opacity',
runningOnCompositor: true
},
{
property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
}
]
}
},
];
// Performance warning tests that set and clear a style property.
var gPerformanceWarningTestsStyle = [
{
desc: 'preserve-3d transform',
frames: {
transform: ['translate(0px)', 'translate(100px)']
},
style: 'transform-style: preserve-3d',
expected: [
{
property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningTransformPreserve3D'
}
]
},
{
desc: 'transform with backface-visibility:hidden',
frames: {
transform: ['translate(0px)', 'translate(100px)']
},
style: 'backface-visibility: hidden;',
expected: [
{
property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden'
}
]
},
{
desc: 'opacity and transform with preserve-3d',
frames: {
opacity: [0, 1],
transform: ['translate(0px)', 'translate(100px)']
},
style: 'transform-style: preserve-3d',
expected: [
{
property: 'opacity',
runningOnCompositor: true
},
{
property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningTransformPreserve3D'
}
]
},
{
desc: 'opacity and transform with backface-visibility:hidden',
frames: {
opacity: [0, 1],
transform: ['translate(0px)', 'translate(100px)']
},
style: 'backface-visibility: hidden;',
expected: [
{
property: 'opacity',
runningOnCompositor: true
},
{
property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden'
}
]
},
];
// Performance warning tests that set and clear the id property
var gPerformanceWarningTestsId= [
{
desc: 'moz-element referencing a transform',
frames: {
transform: ['translate(0px)', 'translate(100px)']
},
id: 'transformed',
createelement: 'width:100px; height:100px; background: -moz-element(#transformed)',
expected: [
{
property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningHasRenderingObserver'
}
]
},
];
var gMultipleAsyncAnimationsTests = [
{
desc: 'opacity and transform with preserve-3d',
style: 'transform-style: preserve-3d',
animations: [
{
frames: {
transform: ['translate(0px)', 'translate(100px)']
},
expected: [
{
property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningTransformPreserve3D'
}
]
},
{
frames: {
opacity: [0, 1]
},
expected: [
{
property: 'opacity',
runningOnCompositor: true,
}
]
}
],
},
{
desc: 'opacity and transform with backface-visibility:hidden',
style: 'backface-visibility: hidden;',
animations: [
{
frames: {
transform: ['translate(0px)', 'translate(100px)']
},
expected: [
{
property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden'
}
]
},
{
frames: {
opacity: [0, 1]
},
expected: [
{
property: 'opacity',
runningOnCompositor: true,
}
]
}
],
},
];
// Test cases that check results of adding/removing a 'width' keyframe on the
// same animation object, where multiple animation objects belong to the same
// element.
// The 'width' property is added to animations[1].
var gMultipleAsyncAnimationsWithGeometricKeyframeTests = [
{
desc: 'transform and opacity with geometric keyframes',
animations: [
{
frames: {
transform: ['translate(0px)', 'translate(100px)']
},
expected: {
withoutGeometric: [
{
property: 'transform',
runningOnCompositor: true
}
],
withGeometric: [
{
property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
}
]
}
},
{
frames: {
opacity: [0, 1]
},
expected: {
withoutGeometric: [
{
property: 'opacity',
runningOnCompositor: true,
}
],
withGeometric: [
{
property: 'width',
runningOnCompositor: false,
},
{
property: 'opacity',
runningOnCompositor: true,
}
]
}
}
],
},
{
desc: 'opacity and transform with geometric keyframes',
animations: [
{
frames: {
opacity: [0, 1]
},
expected: {
withoutGeometric: [
{
property: 'opacity',
runningOnCompositor: true,
}
],
withGeometric: [
{
property: 'opacity',
runningOnCompositor: true,
}
]
}
},
{
frames: {
transform: ['translate(0px)', 'translate(100px)']
},
expected: {
withoutGeometric: [
{
property: 'transform',
runningOnCompositor: true
}
],
withGeometric: [
{
property: 'width',
runningOnCompositor: false,
},
{
property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
}
]
}
}
]
},
];
// Test cases that check results of adding/removing 'width' animation on the
// same element which has async animations.
var gMultipleAsyncAnimationsWithGeometricAnimationTests = [
{
desc: 'transform',
animations: [
{
frames: {
transform: ['translate(0px)', 'translate(100px)']
},
expected: [
{
property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
}
]
},
]
},
{
desc: 'opacity',
animations: [
{
frames: {
opacity: [0, 1]
},
expected: [
{
property: 'opacity',
runningOnCompositor: true
}
]
},
]
},
{
desc: 'opacity and transform',
animations: [
{
frames: {
transform: ['translate(0px)', 'translate(100px)']
},
expected: [
{
property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
}
]
},
{
frames: {
opacity: [0, 1]
},
expected: [
{
property: 'opacity',
runningOnCompositor: true,
}
]
}
],
},
];
var gAnimationsOnTooSmallElementTests = [
{
desc: 'opacity on too small element',
frames: {
opacity: [0, 1]
},
style: { style: 'width: 8px; height: 8px; background-color: red;' +
// We need to set transform here to try creating an
// individual frame for this opacity element.
// Without this, this small element is created on the same
// nsIFrame of mochitest iframe, i.e. the document which are
// running this test, as a result the layer corresponding
// to the frame is sent to compositor.
'transform: translateX(100px);' },
expected: [
{
property: 'opacity',
runningOnCompositor: false,
warning: /Animation cannot be run on the compositor because frame size \(8, 8\) is smaller than \(16, 16\)/
}
]
},
{
desc: 'transform on too small element',
frames: {
transform: ['translate(0px)', 'translate(100px)']
},
style: { style: 'width: 8px; height: 8px; background-color: red;' },
expected: [
{
property: 'transform',
runningOnCompositor: false,
warning: /Animation cannot be run on the compositor because frame size \(8, 8\) is smaller than \(16, 16\)/
}
]
},
];
function start() {
var bundleService = SpecialPowers.Cc['@mozilla.org/intl/stringbundle;1']
.getService(SpecialPowers.Ci.nsIStringBundleService);
gStringBundle = bundleService
.createBundle("chrome://global/locale/layout_errors.properties");
gAnimationsTests.forEach(function(subtest) {
promise_test(function(t) {
var animation = addDivAndAnimate(t,
{ class: 'compositable' },
subtest.frames, 100 * MS_PER_SEC);
return animation.ready.then(function() {
assert_animation_property_state_equals(
animation.effect.getProperties(),
subtest.expected);
});
}, subtest.desc);
});
gAnimationWithGeometricKeyframeTests.forEach(function(subtest) {
promise_test(function(t) {
var animation = addDivAndAnimate(t,
{ class: 'compositable' },
subtest.frames, 100 * MS_PER_SEC);
return animation.ready.then(function() {
// First, a transform animation is running on compositor.
assert_animation_property_state_equals(
animation.effect.getProperties(),
subtest.expected.withoutGeometric);
}).then(function() {
// Add a 'width' property.
var keyframes = animation.effect.getKeyframes();
keyframes[0].width = '100px';
keyframes[1].width = '200px';
animation.effect.setKeyframes(keyframes);
return waitForFrame();
}).then(function() {
// Now the transform animation is not running on compositor because of
// the 'width' property.
assert_animation_property_state_equals(
animation.effect.getProperties(),
subtest.expected.withGeometric);
}).then(function() {
// Remove the 'width' property.
var keyframes = animation.effect.getKeyframes();
delete keyframes[0].width;
delete keyframes[1].width;
animation.effect.setKeyframes(keyframes);
return waitForFrame();
}).then(function() {
// Finally, the transform animation is running on compositor.
assert_animation_property_state_equals(
animation.effect.getProperties(),
subtest.expected.withoutGeometric);
});
}, 'An animation has: ' + subtest.desc);
});
gPerformanceWarningTestsStyle.forEach(function(subtest) {
promise_test(function(t) {
var animation = addDivAndAnimate(t,
{ class: 'compositable' },
subtest.frames, 100 * MS_PER_SEC);
return animation.ready.then(function() {
assert_property_state_on_compositor(
animation.effect.getProperties(),
subtest.expected);
animation.effect.target.style = subtest.style;
return waitForFrame();
}).then(function() {
assert_animation_property_state_equals(
animation.effect.getProperties(),
subtest.expected);
animation.effect.target.style = '';
return waitForFrame();
}).then(function() {
assert_property_state_on_compositor(
animation.effect.getProperties(),
subtest.expected);
});
}, subtest.desc);
});
gPerformanceWarningTestsId.forEach(function(subtest) {
promise_test(function(t) {
if (subtest.createelement) {
addDiv(t, { style: subtest.createelement });
}
var animation = addDivAndAnimate(t,
{ class: 'compositable' },
subtest.frames, 100 * MS_PER_SEC);
return animation.ready.then(function() {
assert_property_state_on_compositor(
animation.effect.getProperties(),
subtest.expected);
animation.effect.target.id = subtest.id;
return waitForFrame();
}).then(function() {
assert_animation_property_state_equals(
animation.effect.getProperties(),
subtest.expected);
animation.effect.target.id = '';
return waitForFrame();
}).then(function() {
assert_property_state_on_compositor(
animation.effect.getProperties(),
subtest.expected);
});
}, subtest.desc);
});
gMultipleAsyncAnimationsTests.forEach(function(subtest) {
promise_test(function(t) {
var div = addDiv(t, { class: 'compositable' });
var animations = subtest.animations.map(function(anim) {
var animation = div.animate(anim.frames, 100 * MS_PER_SEC);
// Bind expected values to animation object.
animation.expected = anim.expected;
return animation;
});
return waitForAllAnimations(animations).then(function() {
animations.forEach(function(anim) {
assert_property_state_on_compositor(
anim.effect.getProperties(),
anim.expected);
});
div.style = subtest.style;
return waitForFrame();
}).then(function() {
animations.forEach(function(anim) {
assert_animation_property_state_equals(
anim.effect.getProperties(),
anim.expected);
});
div.style = '';
return waitForFrame();
}).then(function() {
animations.forEach(function(anim) {
assert_property_state_on_compositor(
anim.effect.getProperties(),
anim.expected);
});
});
}, 'Multiple animations: ' + subtest.desc);
});
gMultipleAsyncAnimationsWithGeometricKeyframeTests.forEach(function(subtest) {
promise_test(function(t) {
var div = addDiv(t, { class: 'compositable' });
var animations = subtest.animations.map(function(anim) {
var animation = div.animate(anim.frames, 100 * MS_PER_SEC);
// Bind expected values to animation object.
animation.expected = anim.expected;
return animation;
});
return waitForAllAnimations(animations).then(function() {
// First, all animations are running on compositor.
animations.forEach(function(anim) {
assert_animation_property_state_equals(
anim.effect.getProperties(),
anim.expected.withoutGeometric);
});
}).then(function() {
// Add a 'width' property to animations[1].
var keyframes = animations[1].effect.getKeyframes();
keyframes[0].width = '100px';
keyframes[1].width = '200px';
animations[1].effect.setKeyframes(keyframes);
return waitForFrame();
}).then(function() {
// Now the transform animation is not running on compositor because of
// the 'width' property.
animations.forEach(function(anim) {
assert_animation_property_state_equals(
anim.effect.getProperties(),
anim.expected.withGeometric);
});
}).then(function() {
// Remove the 'width' property from animations[1].
var keyframes = animations[1].effect.getKeyframes();
delete keyframes[0].width;
delete keyframes[1].width;
animations[1].effect.setKeyframes(keyframes);
return waitForFrame();
}).then(function() {
// Finally, all animations are running on compositor.
animations.forEach(function(anim) {
assert_animation_property_state_equals(
anim.effect.getProperties(),
anim.expected.withoutGeometric);
});
});
}, 'Multiple animations with geometric property: ' + subtest.desc);
});
gMultipleAsyncAnimationsWithGeometricAnimationTests.forEach(function(subtest) {
promise_test(function(t) {
var div = addDiv(t, { class: 'compositable' });
var animations = subtest.animations.map(function(anim) {
var animation = div.animate(anim.frames, 100 * MS_PER_SEC);
// Bind expected values to animation object.
animation.expected = anim.expected;
return animation;
});
var widthAnimation;
return waitForAllAnimations(animations).then(function() {
animations.forEach(function(anim) {
assert_property_state_on_compositor(
anim.effect.getProperties(),
anim.expected);
});
}).then(function() {
// Append 'width' animation on the same element.
widthAnimation = div.animate({ width: ['100px', '200px'] },
100 * MS_PER_SEC);
return waitForFrame();
}).then(function() {
// Now transform animations are not running on compositor because of
// the 'width' animation.
animations.forEach(function(anim) {
assert_animation_property_state_equals(
anim.effect.getProperties(),
anim.expected);
});
// Remove the 'width' animation.
widthAnimation.cancel();
return waitForFrame();
}).then(function() {
// Now all animations are running on compositor.
animations.forEach(function(anim) {
assert_property_state_on_compositor(
anim.effect.getProperties(),
anim.expected);
});
});
}, 'Multiple async animations and geometric animation: ' + subtest.desc);
});
gAnimationsOnTooSmallElementTests.forEach(function(subtest) {
promise_test(function(t) {
var div = addDiv(t, subtest.style);
var animation = div.animate(subtest.frames, 100 * MS_PER_SEC);
return animation.ready.then(function() {
assert_animation_property_state_equals(
animation.effect.getProperties(),
subtest.expected);
});
}, subtest.desc);
});
promise_test(function(t) {
var animation = addDivAndAnimate(t,
{ class: 'compositable' },
{ transform: [ 'translate(0px)',
'translate(100px)'] },
100 * MS_PER_SEC);
return animation.ready.then(function() {
assert_animation_property_state_equals(
animation.effect.getProperties(),
[ { property: 'transform', runningOnCompositor: true } ]);
animation.effect.target.style = 'width: 10000px; height: 10000px';
return waitForFrame();
}).then(function() {
// viewport depends on test environment.
var expectedWarning = new RegExp(
"Animation cannot be run on the compositor because the frame size " +
"\\(10000, 10000\\) is bigger than the viewport \\(\\d+, \\d+\\) " +
"or the visual rectangle \\(10000, 10000\\) is larger than the " +
"maximum allowed value \\(\\d+\\)");
assert_animation_property_state_equals(
animation.effect.getProperties(),
[ {
property: 'transform',
runningOnCompositor: false,
warning: expectedWarning
} ]);
animation.effect.target.style = 'width: 100px; height: 100px';
return waitForFrame();
}).then(function() {
// FIXME: Bug 1253164: the animation should get back on compositor.
assert_animation_property_state_equals(
animation.effect.getProperties(),
[ { property: 'transform', runningOnCompositor: false } ]);
});
}, 'transform on too big element');
promise_test(function(t) {
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('width', '100');
svg.setAttribute('height', '100');
var rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rect.setAttribute('width', '100');
rect.setAttribute('height', '100');
rect.setAttribute('fill', 'red');
svg.appendChild(rect);
document.body.appendChild(svg);
t.add_cleanup(function() {
svg.remove();
});
var animation = svg.animate(
{ transform: ['translate(0px)', 'translate(100px)'] }, 100 * MS_PER_SEC);
return animation.ready.then(function() {
assert_animation_property_state_equals(
animation.effect.getProperties(),
[ { property: 'transform', runningOnCompositor: true } ]);
svg.setAttribute('transform', 'translate(10, 20)');
return waitForFrame();
}).then(function() {
assert_animation_property_state_equals(
animation.effect.getProperties(),
[ {
property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningTransformSVG'
} ]);
svg.removeAttribute('transform');
return waitForFrame();
}).then(function() {
assert_animation_property_state_equals(
animation.effect.getProperties(),
[ { property: 'transform', runningOnCompositor: true } ]);
});
}, 'transform of nsIFrame with SVG transform');
promise_test(function(t) {
var div = addDiv(t, { class: 'compositable',
style: 'animation: fade 100s' });
var cssAnimation = div.getAnimations()[0];
var scriptAnimation = div.animate({ opacity: [ 1, 0 ] }, 100 * MS_PER_SEC);
return scriptAnimation.ready.then(function() {
assert_animation_property_state_equals(
cssAnimation.effect.getProperties(),
[ { property: 'opacity', runningOnCompositor: true } ]);
assert_animation_property_state_equals(
scriptAnimation.effect.getProperties(),
[ { property: 'opacity', runningOnCompositor: true } ]);
});
}, 'overridden animation');
}
</script>
</body>

View file

@ -0,0 +1,993 @@
<!doctype html>
<head>
<meta charset=utf-8>
<title>Bug 1254419 - Test the values returned by
KeyframeEffectReadOnly.getProperties()</title>
<script type="application/javascript" src="../testharness.js"></script>
<script type="application/javascript" src="../testharnessreport.js"></script>
<script type="application/javascript" src="../testcommon.js"></script>
</head>
<body>
<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1254419"
target="_blank">Mozilla Bug 1254419</a>
<div id="log"></div>
<style>
:root {
--var-100px: 100px;
--var-100px-200px: 100px 200px;
}
div {
font-size: 10px; /* For calculating em-based units */
}
</style>
<script>
'use strict';
function assert_properties_equal(actual, expected) {
assert_equals(actual.length, expected.length);
var compareProperties = (a, b) =>
a.property == b.property ? 0 : (a.property < b.property ? -1 : 1);
var sortedActual = actual.sort(compareProperties);
var sortedExpected = expected.sort(compareProperties);
// We want to serialize the values in the following form:
//
// { offset: 0, easing: linear, composite: replace, value: 5px }, ...
//
// So that we can just compare strings and, in the failure case,
// easily see where the differences lie.
var serializeMember = value => {
return typeof value === 'undefined' ? '<not set>' : value;
}
var serializeValues = values =>
values.map(value =>
'{ ' +
[ 'offset', 'value', 'easing', 'composite' ].map(
member => `${member}: ${serializeMember(value[member])}`
).join(', ') +
' }')
.join(', ');
for (var i = 0; i < sortedActual.length; i++) {
assert_equals(sortedActual[i].property,
sortedExpected[i].property,
'CSS property name should match');
assert_equals(serializeValues(sortedActual[i].values),
serializeValues(sortedExpected[i].values),
`Values arrays do not match for `
+ `${sortedActual[i].property} property`);
}
}
// Shorthand for constructing a value object
function value(offset, value, composite, easing) {
return { offset: offset, value: value, easing: easing, composite: composite };
}
var gTests = [
// ---------------------------------------------------------------------
//
// Tests for property-indexed specifications
//
// ---------------------------------------------------------------------
{ desc: 'a one-property two-value property-indexed specification',
frames: { left: ['10px', '20px'] },
expected: [ { property: 'left',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '20px', 'replace') ] } ]
},
{ desc: 'a one-shorthand-property two-value property-indexed'
+ ' specification',
frames: { margin: ['10px', '10px 20px 30px 40px'] },
expected: [ { property: 'margin-top',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '10px', 'replace') ] },
{ property: 'margin-right',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '20px', 'replace') ] },
{ property: 'margin-bottom',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '30px', 'replace') ] },
{ property: 'margin-left',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '40px', 'replace') ] } ]
},
{ desc: 'a two-property (one shorthand and one of its longhand'
+ ' components) two-value property-indexed specification',
frames: { marginTop: ['50px', '60px'],
margin: ['10px', '10px 20px 30px 40px'] },
expected: [ { property: 'margin-top',
values: [ value(0, '50px', 'replace', 'linear'),
value(1, '60px', 'replace') ] },
{ property: 'margin-right',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '20px', 'replace') ] },
{ property: 'margin-bottom',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '30px', 'replace') ] },
{ property: 'margin-left',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '40px', 'replace') ] } ]
},
{ desc: 'a two-property property-indexed specification with different'
+ ' numbers of values',
frames: { left: ['10px', '20px', '30px'],
top: ['40px', '50px'] },
expected: [ { property: 'left',
values: [ value(0, '10px', 'replace', 'linear'),
value(0.5, '20px', 'replace', 'linear'),
value(1, '30px', 'replace') ] },
{ property: 'top',
values: [ value(0, '40px', 'replace', 'linear'),
value(1, '50px', 'replace') ] } ]
},
{ desc: 'a property-indexed specification with an invalid value',
frames: { left: ['10px', '20px', '30px', '40px', '50px'],
top: ['15px', '25px', 'invalid', '45px', '55px'] },
expected: [ { property: 'left',
values: [ value(0, '10px', 'replace', 'linear'),
value(0.25, '20px', 'replace', 'linear'),
value(0.5, '30px', 'replace', 'linear'),
value(0.75, '40px', 'replace', 'linear'),
value(1, '50px', 'replace') ] },
{ property: 'top',
values: [ value(0, '15px', 'replace', 'linear'),
value(0.25, '25px', 'replace', 'linear'),
value(0.75, '45px', 'replace', 'linear'),
value(1, '55px', 'replace') ] } ]
},
{ desc: 'a one-property two-value property-indexed specification that'
+ ' needs to stringify its values',
frames: { opacity: [0, 1] },
expected: [ { property: 'opacity',
values: [ value(0, '0', 'replace', 'linear'),
value(1, '1', 'replace') ] } ]
},
{ desc: 'a property-indexed keyframe where a lesser shorthand precedes'
+ ' a greater shorthand',
frames: { borderLeft: [ '1px solid rgb(1, 2, 3)',
'2px solid rgb(4, 5, 6)' ],
border: [ '3px dotted rgb(7, 8, 9)',
'4px dashed rgb(10, 11, 12)' ] },
expected: [ { property: 'border-bottom-color',
values: [ value(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
value(1, 'rgb(10, 11, 12)', 'replace') ] },
{ property: 'border-left-color',
values: [ value(0, 'rgb(1, 2, 3)', 'replace', 'linear'),
value(1, 'rgb(4, 5, 6)', 'replace') ] },
{ property: 'border-right-color',
values: [ value(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
value(1, 'rgb(10, 11, 12)', 'replace') ] },
{ property: 'border-top-color',
values: [ value(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
value(1, 'rgb(10, 11, 12)', 'replace') ] },
{ property: 'border-bottom-width',
values: [ value(0, '3px', 'replace', 'linear'),
value(1, '4px', 'replace') ] },
{ property: 'border-left-width',
values: [ value(0, '1px', 'replace', 'linear'),
value(1, '2px', 'replace') ] },
{ property: 'border-right-width',
values: [ value(0, '3px', 'replace', 'linear'),
value(1, '4px', 'replace') ] },
{ property: 'border-top-width',
values: [ value(0, '3px', 'replace', 'linear'),
value(1, '4px', 'replace') ] },
{ property: 'border-bottom-style',
values: [ value(0, 'dotted', 'replace', 'linear'),
value(1, 'dashed', 'replace') ] },
{ property: 'border-left-style',
values: [ value(0, 'solid', 'replace', 'linear'),
value(1, 'solid', 'replace') ] },
{ property: 'border-right-style',
values: [ value(0, 'dotted', 'replace', 'linear'),
value(1, 'dashed', 'replace') ] },
{ property: 'border-top-style',
values: [ value(0, 'dotted', 'replace', 'linear'),
value(1, 'dashed', 'replace') ] },
{ property: 'border-image-outset',
values: [ value(0, '0 0 0 0', 'replace', 'linear'),
value(1, '0 0 0 0', 'replace') ] },
{ property: 'border-image-repeat',
values: [ value(0, 'stretch stretch', 'replace', 'linear'),
value(1, 'stretch stretch', 'replace') ] },
{ property: 'border-image-slice',
values: [ value(0, '100% 100% 100% 100%',
'replace', 'linear'),
value(1, '100% 100% 100% 100%', 'replace') ] },
{ property: 'border-image-source',
values: [ value(0, 'none', 'replace', 'linear'),
value(1, 'none', 'replace') ] },
{ property: 'border-image-width',
values: [ value(0, '1 1 1 1', 'replace', 'linear'),
value(1, '1 1 1 1', 'replace') ] },
{ property: '-moz-border-bottom-colors',
values: [ value(0, 'none', 'replace', 'linear'),
value(1, 'none', 'replace') ] },
{ property: '-moz-border-left-colors',
values: [ value(0, 'none', 'replace', 'linear'),
value(1, 'none', 'replace') ] },
{ property: '-moz-border-right-colors',
values: [ value(0, 'none', 'replace', 'linear'),
value(1, 'none', 'replace') ] },
{ property: '-moz-border-top-colors',
values: [ value(0, 'none', 'replace', 'linear'),
value(1, 'none', 'replace') ] } ]
},
{ desc: 'a property-indexed keyframe where a greater shorthand precedes'
+ ' a lesser shorthand',
frames: { border: [ '3px dotted rgb(7, 8, 9)',
'4px dashed rgb(10, 11, 12)' ],
borderLeft: [ '1px solid rgb(1, 2, 3)',
'2px solid rgb(4, 5, 6)' ] },
expected: [ { property: 'border-bottom-color',
values: [ value(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
value(1, 'rgb(10, 11, 12)', 'replace') ] },
{ property: 'border-left-color',
values: [ value(0, 'rgb(1, 2, 3)', 'replace', 'linear'),
value(1, 'rgb(4, 5, 6)', 'replace') ] },
{ property: 'border-right-color',
values: [ value(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
value(1, 'rgb(10, 11, 12)', 'replace') ] },
{ property: 'border-top-color',
values: [ value(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
value(1, 'rgb(10, 11, 12)', 'replace') ] },
{ property: 'border-bottom-width',
values: [ value(0, '3px', 'replace', 'linear'),
value(1, '4px', 'replace') ] },
{ property: 'border-left-width',
values: [ value(0, '1px', 'replace', 'linear'),
value(1, '2px', 'replace') ] },
{ property: 'border-right-width',
values: [ value(0, '3px', 'replace', 'linear'),
value(1, '4px', 'replace') ] },
{ property: 'border-top-width',
values: [ value(0, '3px', 'replace', 'linear'),
value(1, '4px', 'replace') ] },
{ property: 'border-bottom-style',
values: [ value(0, 'dotted', 'replace', 'linear'),
value(1, 'dashed', 'replace') ] },
{ property: 'border-left-style',
values: [ value(0, 'solid', 'replace', 'linear'),
value(1, 'solid', 'replace') ] },
{ property: 'border-right-style',
values: [ value(0, 'dotted', 'replace', 'linear'),
value(1, 'dashed', 'replace') ] },
{ property: 'border-top-style',
values: [ value(0, 'dotted', 'replace', 'linear'),
value(1, 'dashed', 'replace') ] },
{ property: 'border-image-outset',
values: [ value(0, '0 0 0 0', 'replace', 'linear'),
value(1, '0 0 0 0', 'replace') ] },
{ property: 'border-image-repeat',
values: [ value(0, 'stretch stretch', 'replace', 'linear'),
value(1, 'stretch stretch', 'replace') ] },
{ property: 'border-image-slice',
values: [ value(0, '100% 100% 100% 100%',
'replace', 'linear'),
value(1, '100% 100% 100% 100%', 'replace') ] },
{ property: 'border-image-source',
values: [ value(0, 'none', 'replace', 'linear'),
value(1, 'none', 'replace') ] },
{ property: 'border-image-width',
values: [ value(0, '1 1 1 1', 'replace', 'linear'),
value(1, '1 1 1 1', 'replace') ] },
{ property: '-moz-border-bottom-colors',
values: [ value(0, 'none', 'replace', 'linear'),
value(1, 'none', 'replace') ] },
{ property: '-moz-border-left-colors',
values: [ value(0, 'none', 'replace', 'linear'),
value(1, 'none', 'replace') ] },
{ property: '-moz-border-right-colors',
values: [ value(0, 'none', 'replace', 'linear'),
value(1, 'none', 'replace') ] },
{ property: '-moz-border-top-colors',
values: [ value(0, 'none', 'replace', 'linear'),
value(1, 'none', 'replace') ] } ]
},
// ---------------------------------------------------------------------
//
// Tests for keyframe sequences
//
// ---------------------------------------------------------------------
{ desc: 'a keyframe sequence specification with repeated values at'
+ ' offset 0/1 with different easings',
frames: [ { offset: 0.0, left: '100px', easing: 'ease' },
{ offset: 0.0, left: '200px', easing: 'ease' },
{ offset: 0.5, left: '300px', easing: 'linear' },
{ offset: 1.0, left: '400px', easing: 'ease-out' },
{ offset: 1.0, left: '500px', easing: 'step-end' } ],
expected: [ { property: 'left',
values: [ value(0, '100px', 'replace'),
value(0, '200px', 'replace', 'ease'),
value(0.5, '300px', 'replace', 'linear'),
value(1, '400px', 'replace'),
value(1, '500px', 'replace') ] } ]
},
{ desc: 'a one-property two-keyframe sequence',
frames: [ { offset: 0, left: '10px' },
{ offset: 1, left: '20px' } ],
expected: [ { property: 'left',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '20px', 'replace') ] } ]
},
{ desc: 'a two-property two-keyframe sequence',
frames: [ { offset: 0, left: '10px', top: '30px' },
{ offset: 1, left: '20px', top: '40px' } ],
expected: [ { property: 'left',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '20px', 'replace') ] },
{ property: 'top',
values: [ value(0, '30px', 'replace', 'linear'),
value(1, '40px', 'replace') ] } ]
},
{ desc: 'a one shorthand property two-keyframe sequence',
frames: [ { offset: 0, margin: '10px' },
{ offset: 1, margin: '20px 30px 40px 50px' } ],
expected: [ { property: 'margin-top',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '20px', 'replace') ] },
{ property: 'margin-right',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '30px', 'replace') ] },
{ property: 'margin-bottom',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '40px', 'replace') ] },
{ property: 'margin-left',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '50px', 'replace') ] } ]
},
{ desc: 'a two-property (a shorthand and one of its component longhands)'
+ ' two-keyframe sequence',
frames: [ { offset: 0, margin: '10px', marginTop: '20px' },
{ offset: 1, marginTop: '70px',
margin: '30px 40px 50px 60px' } ],
expected: [ { property: 'margin-top',
values: [ value(0, '20px', 'replace', 'linear'),
value(1, '70px', 'replace') ] },
{ property: 'margin-right',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '40px', 'replace') ] },
{ property: 'margin-bottom',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '50px', 'replace') ] },
{ property: 'margin-left',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '60px', 'replace') ] } ]
},
{ desc: 'a keyframe sequence with duplicate values for a given interior'
+ ' offset',
frames: [ { offset: 0.0, left: '10px' },
{ offset: 0.5, left: '20px' },
{ offset: 0.5, left: '30px' },
{ offset: 0.5, left: '40px' },
{ offset: 1.0, left: '50px' } ],
expected: [ { property: 'left',
values: [ value(0, '10px', 'replace', 'linear'),
value(0.5, '20px', 'replace'),
value(0.5, '40px', 'replace', 'linear'),
value(1, '50px', 'replace') ] } ]
},
{ desc: 'a keyframe sequence with duplicate values for offsets 0 and 1',
frames: [ { offset: 0, left: '10px' },
{ offset: 0, left: '20px' },
{ offset: 0, left: '30px' },
{ offset: 1, left: '40px' },
{ offset: 1, left: '50px' },
{ offset: 1, left: '60px' } ],
expected: [ { property: 'left',
values: [ value(0, '10px', 'replace'),
value(0, '30px', 'replace', 'linear'),
value(1, '40px', 'replace'),
value(1, '60px', 'replace') ] } ]
},
{ desc: 'a two-property four-keyframe sequence',
frames: [ { offset: 0, left: '10px' },
{ offset: 0, top: '20px' },
{ offset: 1, top: '30px' },
{ offset: 1, left: '40px' } ],
expected: [ { property: 'left',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '40px', 'replace') ] },
{ property: 'top',
values: [ value(0, '20px', 'replace', 'linear'),
value(1, '30px', 'replace') ] } ]
},
{ desc: 'a one-property keyframe sequence with some omitted offsets',
frames: [ { offset: 0.00, left: '10px' },
{ offset: 0.25, left: '20px' },
{ left: '30px' },
{ left: '40px' },
{ offset: 1.00, left: '50px' } ],
expected: [ { property: 'left',
values: [ value(0, '10px', 'replace', 'linear'),
value(0.25, '20px', 'replace', 'linear'),
value(0.5, '30px', 'replace', 'linear'),
value(0.75, '40px', 'replace', 'linear'),
value(1, '50px', 'replace') ] } ]
},
{ desc: 'a two-property keyframe sequence with some omitted offsets',
frames: [ { offset: 0.00, left: '10px', top: '20px' },
{ offset: 0.25, left: '30px' },
{ left: '40px' },
{ left: '50px', top: '60px' },
{ offset: 1.00, left: '70px', top: '80px' } ],
expected: [ { property: 'left',
values: [ value(0, '10px', 'replace', 'linear'),
value(0.25, '30px', 'replace', 'linear'),
value(0.5, '40px', 'replace', 'linear'),
value(0.75, '50px', 'replace', 'linear'),
value(1, '70px', 'replace') ] },
{ property: 'top',
values: [ value(0, '20px', 'replace', 'linear'),
value(0.75, '60px', 'replace', 'linear'),
value(1, '80px', 'replace') ] } ]
},
{ desc: 'a one-property keyframe sequence with all omitted offsets',
frames: [ { left: '10px' },
{ left: '20px' },
{ left: '30px' },
{ left: '40px' },
{ left: '50px' } ],
expected: [ { property: 'left',
values: [ value(0, '10px', 'replace', 'linear'),
value(0.25, '20px', 'replace', 'linear'),
value(0.5, '30px', 'replace', 'linear'),
value(0.75, '40px', 'replace', 'linear'),
value(1, '50px', 'replace') ] } ]
},
{ desc: 'a keyframe sequence with different easing values, but the'
+ ' same easing value for a given offset',
frames: [ { offset: 0.0, easing: 'ease', left: '10px'},
{ offset: 0.0, easing: 'ease', top: '20px'},
{ offset: 0.5, easing: 'linear', left: '30px' },
{ offset: 0.5, easing: 'linear', top: '40px' },
{ offset: 1.0, easing: 'step-end', left: '50px' },
{ offset: 1.0, easing: 'step-end', top: '60px' } ],
expected: [ { property: 'left',
values: [ value(0, '10px', 'replace', 'ease'),
value(0.5, '30px', 'replace', 'linear'),
value(1, '50px', 'replace') ] },
{ property: 'top',
values: [ value(0, '20px', 'replace', 'ease'),
value(0.5, '40px', 'replace', 'linear'),
value(1, '60px', 'replace') ] } ]
},
{ desc: 'a one-property two-keyframe sequence that needs to'
+ ' stringify its values',
frames: [ { offset: 0, opacity: 0 },
{ offset: 1, opacity: 1 } ],
expected: [ { property: 'opacity',
values: [ value(0, '0', 'replace', 'linear'),
value(1, '1', 'replace') ] } ]
},
{ desc: 'a keyframe sequence where shorthand precedes longhand',
frames: [ { offset: 0, margin: '10px', marginRight: '20px' },
{ offset: 1, margin: '30px' } ],
expected: [ { property: 'margin-top',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '30px', 'replace') ] },
{ property: 'margin-right',
values: [ value(0, '20px', 'replace', 'linear'),
value(1, '30px', 'replace') ] },
{ property: 'margin-bottom',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '30px', 'replace') ] },
{ property: 'margin-left',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '30px', 'replace') ] } ]
},
{ desc: 'a keyframe sequence where longhand precedes shorthand',
frames: [ { offset: 0, marginRight: '20px', margin: '10px' },
{ offset: 1, margin: '30px' } ],
expected: [ { property: 'margin-top',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '30px', 'replace') ] },
{ property: 'margin-right',
values: [ value(0, '20px', 'replace', 'linear'),
value(1, '30px', 'replace') ] },
{ property: 'margin-bottom',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '30px', 'replace') ] },
{ property: 'margin-left',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '30px', 'replace') ] } ]
},
{ desc: 'a keyframe sequence where lesser shorthand precedes greater'
+ ' shorthand',
frames: [ { offset: 0, borderLeft: '1px solid rgb(1, 2, 3)',
border: '2px dotted rgb(4, 5, 6)' },
{ offset: 1, border: '3px dashed rgb(7, 8, 9)' } ],
expected: [ { property: 'border-bottom-color',
values: [ value(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
value(1, 'rgb(7, 8, 9)', 'replace') ] },
{ property: 'border-left-color',
values: [ value(0, 'rgb(1, 2, 3)', 'replace', 'linear'),
value(1, 'rgb(7, 8, 9)', 'replace') ] },
{ property: 'border-right-color',
values: [ value(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
value(1, 'rgb(7, 8, 9)', 'replace') ] },
{ property: 'border-top-color',
values: [ value(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
value(1, 'rgb(7, 8, 9)', 'replace') ] },
{ property: 'border-bottom-width',
values: [ value(0, '2px', 'replace', 'linear'),
value(1, '3px', 'replace') ] },
{ property: 'border-left-width',
values: [ value(0, '1px', 'replace', 'linear'),
value(1, '3px', 'replace') ] },
{ property: 'border-right-width',
values: [ value(0, '2px', 'replace', 'linear'),
value(1, '3px', 'replace') ] },
{ property: 'border-top-width',
values: [ value(0, '2px', 'replace', 'linear'),
value(1, '3px', 'replace') ] },
{ property: 'border-bottom-style',
values: [ value(0, 'dotted', 'replace', 'linear'),
value(1, 'dashed', 'replace') ] },
{ property: 'border-left-style',
values: [ value(0, 'solid', 'replace', 'linear'),
value(1, 'dashed', 'replace') ] },
{ property: 'border-right-style',
values: [ value(0, 'dotted', 'replace', 'linear'),
value(1, 'dashed', 'replace') ] },
{ property: 'border-top-style',
values: [ value(0, 'dotted', 'replace', 'linear'),
value(1, 'dashed', 'replace') ] },
{ property: 'border-image-outset',
values: [ value(0, '0 0 0 0', 'replace', 'linear'),
value(1, '0 0 0 0', 'replace') ] },
{ property: 'border-image-repeat',
values: [ value(0, 'stretch stretch', 'replace', 'linear'),
value(1, 'stretch stretch', 'replace') ] },
{ property: 'border-image-slice',
values: [ value(0, '100% 100% 100% 100%',
'replace', 'linear'),
value(1, '100% 100% 100% 100%', 'replace') ] },
{ property: 'border-image-source',
values: [ value(0, 'none', 'replace', 'linear'),
value(1, 'none', 'replace') ] },
{ property: 'border-image-width',
values: [ value(0, '1 1 1 1', 'replace', 'linear'),
value(1, '1 1 1 1', 'replace') ] },
{ property: '-moz-border-bottom-colors',
values: [ value(0, 'none', 'replace', 'linear'),
value(1, 'none', 'replace') ] },
{ property: '-moz-border-left-colors',
values: [ value(0, 'none', 'replace', 'linear'),
value(1, 'none', 'replace') ] },
{ property: '-moz-border-right-colors',
values: [ value(0, 'none', 'replace', 'linear'),
value(1, 'none', 'replace') ] },
{ property: '-moz-border-top-colors',
values: [ value(0, 'none', 'replace', 'linear'),
value(1, 'none', 'replace') ] } ]
},
{ desc: 'a keyframe sequence where greater shorthand precedes'
+ ' lesser shorthand',
frames: [ { offset: 0, border: '2px dotted rgb(4, 5, 6)',
borderLeft: '1px solid rgb(1, 2, 3)' },
{ offset: 1, border: '3px dashed rgb(7, 8, 9)' } ],
expected: [ { property: 'border-bottom-color',
values: [ value(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
value(1, 'rgb(7, 8, 9)', 'replace') ] },
{ property: 'border-left-color',
values: [ value(0, 'rgb(1, 2, 3)', 'replace', 'linear'),
value(1, 'rgb(7, 8, 9)', 'replace') ] },
{ property: 'border-right-color',
values: [ value(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
value(1, 'rgb(7, 8, 9)', 'replace') ] },
{ property: 'border-top-color',
values: [ value(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
value(1, 'rgb(7, 8, 9)', 'replace') ] },
{ property: 'border-bottom-width',
values: [ value(0, '2px', 'replace', 'linear'),
value(1, '3px', 'replace') ] },
{ property: 'border-left-width',
values: [ value(0, '1px', 'replace', 'linear'),
value(1, '3px', 'replace') ] },
{ property: 'border-right-width',
values: [ value(0, '2px', 'replace', 'linear'),
value(1, '3px', 'replace') ] },
{ property: 'border-top-width',
values: [ value(0, '2px', 'replace', 'linear'),
value(1, '3px', 'replace') ] },
{ property: 'border-bottom-style',
values: [ value(0, 'dotted', 'replace', 'linear'),
value(1, 'dashed', 'replace') ] },
{ property: 'border-left-style',
values: [ value(0, 'solid', 'replace', 'linear'),
value(1, 'dashed', 'replace') ] },
{ property: 'border-right-style',
values: [ value(0, 'dotted', 'replace', 'linear'),
value(1, 'dashed', 'replace') ] },
{ property: 'border-top-style',
values: [ value(0, 'dotted', 'replace', 'linear'),
value(1, 'dashed', 'replace') ] },
{ property: 'border-image-outset',
values: [ value(0, '0 0 0 0', 'replace', 'linear'),
value(1, '0 0 0 0', 'replace') ] },
{ property: 'border-image-repeat',
values: [ value(0, 'stretch stretch', 'replace', 'linear'),
value(1, 'stretch stretch', 'replace') ] },
{ property: 'border-image-slice',
values: [ value(0, '100% 100% 100% 100%',
'replace', 'linear'),
value(1, '100% 100% 100% 100%', 'replace') ] },
{ property: 'border-image-source',
values: [ value(0, 'none', 'replace', 'linear'),
value(1, 'none', 'replace') ] },
{ property: 'border-image-width',
values: [ value(0, '1 1 1 1', 'replace', 'linear'),
value(1, '1 1 1 1', 'replace') ] },
{ property: '-moz-border-bottom-colors',
values: [ value(0, 'none', 'replace', 'linear'),
value(1, 'none', 'replace') ] },
{ property: '-moz-border-left-colors',
values: [ value(0, 'none', 'replace', 'linear'),
value(1, 'none', 'replace') ] },
{ property: '-moz-border-right-colors',
values: [ value(0, 'none', 'replace', 'linear'),
value(1, 'none', 'replace') ] },
{ property: '-moz-border-top-colors',
values: [ value(0, 'none', 'replace', 'linear'),
value(1, 'none', 'replace') ] } ]
},
// ---------------------------------------------------------------------
//
// Tests for unit conversion
//
// ---------------------------------------------------------------------
{ desc: 'em units are resolved to px values',
frames: { left: ['10em', '20em'] },
expected: [ { property: 'left',
values: [ value(0, '100px', 'replace', 'linear'),
value(1, '200px', 'replace') ] } ]
},
{ desc: 'calc() expressions are resolved to the equivalent units',
frames: { left: ['calc(10em + 10px)', 'calc(10em + 10%)'] },
expected: [ { property: 'left',
values: [ value(0, 'calc(110px)', 'replace', 'linear'),
value(1, 'calc(100px + 10%)', 'replace') ] } ]
},
// ---------------------------------------------------------------------
//
// Tests for CSS variable handling conversion
//
// ---------------------------------------------------------------------
{ desc: 'CSS variables are resolved to their corresponding values',
frames: { left: ['10px', 'var(--var-100px)'] },
expected: [ { property: 'left',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '100px', 'replace') ] } ]
},
{ desc: 'CSS variables in calc() expressions are resolved',
frames: { left: ['10px', 'calc(var(--var-100px) / 2 - 10%)'] },
expected: [ { property: 'left',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, 'calc(50px + -10%)', 'replace') ] } ]
},
{ desc: 'CSS variables in shorthands are resolved to their corresponding'
+ ' values',
frames: { margin: ['10px', 'var(--var-100px-200px)'] },
expected: [ { property: 'margin-top',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '100px', 'replace') ] },
{ property: 'margin-right',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '200px', 'replace') ] },
{ property: 'margin-bottom',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '100px', 'replace') ] },
{ property: 'margin-left',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '200px', 'replace') ] } ]
},
// ---------------------------------------------------------------------
//
// Tests for properties that parse correctly but which we fail to
// convert to computed values.
//
// ---------------------------------------------------------------------
{ desc: 'a property that can\'t be resolved to computed values in'
+ ' initial keyframe',
frames: [ { margin: '5px', simulateComputeValuesFailure: true },
{ margin: '5px' } ],
expected: [ ]
},
{ desc: 'a property that can\'t be resolved to computed values in'
+ ' initial keyframe where we have enough values to create'
+ ' a final segment',
frames: [ { margin: '5px', simulateComputeValuesFailure: true },
{ margin: '5px' },
{ margin: '5px' } ],
expected: [ ]
},
{ desc: 'a property that can\'t be resolved to computed values in'
+ ' initial overlapping keyframes (first in series of two)',
frames: [ { margin: '5px', offset: 0,
simulateComputeValuesFailure: true },
{ margin: '5px', offset: 0 },
{ margin: '5px' } ],
expected: [ { property: 'margin-top',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-right',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-bottom',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-left',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] } ]
},
{ desc: 'a property that can\'t be resolved to computed values in'
+ ' initial overlapping keyframes (second in series of two)',
frames: [ { margin: '5px', offset: 0 },
{ margin: '5px', offset: 0,
simulateComputeValuesFailure: true },
{ margin: '5px' } ],
expected: [ { property: 'margin-top',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-right',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-bottom',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-left',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] } ]
},
{ desc: 'a property that can\'t be resolved to computed values in'
+ ' initial overlapping keyframes (second in series of three)',
frames: [ { margin: '5px', offset: 0 },
{ margin: '5px', offset: 0,
simulateComputeValuesFailure: true },
{ margin: '5px', offset: 0 },
{ margin: '5px' } ],
expected: [ { property: 'margin-top',
values: [ value(0, '5px', 'replace'),
value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-right',
values: [ value(0, '5px', 'replace'),
value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-bottom',
values: [ value(0, '5px', 'replace'),
value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-left',
values: [ value(0, '5px', 'replace'),
value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] } ]
},
{ desc: 'a property that can\'t be resolved to computed values in'
+ ' final keyframe',
frames: [ { margin: '5px' },
{ margin: '5px', simulateComputeValuesFailure: true } ],
expected: [ ]
},
{ desc: 'a property that can\'t be resolved to computed values in'
+ ' final keyframe where it forms the last segment in the series',
frames: [ { margin: '5px' },
{ margin: '5px',
marginLeft: '5px',
marginRight: '5px',
marginBottom: '5px',
// margin-top sorts last and only it will be missing since
// the other longhand components are specified
simulateComputeValuesFailure: true } ],
expected: [ { property: 'margin-bottom',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-left',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-right',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] } ]
},
{ desc: 'a property that can\'t be resolved to computed values in'
+ ' final keyframe where we have enough values to create'
+ ' an initial segment',
frames: [ { margin: '5px' },
{ margin: '5px' },
{ margin: '5px', simulateComputeValuesFailure: true } ],
expected: [ ]
},
{ desc: 'a property that can\'t be resolved to computed values in'
+ ' final overlapping keyframes (first in series of two)',
frames: [ { margin: '5px' },
{ margin: '5px', offset: 1,
simulateComputeValuesFailure: true },
{ margin: '5px', offset: 1 } ],
expected: [ { property: 'margin-top',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-right',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-bottom',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-left',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] } ]
},
{ desc: 'a property that can\'t be resolved to computed values in'
+ ' final overlapping keyframes (second in series of two)',
frames: [ { margin: '5px' },
{ margin: '5px', offset: 1 },
{ margin: '5px', offset: 1,
simulateComputeValuesFailure: true } ],
expected: [ { property: 'margin-top',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-right',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-bottom',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-left',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] } ]
},
{ desc: 'a property that can\'t be resolved to computed values in'
+ ' final overlapping keyframes (second in series of three)',
frames: [ { margin: '5px' },
{ margin: '5px', offset: 1 },
{ margin: '5px', offset: 1,
simulateComputeValuesFailure: true },
{ margin: '5px', offset: 1 } ],
expected: [ { property: 'margin-top',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace'),
value(1, '5px', 'replace') ] },
{ property: 'margin-right',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace'),
value(1, '5px', 'replace') ] },
{ property: 'margin-bottom',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace'),
value(1, '5px', 'replace') ] },
{ property: 'margin-left',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace'),
value(1, '5px', 'replace') ] } ]
},
{ desc: 'a property that can\'t be resolved to computed values in'
+ ' intermediate keyframe',
frames: [ { margin: '5px' },
{ margin: '5px', simulateComputeValuesFailure: true },
{ margin: '5px' } ],
expected: [ { property: 'margin-top',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-right',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-bottom',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-left',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] } ]
},
{ desc: 'a property that can\'t be resolved to computed values in'
+ ' initial keyframe along with other values',
// simulateComputeValuesFailure only applies to shorthands so we can set
// it on the same keyframe and it will only apply to |margin| and not
// |left|.
frames: [ { margin: '77%', left: '10px',
simulateComputeValuesFailure: true },
{ margin: '5px', left: '20px' } ],
expected: [ { property: 'left',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '20px', 'replace') ] } ],
},
{ desc: 'a property that can\'t be resolved to computed values in'
+ ' initial keyframe along with other values where those'
+ ' values sort after the property with missing values',
frames: [ { margin: '77%', right: '10px',
simulateComputeValuesFailure: true },
{ margin: '5px', right: '20px' } ],
expected: [ { property: 'right',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '20px', 'replace') ] } ],
},
{ desc: 'a property that can\'t be resolved to computed values in'
+ ' final keyframe along with other values',
frames: [ { margin: '5px', left: '10px' },
{ margin: '5px', left: '20px',
simulateComputeValuesFailure: true } ],
expected: [ { property: 'left',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '20px', 'replace') ] } ],
},
{ desc: 'a property that can\'t be resolved to computed values in'
+ ' final keyframe along with other values where those'
+ ' values sort after the property with missing values',
frames: [ { margin: '5px', right: '10px' },
{ margin: '5px', right: '20px',
simulateComputeValuesFailure: true } ],
expected: [ { property: 'right',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '20px', 'replace') ] } ],
},
{ desc: 'a property that can\'t be resolved to computed values in'
+ ' an intermediate keyframe along with other values',
frames: [ { margin: '5px', left: '10px' },
{ margin: '5px', left: '20px',
simulateComputeValuesFailure: true },
{ margin: '5px', left: '30px' } ],
expected: [ { property: 'margin-top',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-right',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-bottom',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-left',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'left',
values: [ value(0, '10px', 'replace', 'linear'),
value(0.5, '20px', 'replace', 'linear'),
value(1, '30px', 'replace') ] } ]
},
{ desc: 'a property that can\'t be resolved to computed values in'
+ ' an intermediate keyframe by itself',
frames: [ { margin: '5px', left: '10px' },
{ margin: '5px',
simulateComputeValuesFailure: true },
{ margin: '5px', left: '30px' } ],
expected: [ { property: 'margin-top',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-right',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-bottom',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-left',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'left',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '30px', 'replace') ] } ]
},
];
gTests.forEach(function(subtest) {
test(function(t) {
var div = addDiv(t);
var animation = div.animate(subtest.frames, 100 * MS_PER_SEC);
assert_properties_equal(animation.effect.getProperties(),
subtest.expected);
}, subtest.desc);
});
</script>
</body>

View file

@ -0,0 +1,83 @@
<!DOCTYPE html>
<head>
<meta charset=utf-8>
<title>Test getAnimations() for generated-content elements</title>
<script type="application/javascript" src="../testharness.js"></script>
<script type="application/javascript" src="../testharnessreport.js"></script>
<script type="application/javascript" src="../testcommon.js"></script>
<style>
@keyframes anim { }
@keyframes anim2 { }
.before::before {
content: '';
animation: anim 100s;
}
.after::after {
content: '';
animation: anim 100s, anim2 100s;
}
</style>
</head>
<body>
<div id='root' class='before after'>
<div class='before'></div>
<div></div>
</div>
<script>
'use strict';
const {Cc, Ci, Cu} = SpecialPowers;
function getWalker(node) {
var walker = Cc["@mozilla.org/inspector/deep-tree-walker;1"].
createInstance(Ci.inIDeepTreeWalker);
walker.showAnonymousContent = true;
walker.init(node.ownerDocument, Ci.nsIDOMNodeFilter.SHOW_ALL);
walker.currentNode = node;
return walker;
}
test(function(t) {
var root = document.getElementById('root');
// Flush first to make sure the generated-content elements are ready
// in the tree.
flushComputedStyle(root);
var before = getWalker(root).firstChild();
var after = getWalker(root).lastChild();
// Sanity Checks
assert_equals(document.getAnimations().length, 4,
'All animations in this document');
assert_equals(before.tagName, '_moz_generated_content_before',
'First child is ::before element');
assert_equals(after.tagName, '_moz_generated_content_after',
'Last child is ::after element');
// Test Element.getAnimations() for generated-content elements
assert_equals(before.getAnimations().length, 1,
'Animations of ::before generated-content element');
assert_equals(after.getAnimations().length, 2,
'Animations of ::after generated-content element');
}, 'Element.getAnimations() used on generated-content elements');
test(function(t) {
var root = document.getElementById('root');
flushComputedStyle(root);
var walker = getWalker(root);
var animations = [];
var element = walker.currentNode;
while (element) {
if (element.getAnimations) {
animations = [...animations, ...element.getAnimations()];
}
element = walker.nextNode();
}
assert_equals(animations.length, document.getAnimations().length,
'The number of animations got by DeepTreeWalker and ' +
'document.getAnimations() should be the same');
}, 'Element.getAnimations() used by traversing DeepTreeWalker');
</script>
</body>

View file

@ -0,0 +1,854 @@
<!DOCTYPE html>
<meta charset=utf-8>
<title>
Test chrome-only MutationObserver animation notifications for sync APIs
</title>
<script type="application/javascript" src="../testharness.js"></script>
<script type="application/javascript" src="../testharnessreport.js"></script>
<script type="application/javascript" src="../testcommon.js"></script>
<div id="log"></div>
<style>
@keyframes anim {
to { transform: translate(100px); }
}
@keyframes anotherAnim {
to { transform: translate(0px); }
}
</style>
<script>
function assert_record_list(actual, expected, desc, index, listName) {
assert_equals(actual.length, expected.length,
`${desc} - record[${index}].${listName} length`);
if (actual.length != expected.length) {
return;
}
for (var i = 0; i < actual.length; i++) {
assert_not_equals(actual.indexOf(expected[i]), -1,
`${desc} - record[${index}].${listName} contains expected Animation`);
}
}
function assert_equals_records(actual, expected, desc) {
assert_equals(actual.length, expected.length, `${desc} - number of records`);
if (actual.length != expected.length) {
return;
}
for (var i = 0; i < actual.length; i++) {
assert_record_list(actual[i].addedAnimations,
expected[i].added, desc, i, "addedAnimations");
assert_record_list(actual[i].changedAnimations,
expected[i].changed, desc, i, "changedAnimations");
assert_record_list(actual[i].removedAnimations,
expected[i].removed, desc, i, "removedAnimations");
}
}
// Create a pseudo element
function createPseudo(test, element, type) {
addStyle(test, { '@keyframes anim': '',
['.pseudo::' + type]: 'animation: anim 10s;' });
element.classList.add('pseudo');
var anims = document.getAnimations();
assert_true(anims.length >= 1);
var anim = anims[anims.length - 1];
assert_equals(anim.effect.target.parentElement, element);
assert_equals(anim.effect.target.type, '::' + type);
anim.cancel();
return anim.effect.target;
}
[ { subtree: false },
{ subtree: true }
].forEach(aOptions => {
test(t => {
var div = addDiv(t);
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var anim = div.animate({ opacity: [ 0, 1 ] }, 200 * MS_PER_SEC);
assert_equals_records(observer.takeRecords(),
[{ added: [anim], changed: [], removed: [] }],
"records after animation is added");
anim.effect.timing.duration = 100 * MS_PER_SEC;
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [anim], removed: [] }],
"records after duration is changed");
anim.effect.timing.duration = 100 * MS_PER_SEC;
assert_equals_records(observer.takeRecords(),
[], "records after assigning same value");
anim.currentTime = anim.effect.timing.duration * 2;
anim.finish();
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [], removed: [anim] }],
"records after animation end");
anim.effect.timing.duration = anim.effect.timing.duration * 3;
assert_equals_records(observer.takeRecords(),
[{ added: [anim], changed: [], removed: [] }],
"records after animation restarted");
anim.effect.timing.duration = "auto";
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [], removed: [anim] }],
"records after duration set \"auto\"");
anim.effect.timing.duration = "auto";
assert_equals_records(observer.takeRecords(),
[], "records after assigning same value \"auto\"");
}, "change_duration_and_currenttime");
test(t => {
var div = addDiv(t);
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var anim = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
assert_equals_records(observer.takeRecords(),
[{ added: [anim], changed: [], removed: [] }],
"records after animation is added");
anim.effect.timing.endDelay = 10 * MS_PER_SEC;
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [anim], removed: [] }],
"records after endDelay is changed");
anim.effect.timing.endDelay = 10 * MS_PER_SEC;
assert_equals_records(observer.takeRecords(),
[], "records after assigning same value");
anim.currentTime = 109 * MS_PER_SEC;
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [], removed: [anim] }],
"records after currentTime during endDelay");
anim.effect.timing.endDelay = -110 * MS_PER_SEC;
assert_equals_records(observer.takeRecords(),
[], "records after assigning negative value");
}, "change_enddelay_and_currenttime");
test(t => {
var div = addDiv(t);
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var anim = div.animate({ opacity: [ 0, 1 ] },
{ duration: 100 * MS_PER_SEC,
endDelay: -100 * MS_PER_SEC });
assert_equals_records(observer.takeRecords(),
[], "records after animation is added");
}, "zero_end_time");
test(t => {
var div = addDiv(t);
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var anim = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
assert_equals_records(observer.takeRecords(),
[{ added: [anim], changed: [], removed: [] }],
"records after animation is added");
anim.effect.timing.iterations = 2;
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [anim], removed: [] }],
"records after iterations is changed");
anim.effect.timing.iterations = 2;
assert_equals_records(observer.takeRecords(),
[], "records after assigning same value");
anim.effect.timing.iterations = 0;
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [], removed: [anim] }],
"records after animation end");
anim.effect.timing.iterations = Infinity;
assert_equals_records(observer.takeRecords(),
[{ added: [anim], changed: [], removed: [] }],
"records after animation restarted");
}, "change_iterations");
test(t => {
var div = addDiv(t);
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var anim = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
assert_equals_records(observer.takeRecords(),
[{ added: [anim], changed: [], removed: [] }],
"records after animation is added");
anim.effect.timing.delay = 100;
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [anim], removed: [] }],
"records after delay is changed");
anim.effect.timing.delay = 100;
assert_equals_records(observer.takeRecords(),
[], "records after assigning same value");
anim.effect.timing.delay = -100 * MS_PER_SEC;
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [], removed: [anim] }],
"records after animation end");
anim.effect.timing.delay = 0;
assert_equals_records(observer.takeRecords(),
[{ added: [anim], changed: [], removed: [] }],
"records after animation restarted");
}, "change_delay");
test(t => {
var div = addDiv(t);
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var anim = div.animate({ opacity: [ 0, 1 ] },
{ duration: 100 * MS_PER_SEC,
easing: "steps(2, start)" });
assert_equals_records(observer.takeRecords(),
[{ added: [anim], changed: [], removed: [] }],
"records after animation is added");
anim.effect.timing.easing = "steps(2, end)";
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [anim], removed: [] }],
"records after easing is changed");
anim.effect.timing.easing = "steps(2, end)";
assert_equals_records(observer.takeRecords(),
[], "records after assigning same value");
}, "change_easing");
test(t => {
var div = addDiv(t);
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var anim = div.animate({ opacity: [ 0, 1 ] },
{ duration: 100, delay: -100 });
assert_equals_records(observer.takeRecords(),
[], "records after assigning negative value");
}, "negative_delay_in_constructor");
test(t => {
var div = addDiv(t);
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var effect = new KeyframeEffectReadOnly(null,
{ opacity: [ 0, 1 ] },
{ duration: 100 * MS_PER_SEC });
var anim = new Animation(effect, document.timeline);
anim.play();
assert_equals_records(observer.takeRecords(),
[], "no records after animation is added");
}, "create_animation_without_target");
test(t => {
var div = addDiv(t);
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var anim = div.animate({ opacity: [ 0, 1 ] },
{ duration: 100 * MS_PER_SEC });
assert_equals_records(observer.takeRecords(),
[{ added: [anim], changed: [], removed: [] }],
"records after animation is added");
anim.effect.target = div;
assert_equals_records(observer.takeRecords(),
[], "no records after setting the same target");
anim.effect.target = null;
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [], removed: [anim] }],
"records after setting null");
anim.effect.target = null;
assert_equals_records(observer.takeRecords(),
[], "records after setting redundant null");
}, "set_redundant_animation_target");
test(t => {
var div = addDiv(t);
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var anim = div.animate({ opacity: [ 0, 1 ] },
{ duration: 100 * MS_PER_SEC });
assert_equals_records(observer.takeRecords(),
[{ added: [anim], changed: [], removed: [] }],
"records after animation is added");
anim.effect = null;
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [], removed: [anim] }],
"records after animation is removed");
}, "set_null_animation_effect");
test(t => {
var div = addDiv(t);
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var anim = new Animation();
anim.play();
anim.effect = new KeyframeEffect(div, { opacity: [ 0, 1 ] },
100 * MS_PER_SEC);
assert_equals_records(observer.takeRecords(),
[{ added: [anim], changed: [], removed: [] }],
"records after animation is added");
}, "set_effect_on_null_effect_animation");
test(t => {
var div = addDiv(t);
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var anim = div.animate({ marginLeft: [ "0px", "100px" ] },
100 * MS_PER_SEC);
assert_equals_records(observer.takeRecords(),
[{ added: [anim], changed: [], removed: [] }],
"records after animation is added");
anim.effect = new KeyframeEffect(div, { opacity: [ 0, 1 ] },
100 * MS_PER_SEC);
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [anim], removed: [] }],
"records after replace effects");
}, "replace_effect_targeting_on_the_same_element");
test(t => {
var div = addDiv(t);
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var anim = div.animate({ marginLeft: [ "0px", "100px" ] },
100 * MS_PER_SEC);
assert_equals_records(observer.takeRecords(),
[{ added: [anim], changed: [], removed: [] }],
"records after animation is added");
anim.currentTime = 60 * MS_PER_SEC;
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [anim], removed: [] }],
"records after animation is changed");
anim.effect = new KeyframeEffect(div, { opacity: [ 0, 1 ] },
50 * MS_PER_SEC);
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [], removed: [anim] }],
"records after replacing effects");
}, "replace_effect_targeting_on_the_same_element_not_in_effect");
test(t => {
var div = addDiv(t);
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var anim = div.animate([ { marginLeft: "0px" },
{ marginLeft: "-20px" },
{ marginLeft: "100px" },
{ marginLeft: "50px" } ],
{ duration: 100 * MS_PER_SEC });
assert_equals_records(observer.takeRecords(),
[{ added: [anim], changed: [], removed: [] }],
"records after animation is added");
anim.effect.spacing = "paced(margin-left)";
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [anim], removed: [] }],
"records after animation is changed");
}, "set_spacing");
test(t => {
var div = addDiv(t);
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var anim = div.animate([ { marginLeft: "0px" },
{ marginLeft: "-20px" },
{ marginLeft: "100px" },
{ marginLeft: "50px" } ],
{ duration: 100 * MS_PER_SEC,
spacing: "paced(margin-left)" });
assert_equals_records(observer.takeRecords(),
[{ added: [anim], changed: [], removed: [] }],
"records after animation is added");
anim.effect.spacing = "paced(animation-duration)";
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [anim], removed: [] }],
"records after setting a non-animatable paced property");
}, "set_spacing_on_a_non-animatable_property");
test(t => {
var div = addDiv(t);
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var anim = div.animate([ { marginLeft: "0px" },
{ marginLeft: "-20px" },
{ marginLeft: "100px" },
{ marginLeft: "50px" } ],
{ duration: 100 * MS_PER_SEC,
spacing: "paced(margin-left)" });
assert_equals_records(observer.takeRecords(),
[{ added: [anim], changed: [], removed: [] }],
"records after animation is added");
anim.effect.spacing = "paced(margin-left)";
assert_equals_records(observer.takeRecords(),
[], "no record after setting the same spacing");
}, "set_the_same_spacing");
// Test that starting a single animation that is cancelled by calling
// cancel() dispatches an added notification and then a removed
// notification.
test(t => {
var div = addDiv(t, { style: "animation: anim 100s forwards" });
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var animations = div.getAnimations();
assert_equals(animations.length, 1,
"getAnimations().length after animation start");
assert_equals_records(observer.takeRecords(),
[{ added: animations, changed: [], removed: [] }],
"records after animation start");
animations[0].cancel();
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [], removed: animations }],
"records after animation end");
// Re-trigger the animation.
animations[0].play();
// Single MutationRecord for the Animation (re-)addition.
assert_equals_records(observer.takeRecords(),
[{ added: animations, changed: [], removed: [] }],
"records after animation start");
}, "single_animation_cancelled_api");
// Test that updating a property on the Animation object dispatches a changed
// notification.
[
{ prop: "playbackRate", val: 0.5 },
{ prop: "startTime", val: 50 * MS_PER_SEC },
{ prop: "currentTime", val: 50 * MS_PER_SEC },
].forEach(function(aChangeTest) {
test(t => {
// We use a forwards fill mode so that even if the change we make causes
// the animation to become finished, it will still be "relevant" so we
// won't mark it as removed.
var div = addDiv(t, { style: "animation: anim 100s forwards" });
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var animations = div.getAnimations();
assert_equals(animations.length, 1,
"getAnimations().length after animation start");
assert_equals_records(observer.takeRecords(),
[{ added: animations, changed: [], removed: [] }],
"records after animation start");
// Update the property.
animations[0][aChangeTest.prop] = aChangeTest.val;
// Make a redundant change.
animations[0][aChangeTest.prop] = animations[0][aChangeTest.prop];
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: animations, removed: [] }],
"records after animation property change");
}, `single_animation_api_change_${aChangeTest.prop}`);
});
// Test that making a redundant change to currentTime while an Animation
// is pause-pending still generates a change MutationRecord since setting
// the currentTime to any value in this state aborts the pending pause.
test(t => {
var div = addDiv(t, { style: "animation: anim 100s" });
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var animations = div.getAnimations();
assert_equals(animations.length, 1,
"getAnimations().length after animation start");
assert_equals_records(observer.takeRecords(),
[{ added: animations, changed: [], removed: [] }],
"records after animation start");
animations[0].pause();
// We are now pause-pending. Even if we make a redundant change to the
// currentTime, we should still get a change record because setting the
// currentTime while pause-pending has the effect of cancelling a pause.
animations[0].currentTime = animations[0].currentTime;
// Two MutationRecords for the Animation changes: one for pausing, one
// for aborting the pause.
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: animations, removed: [] },
{ added: [], changed: animations, removed: [] }],
"records after pausing then seeking");
}, "change_currentTime_while_pause_pending");
// Test that calling finish() on a forwards-filling Animation dispatches
// a changed notification.
test(t => {
var div = addDiv(t, { style: "animation: anim 100s forwards" });
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var animations = div.getAnimations();
assert_equals(animations.length, 1,
"getAnimations().length after animation start");
assert_equals_records(observer.takeRecords(),
[{ added: animations, changed: [], removed: [] }],
"records after animation start");
animations[0].finish();
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: animations, removed: [] }],
"records after finish()");
// Redundant finish.
animations[0].finish();
// Ensure no change records.
assert_equals_records(observer.takeRecords(),
[], "records after redundant finish()");
}, "finish_with_forwards_fill");
// Test that calling finish() on an Animation that does not fill forwards,
// dispatches a removal notification.
test(t => {
var div = addDiv(t, { style: "animation: anim 100s" });
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var animations = div.getAnimations();
assert_equals(animations.length, 1,
"getAnimations().length after animation start");
assert_equals_records(observer.takeRecords(),
[{ added: animations, changed: [], removed: [] }],
"records after animation start");
animations[0].finish();
// Single MutationRecord for the Animation removal.
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [], removed: animations }],
"records after finishing");
}, "finish_without_fill");
// Test that calling finish() on a forwards-filling Animation dispatches
test(t => {
var div = addDiv(t, { style: "animation: anim 100s" });
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var animation = div.getAnimations()[0];
assert_equals_records(observer.takeRecords(),
[{ added: [animation], changed: [], removed: []}],
"records after creation");
animation.id = "new id";
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [animation], removed: []}],
"records after id is changed");
animation.id = "new id";
assert_equals_records(observer.takeRecords(),
[], "records after assigning same value with id");
}, "change_id");
// Test that calling reverse() dispatches a changed notification.
test(t => {
var div = addDiv(t, { style: "animation: anim 100s both" });
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var animations = div.getAnimations();
assert_equals(animations.length, 1,
"getAnimations().length after animation start");
assert_equals_records(observer.takeRecords(),
[{ added: animations, changed: [], removed: [] }],
"records after animation start");
animations[0].reverse();
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: animations, removed: [] }],
"records after calling reverse()");
}, "reverse");
// Test that calling reverse() does *not* dispatch a changed notification
// when playbackRate == 0.
test(t => {
var div = addDiv(t, { style: "animation: anim 100s both" });
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var animations = div.getAnimations();
assert_equals(animations.length, 1,
"getAnimations().length after animation start");
assert_equals_records(observer.takeRecords(),
[{ added: animations, changed: [], removed: [] }],
"records after animation start");
// Seek to the middle and set playbackRate to zero.
animations[0].currentTime = 50 * MS_PER_SEC;
animations[0].playbackRate = 0;
// Two MutationRecords, one for each change.
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: animations, removed: [] },
{ added: [], changed: animations, removed: [] }],
"records after seeking and setting playbackRate");
animations[0].reverse();
// We should get no notifications.
assert_equals_records(observer.takeRecords(),
[], "records after calling reverse()");
}, "reverse_with_zero_playbackRate");
// Test that attempting to start an animation that should already be finished
// does not send any notifications.
test(t => {
// Start an animation that should already be finished.
var div = addDiv(t, { style: "animation: anim 1s -2s;" });
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
// The animation should cause no Animations to be created.
var animations = div.getAnimations();
assert_equals(animations.length, 0,
"getAnimations().length after animation start");
// And we should get no notifications.
assert_equals_records(observer.takeRecords(),
[], "records after attempted animation start");
}, "already_finished");
test(t => {
var div = addDiv(t, { style: "animation: anim 100s, anotherAnim 100s" });
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var animations = div.getAnimations();
assert_equals_records(observer.takeRecords(),
[{ added: animations, changed: [], removed: []}],
"records after creation");
div.style.animation = "anotherAnim 100s, anim 100s";
animations = div.getAnimations();
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: animations, removed: []}],
"records after the order is changed");
div.style.animation = "anotherAnim 100s, anim 100s";
assert_equals_records(observer.takeRecords(),
[], "no records after applying the same order");
}, "animtion_order_change");
});
test(t => {
var div = addDiv(t);
var observer = setupSynchronousObserver(t, div, true);
var child = document.createElement("div");
div.appendChild(child);
var anim1 = div.animate({ marginLeft: [ "0px", "50px" ] },
100 * MS_PER_SEC);
var anim2 = child.animate({ marginLeft: [ "0px", "100px" ] },
50 * MS_PER_SEC);
assert_equals_records(observer.takeRecords(),
[{ added: [anim1], changed: [], removed: [] },
{ added: [anim2], changed: [], removed: [] }],
"records after animation is added");
// After setting a new effect, we remove the current animation, anim1,
// because it is no longer attached to |div|, and then remove the previous
// animation, anim2. Finally, add back the anim1 which is in effect on
// |child| now. In addition, we sort them by tree order and they are
// batched.
anim1.effect = anim2.effect;
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [], removed: [anim1] }, // div
{ added: [anim1], changed: [], removed: [anim2] }], // child
"records after animation effects are changed");
}, "set_effect_with_previous_animation");
test(t => {
var div = addDiv(t);
var observer = setupSynchronousObserver(t, document, true);
var anim = div.animate({ opacity: [ 0, 1 ] },
{ duration: 100 * MS_PER_SEC });
var newTarget = document.createElement("div");
assert_equals_records(observer.takeRecords(),
[{ added: [anim], changed: [], removed: [] }],
"records after animation is added");
anim.effect.target = null;
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [], removed: [anim] }],
"records after setting null");
anim.effect.target = div;
assert_equals_records(observer.takeRecords(),
[{ added: [anim], changed: [], removed: [] }],
"records after setting a target");
anim.effect.target = addDiv(t);
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [], removed: [anim] },
{ added: [anim], changed: [], removed: [] }],
"records after setting a different target");
}, "set_animation_target");
test(t => {
var div = addDiv(t);
var pseudoTarget = createPseudo(t, div, 'before');
var observer = setupSynchronousObserver(t, div, true);
var anim = pseudoTarget.animate({ opacity: [ 0, 1 ] }, 200 * MS_PER_SEC);
assert_equals_records(observer.takeRecords(),
[{ added: [anim], changed: [], removed: [] }],
"records after animation is added");
anim.effect.timing.duration = 100 * MS_PER_SEC;
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [anim], removed: [] }],
"records after duration is changed");
anim.effect.timing.duration = 100 * MS_PER_SEC;
assert_equals_records(observer.takeRecords(),
[], "records after assigning same value");
anim.currentTime = anim.effect.timing.duration * 2;
anim.finish();
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [], removed: [anim] }],
"records after animation end");
anim.effect.timing.duration = anim.effect.timing.duration * 3;
assert_equals_records(observer.takeRecords(),
[{ added: [anim], changed: [], removed: [] }],
"records after animation restarted");
anim.effect.timing.duration = "auto";
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [], removed: [anim] }],
"records after duration set \"auto\"");
anim.effect.timing.duration = "auto";
assert_equals_records(observer.takeRecords(),
[], "records after assigning same value \"auto\"");
}, "change_duration_and_currenttime_on_pseudo_elements");
test(t => {
var div = addDiv(t);
var pseudoTarget = createPseudo(t, div, 'before');
var observer = setupSynchronousObserver(t, div, false);
var anim = div.animate({ opacity: [ 0, 1 ] },
{ duration: 100 * MS_PER_SEC });
var pAnim = pseudoTarget.animate({ opacity: [ 0, 1 ] },
{ duration: 100 * MS_PER_SEC });
assert_equals_records(observer.takeRecords(),
[{ added: [anim], changed: [], removed: [] }],
"records after animation is added");
anim.finish();
pAnim.finish();
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [], removed: [anim] }],
"records after animation is finished");
}, "exclude_animations_targeting_pseudo_elements");
</script>

View file

@ -0,0 +1,815 @@
<!doctype html>
<head>
<meta charset=utf-8>
<title>Tests restyles caused by animations</title>
<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
<script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
<script src="chrome://mochikit/content/tests/SimpleTest/paint_listener.js"></script>
<script src="../testcommon.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<style>
@keyframes opacity {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes background-color {
from { background-color: red; }
to { background-color: blue; }
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
div {
/* Element needs geometry to be eligible for layerization */
width: 100px;
height: 100px;
background-color: white;
}
</style>
</head>
<body>
<script>
'use strict';
function observeStyling(frameCount, onFrame) {
var docShell = window.QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor)
.getInterface(SpecialPowers.Ci.nsIWebNavigation)
.QueryInterface(SpecialPowers.Ci.nsIDocShell);
docShell.recordProfileTimelineMarkers = true;
docShell.popProfileTimelineMarkers();
return new Promise(function(resolve) {
return waitForAnimationFrames(frameCount, onFrame).then(function() {
var markers = docShell.popProfileTimelineMarkers();
docShell.recordProfileTimelineMarkers = false;
var stylingMarkers = markers.filter(function(marker, index) {
return marker.name == 'Styles' &&
(marker.restyleHint == 'eRestyle_CSSAnimations' ||
marker.restyleHint == 'eRestyle_CSSTransitions');
});
resolve(stylingMarkers);
});
});
}
function ensureElementRemoval(aElement) {
return new Promise(function(resolve) {
aElement.remove();
waitForAllPaintsFlushed(resolve);
});
}
SimpleTest.waitForExplicitFinish();
var omtaEnabled = isOMTAEnabled();
var isAndroid = !!navigator.userAgent.includes("Android");
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(function() {
add_task(function* restyling_for_main_thread_animations() {
var div = addDiv(null, { style: 'animation: background-color 100s' });
var animation = div.getAnimations()[0];
yield animation.ready;
ok(!animation.isRunningOnCompositor);
var markers = yield observeStyling(5);
is(markers.length, 5,
'CSS animations running on the main-thread should update style ' +
'on the main thread');
yield ensureElementRemoval(div);
});
add_task_if_omta_enabled(function* no_restyling_for_compositor_animations() {
var div = addDiv(null, { style: 'animation: opacity 100s' });
var animation = div.getAnimations()[0];
yield animation.ready;
ok(animation.isRunningOnCompositor);
var markers = yield observeStyling(5);
is(markers.length, 0,
'CSS animations running on the compositor should not update style ' +
'on the main thread');
yield ensureElementRemoval(div);
});
add_task_if_omta_enabled(function* no_restyling_for_compositor_transitions() {
var div = addDiv(null, { style: 'transition: opacity 100s; opacity: 0' });
getComputedStyle(div).opacity;
div.style.opacity = 1;
var animation = div.getAnimations()[0];
yield animation.ready;
ok(animation.isRunningOnCompositor);
var markers = yield observeStyling(5);
is(markers.length, 0,
'CSS transitions running on the compositor should not update style ' +
'on the main thread');
yield ensureElementRemoval(div);
});
add_task_if_omta_enabled(function* no_restyling_when_animation_duration_is_changed() {
var div = addDiv(null, { style: 'animation: opacity 100s' });
var animation = div.getAnimations()[0];
yield animation.ready;
ok(animation.isRunningOnCompositor);
div.animationDuration = '200s';
var markers = yield observeStyling(5);
is(markers.length, 0,
'Animations running on the compositor should not update style ' +
'on the main thread');
yield ensureElementRemoval(div);
});
add_task_if_omta_enabled(function* only_one_restyling_after_finish_is_called() {
var div = addDiv(null, { style: 'animation: opacity 100s' });
var animation = div.getAnimations()[0];
yield animation.ready;
ok(animation.isRunningOnCompositor);
animation.finish();
var markers = yield observeStyling(5);
is(markers.length, 1,
'Animations running on the compositor should only update style ' +
'once after finish() is called');
yield ensureElementRemoval(div);
});
add_task(function* no_restyling_mouse_movement_on_finished_transition() {
var div = addDiv(null, { style: 'transition: opacity 1ms; opacity: 0' });
getComputedStyle(div).opacity;
div.style.opacity = 1;
var animation = div.getAnimations()[0];
var initialRect = div.getBoundingClientRect();
yield animation.finished;
var mouseX = initialRect.left + initialRect.width / 2;
var mouseY = initialRect.top + initialRect.height / 2;
var markers = yield observeStyling(5, function() {
// We can't use synthesizeMouse here since synthesizeMouse causes
// layout flush.
synthesizeMouseAtPoint(mouseX++, mouseY++,
{ type: 'mousemove' }, window);
});
is(markers.length, 0,
'Bug 1219236: Finished transitions should never cause restyles ' +
'when mouse is moved on the animations');
yield ensureElementRemoval(div);
});
add_task(function* no_restyling_mouse_movement_on_finished_animation() {
var div = addDiv(null, { style: 'animation: opacity 1ms' });
var animation = div.getAnimations()[0];
var initialRect = div.getBoundingClientRect();
yield animation.finished;
var mouseX = initialRect.left + initialRect.width / 2;
var mouseY = initialRect.top + initialRect.height / 2;
var markers = yield observeStyling(5, function() {
// We can't use synthesizeMouse here since synthesizeMouse causes
// layout flush.
synthesizeMouseAtPoint(mouseX++, mouseY++,
{ type: 'mousemove' }, window);
});
is(markers.length, 0,
'Bug 1219236: Finished animations should never cause restyles ' +
'when mouse is moved on the animations');
yield ensureElementRemoval(div);
});
add_task_if_omta_enabled(function* no_restyling_compositor_animations_out_of_view_element() {
if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
return;
}
var div = addDiv(null,
{ style: 'animation: opacity 100s; transform: translateY(-400px);' });
var animation = div.getAnimations()[0];
yield animation.ready;
ok(!animation.isRunningOnCompositor);
var markers = yield observeStyling(5);
is(markers.length, 0,
'Animations running on the compositor in an out-of-view element ' +
'should never cause restyles');
yield ensureElementRemoval(div);
});
add_task(function* no_restyling_main_thread_animations_out_of_view_element() {
if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
return;
}
var div = addDiv(null,
{ style: 'animation: background-color 100s; transform: translateY(-400px);' });
var animation = div.getAnimations()[0];
yield animation.ready;
var markers = yield observeStyling(5);
is(markers.length, 0,
'Animations running on the main-thread in an out-of-view element ' +
'should never cause restyles');
yield ensureElementRemoval(div);
});
add_task_if_omta_enabled(function* no_restyling_compositor_animations_in_scrolled_out_element() {
if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
return;
}
/*
On Android the opacity animation runs on the compositor even if it is
scrolled out of view. We will fix this in bug 1247800.
*/
if (isAndroid) {
return;
}
var parentElement = addDiv(null,
{ style: 'overflow-y: scroll; height: 20px;' });
var div = addDiv(null,
{ style: 'animation: opacity 100s; position: relative; top: 100px;' });
parentElement.appendChild(div);
var animation = div.getAnimations()[0];
yield animation.ready;
var markers = yield observeStyling(5);
is(markers.length, 0,
'Animations running on the compositor for elements ' +
'which are scrolled out should never cause restyles');
yield ensureElementRemoval(parentElement);
});
add_task(function* no_restyling_main_thread_animations_in_scrolled_out_element() {
if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
return;
}
/*
On Android throttled animations are left behind on the main thread in some
frames, We will fix this in bug 1247800.
*/
if (isAndroid) {
return;
}
var parentElement = addDiv(null,
{ style: 'overflow-y: scroll; height: 20px;' });
var div = addDiv(null,
{ style: 'animation: background-color 100s; position: relative; top: 100px;' });
parentElement.appendChild(div);
var animation = div.getAnimations()[0];
yield animation.ready;
var markers = yield observeStyling(5);
is(markers.length, 0,
'Animations running on the main-thread for elements ' +
'which are scrolled out should never cause restyles');
yield ensureElementRemoval(parentElement);
});
add_task(function* no_restyling_main_thread_animations_in_nested_scrolled_out_element() {
if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
return;
}
/*
On Android throttled animations are left behind on the main thread in some
frames, We will fix this in bug 1247800.
*/
if (isAndroid) {
return;
}
var grandParent = addDiv(null,
{ style: 'overflow-y: scroll; height: 20px;' });
var parentElement = addDiv(null,
{ style: 'overflow-y: scroll; height: 100px;' });
var div = addDiv(null,
{ style: 'animation: background-color 100s; position: relative; top: 100px;' });
grandParent.appendChild(parentElement);
parentElement.appendChild(div);
var animation = div.getAnimations()[0];
yield animation.ready;
var markers = yield 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');
yield ensureElementRemoval(grandParent);
});
add_task_if_omta_enabled(function* no_restyling_compositor_animations_in_visiblily_hidden_element() {
var div = addDiv(null,
{ style: 'animation: opacity 100s; visibility: hidden' });
var animation = div.getAnimations()[0];
yield animation.ready;
ok(!animation.isRunningOnCompositor);
var markers = yield observeStyling(5);
todo_is(markers.length, 0,
'Bug 1237454: Animations running on the compositor in ' +
'visibility hidden element should never cause restyles');
yield ensureElementRemoval(div);
});
add_task(function* restyling_main_thread_animations_moved_in_view_by_scrolling() {
if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
return;
}
/*
On Android throttled animations are left behind on the main thread in some
frames, We will fix this in bug 1247800.
*/
if (isAndroid) {
return;
}
var parentElement = addDiv(null,
{ style: 'overflow-y: scroll; height: 20px;' });
var div = addDiv(null,
{ style: 'animation: background-color 100s; position: relative; top: 100px;' });
parentElement.appendChild(div);
var animation = div.getAnimations()[0];
var parentRect = parentElement.getBoundingClientRect();
var centerX = parentRect.left + parentRect.width / 2;
var centerY = parentRect.top + parentRect.height / 2;
yield animation.ready;
var markers = yield observeStyling(1, function() {
// We can't use synthesizeWheel here since synthesizeWheel causes
// layout flush.
synthesizeWheelAtPoint(centerX, centerY,
{ deltaMode: WheelEvent.DOM_DELTA_PIXEL,
deltaY: 100 });
});
is(markers.length, 1,
'Animations running on the main-thread which were in scrolled out ' +
'elements should update restyling soon after the element moved in ' +
'view by scrolling');
yield ensureElementRemoval(parentElement);
});
add_task(function* restyling_main_thread_animations_moved_in_view_by_scrolling() {
if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
return;
}
var grandParent = addDiv(null,
{ style: 'overflow-y: scroll; height: 20px;' });
var parentElement = addDiv(null,
{ style: 'overflow-y: scroll; height: 200px;' });
var div = addDiv(null,
{ style: 'animation: background-color 100s; position: relative; top: 100px;' });
grandParent.appendChild(parentElement);
parentElement.appendChild(div);
var animation = div.getAnimations()[0];
var parentRect = grandParent.getBoundingClientRect();
var centerX = parentRect.left + parentRect.width / 2;
var centerY = parentRect.top + parentRect.height / 2;
yield animation.ready;
var markers = yield observeStyling(1, function() {
// We can't use synthesizeWheel here since synthesizeWheel causes
// layout flush.
synthesizeWheelAtPoint(centerX, centerY,
{ deltaMode: WheelEvent.DOM_DELTA_PIXEL,
deltaY: 100 });
});
// FIXME: We should reduce a redundant restyle here.
ok(markers.length >= 1,
'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');
yield ensureElementRemoval(grandParent);
});
add_task(function* restyling_main_thread_animations_move_out_of_view_by_scrolling() {
if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
return;
}
/*
On Android throttled animations are left behind on the main thread in some
frames, We will fix this in bug 1247800.
*/
if (isAndroid) {
return;
}
var parentElement = addDiv(null,
{ style: 'overflow-y: scroll; height: 200px;' });
var div = addDiv(null,
{ style: 'animation: background-color 100s;' });
var pad = addDiv(null,
{ style: 'height: 400px;' });
parentElement.appendChild(div);
parentElement.appendChild(pad);
var animation = div.getAnimations()[0];
var parentRect = parentElement.getBoundingClientRect();
var centerX = parentRect.left + parentRect.width / 2;
var centerY = parentRect.top + parentRect.height / 2;
yield animation.ready;
// We can't use synthesizeWheel here since synthesizeWheel causes
// layout flush.
synthesizeWheelAtPoint(centerX, centerY,
{ deltaMode: WheelEvent.DOM_DELTA_PIXEL,
deltaY: 200 });
var markers = yield 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');
yield ensureElementRemoval(parentElement);
});
add_task(function* restyling_main_thread_animations_moved_in_view_by_resizing() {
if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
return;
}
var parentElement = addDiv(null,
{ style: 'overflow-y: scroll; height: 20px;' });
var div = addDiv(null,
{ style: 'animation: background-color 100s; position: relative; top: 100px;' });
parentElement.appendChild(div);
var animation = div.getAnimations()[0];
yield animation.ready;
var markers = yield observeStyling(1, function() {
parentElement.style.height = '100px';
});
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');
yield ensureElementRemoval(parentElement);
});
add_task(function* no_restyling_main_thread_animations_in_visiblily_hidden_element() {
var div = addDiv(null,
{ style: 'animation: background-color 100s; visibility: hidden' });
var animation = div.getAnimations()[0];
yield animation.ready;
var markers = yield observeStyling(5);
todo_is(markers.length, 0,
'Bug 1237454: Animations running on the main-thread in ' +
'visibility hidden element should never cause restyles');
yield ensureElementRemoval(div);
});
add_task_if_omta_enabled(function* no_restyling_compositor_animations_after_pause_is_called() {
var div = addDiv(null, { style: 'animation: opacity 100s' });
var animation = div.getAnimations()[0];
yield animation.ready;
ok(animation.isRunningOnCompositor);
animation.pause();
yield animation.ready;
var markers = yield observeStyling(5);
is(markers.length, 0,
'Bug 1232563: Paused animations running on the compositor should ' +
'never cause restyles once after pause() is called');
yield ensureElementRemoval(div);
});
add_task(function* no_restyling_main_thread_animations_after_pause_is_called() {
var div = addDiv(null, { style: 'animation: background-color 100s' });
var animation = div.getAnimations()[0];
yield animation.ready;
animation.pause();
yield animation.ready;
var markers = yield observeStyling(5);
is(markers.length, 0,
'Bug 1232563: Paused animations running on the main-thread should ' +
'never cause restyles after pause() is called');
yield ensureElementRemoval(div);
});
add_task_if_omta_enabled(function* only_one_restyling_when_current_time_is_set_to_middle_of_duration() {
var div = addDiv(null, { style: 'animation: opacity 100s' });
var animation = div.getAnimations()[0];
yield animation.ready;
animation.currentTime = 50 * MS_PER_SEC;
var markers = yield 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');
yield ensureElementRemoval(div);
});
add_task_if_omta_enabled(function* change_duration_and_currenttime() {
var div = addDiv(null);
var animation = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
yield animation.ready;
ok(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(!animation.isRunningOnCompositor);
// Extend the duration.
animation.effect.timing.duration = 800 * MS_PER_SEC;
var markers = yield observeStyling(5);
is(markers.length, 1,
'Animations running on the compositor should update style ' +
'when timing.duration is made longer than the current time');
yield ensureElementRemoval(div);
});
add_task(function* script_animation_on_display_none_element() {
var div = addDiv(null);
var animation = div.animate({ backgroundColor: [ 'red', 'blue' ] },
100 * MS_PER_SEC);
yield animation.ready;
div.style.display = 'none';
// We need to wait a frame to apply display:none style.
yield waitForFrame();
is(animation.playState, 'running',
'Script animations keep running even when the target element has ' +
'"display: none" style');
ok(!animation.isRunningOnCompositor,
'Script animations on "display:none" element should not run on the ' +
'compositor');
var markers = yield observeStyling(5);
is(markers.length, 0,
'Script animations on "display: none" element should not update styles');
div.style.display = '';
// We need to wait a frame to unapply display:none style.
yield waitForFrame();
var markers = yield observeStyling(5);
is(markers.length, 5,
'Script animations restored from "display: none" state should update ' +
'styles');
yield ensureElementRemoval(div);
});
add_task_if_omta_enabled(function* compositable_script_animation_on_display_none_element() {
var div = addDiv(null);
var animation = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
yield animation.ready;
div.style.display = 'none';
// We need to wait a frame to apply display:none style.
yield waitForFrame();
is(animation.playState, 'running',
'Opacity script animations keep running even when the target element ' +
'has "display: none" style');
ok(!animation.isRunningOnCompositor,
'Opacity script animations on "display:none" element should not ' +
'run on the compositor');
var markers = yield observeStyling(5);
is(markers.length, 0,
'Opacity script animations on "display: none" element should not ' +
'update styles');
div.style.display = '';
// We need to wait a frame to unapply display:none style.
yield waitForFrame();
ok(animation.isRunningOnCompositor,
'Opacity script animations restored from "display: none" should be ' +
'run on the compositor');
yield ensureElementRemoval(div);
});
add_task(function* restyling_for_empty_keyframes() {
var div = addDiv(null);
var animation = div.animate({ }, 100 * MS_PER_SEC);
yield animation.ready;
var markers = yield observeStyling(5);
is(markers.length, 0,
'Animations with no keyframes should not cause restyles');
animation.effect.setKeyframes({ backgroundColor: ['red', 'blue'] });
markers = yield observeStyling(5);
is(markers.length, 5,
'Setting valid keyframes should cause regular animation restyles to ' +
'occur');
animation.effect.setKeyframes({ });
markers = yield observeStyling(5);
is(markers.length, 1,
'Setting an empty set of keyframes should trigger a single restyle ' +
'to remove the previous animated style');
yield ensureElementRemoval(div);
});
add_task_if_omta_enabled(function* no_restyling_when_animation_style_when_re_setting_same_animation_property() {
var div = addDiv(null, { style: 'animation: opacity 100s' });
var animation = div.getAnimations()[0];
yield animation.ready;
ok(animation.isRunningOnCompositor);
// Apply the same animation style
div.style.animation = 'opacity 100s';
var markers = yield observeStyling(5);
is(markers.length, 0,
'Applying same animation style ' +
'should never cause restyles');
yield ensureElementRemoval(div);
});
add_task(function* necessary_update_should_be_invoked() {
var div = addDiv(null, { style: 'animation: background-color 100s' });
var animation = div.getAnimations()[0];
yield animation.ready;
yield waitForAnimationFrames(5);
// Apply another animation style
div.style.animation = 'background-color 110s';
var animation = div.getAnimations()[0];
var markers = yield observeStyling(5);
is(markers.length, 5,
'Applying animation style with different duration ' +
'should cause restyles on every frame.');
yield ensureElementRemoval(div);
});
add_task_if_omta_enabled(
function* changing_cascading_result_for_main_thread_animation() {
var div = addDiv(null, { style: 'background-color: blue' });
var animation = div.animate({ opacity: [0, 1],
backgroundColor: ['green', 'red'] },
100 * MS_PER_SEC);
yield animation.ready;
ok(animation.isRunningOnCompositor,
'The opacity animation is running on the compositor');
// Make the background-color style as !important to cause an update
// to the cascade.
// Bug 1300982: The background-color animation should be no longer
// running on the main thread.
div.style.setProperty('background-color', '1', 'important');
var markers = yield 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');
yield ensureElementRemoval(div);
}
);
add_task(function* restyling_for_animation_on_orphaned_element() {
var div = addDiv(null);
var animation = div.animate({ marginLeft: [ '0px', '100px' ] },
100 * MS_PER_SEC);
yield animation.ready;
div.remove();
var markers = yield observeStyling(5);
is(markers.length, 0,
'Animation on orphaned element should not cause restyles');
document.body.appendChild(div);
markers = yield observeStyling(1);
// We are observing restyles in rAF callback which is processed before
// restyling process in each frame, so in the first frame there should be
// no observed restyle since we don't process restyle while the element
// is not attached to the document.
is(markers.length, 0,
'We observe no restyle in the first frame right after re-atatching ' +
'to the document');
markers = yield observeStyling(5);
is(markers.length, 5,
'Animation on re-attached to the document begins to update style');
yield 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.
function* restyling_for_opacity_animation_on_re_attached_element() {
var div = addDiv(null, { style: 'opacity: 1 ! important' });
var animation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
yield animation.ready;
ok(!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();
var markers = yield 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.
yield waitForFrame();
ok(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');
yield ensureElementRemoval(div);
}
);
});
</script>
</body>

View file

@ -0,0 +1,966 @@
<!doctype html>
<head>
<meta charset=utf-8>
<title>Bug 1045994 - Add a chrome-only property to inspect if an animation is
running on the compositor or not</title>
<script type="application/javascript" src="../testharness.js"></script>
<script type="application/javascript" src="../testharnessreport.js"></script>
<script type="application/javascript" src="../testcommon.js"></script>
<style>
@keyframes anim {
to { transform: translate(100px) }
}
@keyframes transform-starts-with-none {
0% { transform: none }
99% { transform: none }
100% { transform: translate(100px) }
}
@keyframes opacity {
to { opacity: 0 }
}
@keyframes background_and_translate {
to { background-color: red; transform: translate(100px); }
}
@keyframes background {
to { background-color: red; }
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes rotate-and-opacity {
from { transform: rotate(0deg); opacity: 1;}
to { transform: rotate(360deg); opacity: 0;}
}
div {
/* Element needs geometry to be eligible for layerization */
width: 100px;
height: 100px;
background-color: white;
}
</style>
</head>
<body>
<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1045994"
target="_blank">Mozilla Bug 1045994</a>
<div id="log"></div>
<script>
'use strict';
/** Test for bug 1045994 - Add a chrome-only property to inspect if an
animation is running on the compositor or not **/
var omtaEnabled = isOMTAEnabled();
function assert_animation_is_running_on_compositor(animation, desc) {
assert_equals(animation.isRunningOnCompositor, omtaEnabled,
desc + ' at ' + animation.currentTime + 'ms');
}
function assert_animation_is_not_running_on_compositor(animation, desc) {
assert_equals(animation.isRunningOnCompositor, false,
desc + ' at ' + animation.currentTime + 'ms');
}
promise_test(function(t) {
// FIXME: When we implement Element.animate, use that here instead of CSS
// so that we remove any dependency on the CSS mapping.
var div = addDiv(t, { style: 'animation: anim 100s' });
var animation = div.getAnimations()[0];
return animation.ready.then(function() {
assert_animation_is_running_on_compositor(animation,
'Animation reports that it is running on the compositor'
+ ' during playback');
div.style.animationPlayState = 'paused';
return animation.ready;
}).then(function() {
assert_animation_is_not_running_on_compositor(animation,
'Animation reports that it is NOT running on the compositor'
+ ' when paused');
});
}, '');
promise_test(function(t) {
var div = addDiv(t, { style: 'animation: background 100s' });
var animation = div.getAnimations()[0];
return animation.ready.then(function() {
assert_animation_is_not_running_on_compositor(animation,
'Animation reports that it is NOT running on the compositor'
+ ' for animation of "background"');
});
}, 'isRunningOnCompositor is false for animation of "background"');
promise_test(function(t) {
var div = addDiv(t, { style: 'animation: background_and_translate 100s' });
var animation = div.getAnimations()[0];
return animation.ready.then(function() {
assert_animation_is_running_on_compositor(animation,
'Animation reports that it is running on the compositor'
+ ' when the animation has two properties, where one can run'
+ ' on the compositor, the other cannot');
});
}, 'isRunningOnCompositor is true if the animation has at least one ' +
'property can run on compositor');
promise_test(function(t) {
var div = addDiv(t, { style: 'animation: anim 100s' });
var animation = div.getAnimations()[0];
return animation.ready.then(function() {
animation.pause();
return animation.ready;
}).then(function() {
assert_animation_is_not_running_on_compositor(animation,
'Animation reports that it is NOT running on the compositor'
+ ' when animation.pause() is called');
});
}, 'isRunningOnCompositor is false when the animation.pause() is called');
promise_test(function(t) {
var div = addDiv(t, { style: 'animation: anim 100s' });
var animation = div.getAnimations()[0];
return animation.ready.then(function() {
animation.finish();
assert_animation_is_not_running_on_compositor(animation,
'Animation reports that it is NOT running on the compositor'
+ ' immediately after animation.finish() is called');
// Check that we don't set the flag back again on the next tick.
return waitForFrame();
}).then(function() {
assert_animation_is_not_running_on_compositor(animation,
'Animation reports that it is NOT running on the compositor'
+ ' on the next tick after animation.finish() is called');
});
}, 'isRunningOnCompositor is false when the animation.finish() is called');
promise_test(function(t) {
var div = addDiv(t, { style: 'animation: anim 100s' });
var animation = div.getAnimations()[0];
return animation.ready.then(function() {
animation.currentTime = 100 * MS_PER_SEC;
assert_animation_is_not_running_on_compositor(animation,
'Animation reports that it is NOT running on the compositor'
+ ' immediately after manually seeking the animation to the end');
// Check that we don't set the flag back again on the next tick.
return waitForFrame();
}).then(function() {
assert_animation_is_not_running_on_compositor(animation,
'Animation reports that it is NOT running on the compositor'
+ ' on the next tick after manually seeking the animation to the end');
});
}, 'isRunningOnCompositor is false when manually seeking the animation to ' +
'the end');
promise_test(function(t) {
var div = addDiv(t, { style: 'animation: anim 100s' });
var animation = div.getAnimations()[0];
return animation.ready.then(function() {
animation.cancel();
assert_animation_is_not_running_on_compositor(animation,
'Animation reports that it is NOT running on the compositor'
+ ' immediately after animation.cancel() is called');
// Check that we don't set the flag back again on the next tick.
return waitForFrame();
}).then(function() {
assert_animation_is_not_running_on_compositor(animation,
'Animation reports that it is NOT running on the compositor'
+ ' on the next tick after animation.cancel() is called');
});
}, 'isRunningOnCompositor is false when animation.cancel() is called');
// This is to test that we don't simply clobber the flag when ticking
// animations and then set it again during painting.
promise_test(function(t) {
var div = addDiv(t, { style: 'animation: anim 100s' });
var animation = div.getAnimations()[0];
return animation.ready.then(function() {
return new Promise(function(resolve) {
window.requestAnimationFrame(function() {
t.step(function() {
assert_animation_is_running_on_compositor(animation,
'Animation reports that it is running on the compositor'
+ ' in requestAnimationFrame callback');
});
resolve();
});
});
});
}, 'isRunningOnCompositor is true in requestAnimationFrame callback');
promise_test(function(t) {
var div = addDiv(t, { style: 'animation: anim 100s' });
var animation = div.getAnimations()[0];
return animation.ready.then(function() {
return new Promise(function(resolve) {
var observer = new MutationObserver(function(records) {
var changedAnimation;
records.forEach(function(record) {
changedAnimation =
record.changedAnimations.find(function(changedAnim) {
return changedAnim == animation;
});
});
t.step(function() {
assert_true(!!changedAnimation, 'The animation should be recorded '
+ 'as one of the changedAnimations');
assert_animation_is_running_on_compositor(animation,
'Animation reports that it is running on the compositor'
+ ' in MutationObserver callback');
});
resolve();
});
observer.observe(div, { animations: true, subtree: false });
t.add_cleanup(function() {
observer.disconnect();
});
div.style.animationDuration = "200s";
});
});
}, 'isRunningOnCompositor is true in MutationObserver callback');
// This is to test that we don't temporarily clear the flag when forcing
// an unthrottled sample.
promise_test(function(t) {
return new Promise(function(resolve) {
// Needs scrollbars to cause overflow.
SpecialPowers.pushPrefEnv({ set: [["ui.showHideScrollbars", 1]] },
resolve);
}).then(function() {
var div = addDiv(t, { style: 'animation: rotate 100s' });
var animation = div.getAnimations()[0];
return animation.ready.then(function() {
return new Promise(function(resolve) {
var timeAtStart = window.performance.now();
function handleFrame() {
t.step(function() {
assert_animation_is_running_on_compositor(animation,
'Animation reports that it is running on the compositor'
+ ' in requestAnimationFrame callback');
});
// we have to wait at least 200ms because this animation is
// unthrottled on every 200ms.
// See http://hg.mozilla.org/mozilla-central/file/cafb1c90f794/layout/style/AnimationCommon.cpp#l863
if (window.performance.now() - timeAtStart > 200) {
resolve();
return;
}
window.requestAnimationFrame(handleFrame);
}
window.requestAnimationFrame(handleFrame);
});
});
});
}, 'isRunningOnCompositor remains true in requestAnimationFrameCallback for ' +
'overflow animation');
promise_test(function(t) {
var div = addDiv(t, { style: 'transition: opacity 100s; opacity: 1' });
getComputedStyle(div).opacity;
div.style.opacity = 0;
var animation = div.getAnimations()[0];
return animation.ready.then(function() {
assert_animation_is_running_on_compositor(animation,
'Transition reports that it is running on the compositor'
+ ' during playback for opacity transition');
});
}, 'isRunningOnCompositor for transitions');
promise_test(function(t) {
var div = addDiv(t, { style: 'animation: rotate-and-opacity 100s; ' +
'backface-visibility: hidden; ' +
'transform: none !important;' });
var animation = div.getAnimations()[0];
return animation.ready.then(function() {
assert_animation_is_running_on_compositor(animation,
'If an animation has a property that can run on the compositor and a '
+ 'property that cannot (due to Gecko limitations) but where the latter'
+ 'property is overridden in the CSS cascade, the animation should '
+ 'still report that it is running on the compositor');
});
}, 'isRunningOnCompositor is true when a property that would otherwise block ' +
'running on the compositor is overridden in the CSS cascade');
promise_test(function(t) {
var animation = addDivAndAnimate(t,
{},
{ opacity: [ 0, 1 ] }, 200 * MS_PER_SEC);
return animation.ready.then(function() {
assert_animation_is_running_on_compositor(animation,
'Animation reports that it is running on the compositor');
animation.currentTime = 150 * MS_PER_SEC;
animation.effect.timing.duration = 100 * MS_PER_SEC;
assert_animation_is_not_running_on_compositor(animation,
'Animation reports that it is NOT running on the compositor'
+ ' when the animation is set a shorter duration than current time');
});
}, 'animation is immediately removed from compositor' +
'when timing.duration is made shorter than the current time');
promise_test(function(t) {
var animation = addDivAndAnimate(t,
{},
{ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
return animation.ready.then(function() {
assert_animation_is_running_on_compositor(animation,
'Animation reports that it is running on the compositor');
animation.currentTime = 500 * MS_PER_SEC;
assert_animation_is_not_running_on_compositor(animation,
'Animation reports that it is NOT running on the compositor'
+ ' when finished');
animation.effect.timing.duration = 1000 * MS_PER_SEC;
return waitForFrame();
}).then(function() {
assert_animation_is_running_on_compositor(animation,
'Animation reports that it is running on the compositor'
+ ' when restarted');
});
}, 'animation is added to compositor' +
' when timing.duration is made longer than the current time');
promise_test(function(t) {
var animation = addDivAndAnimate(t,
{},
{ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
return animation.ready.then(function() {
assert_animation_is_running_on_compositor(animation,
'Animation reports that it is running on the compositor');
animation.effect.timing.endDelay = 100 * MS_PER_SEC;
assert_animation_is_running_on_compositor(animation,
'Animation reports that it is running on the compositor'
+ ' when endDelay is changed');
animation.currentTime = 110 * MS_PER_SEC;
return waitForFrame();
}).then(function() {
assert_animation_is_not_running_on_compositor(animation,
'Animation reports that it is NOT running on the compositor'
+ ' when currentTime is during endDelay');
});
}, 'animation is removed from compositor' +
' when current time is made longer than the duration even during endDelay');
promise_test(function(t) {
var animation = addDivAndAnimate(t,
{},
{ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
return animation.ready.then(function() {
assert_animation_is_running_on_compositor(animation,
'Animation reports that it is running on the compositor');
animation.effect.timing.endDelay = -200 * MS_PER_SEC;
return waitForFrame();
}).then(function() {
assert_animation_is_not_running_on_compositor(animation,
'Animation reports that it is NOT running on the compositor'
+ ' when endTime is negative value');
});
}, 'animation is removed from compositor' +
' when endTime is negative value');
promise_test(function(t) {
var animation = addDivAndAnimate(t,
{},
{ opacity: [ 0, 1 ] }, 200 * MS_PER_SEC);
return animation.ready.then(function() {
assert_animation_is_running_on_compositor(animation,
'Animation reports that it is running on the compositor');
animation.effect.timing.endDelay = -100 * MS_PER_SEC;
return waitForFrame();
}).then(function() {
assert_animation_is_running_on_compositor(animation,
'Animation reports that it is running on the compositor'
+ ' when endTime is positive and endDelay is negative');
animation.currentTime = 110 * MS_PER_SEC;
return waitForFrame();
}).then(function() {
assert_animation_is_not_running_on_compositor(animation,
'Animation reports that it is NOT running on the compositor'
+ ' when currentTime is after endTime');
});
}, 'animation is NOT running on compositor' +
' when endTime is positive and endDelay is negative');
promise_test(function(t) {
var effect = new KeyframeEffect(null,
{ opacity: [ 0, 1 ] },
100 * MS_PER_SEC);
var animation = new Animation(effect, document.timeline);
animation.play();
var div = addDiv(t);
return animation.ready.then(function() {
assert_animation_is_not_running_on_compositor(animation,
'Animation with null target reports that it is not running ' +
'on the compositor');
animation.effect.target = div;
return waitForFrame();
}).then(function() {
assert_animation_is_running_on_compositor(animation,
'Animation reports that it is running on the compositor ' +
'after setting a valid target');
});
}, 'animation is added to the compositor when setting a valid target');
promise_test(function(t) {
var div = addDiv(t);
var animation = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
return animation.ready.then(function() {
assert_animation_is_running_on_compositor(animation,
'Animation reports that it is running on the compositor');
animation.effect.target = null;
assert_animation_is_not_running_on_compositor(animation,
'Animation reports that it is NOT running on the ' +
'compositor after setting null target');
});
}, 'animation is removed from the compositor when setting null target');
promise_test(function(t) {
var div = addDiv(t);
var animation = div.animate({ opacity: [ 0, 1 ] },
{ duration: 100 * MS_PER_SEC,
delay: 100 * MS_PER_SEC,
fill: 'backwards' });
return animation.ready.then(function() {
assert_animation_is_running_on_compositor(animation,
'Animation with fill:backwards in delay phase reports ' +
'that it is running on the compositor');
animation.currentTime = 100 * MS_PER_SEC;
return waitForFrame();
}).then(function() {
assert_animation_is_running_on_compositor(animation,
'Animation with fill:backwards in delay phase reports ' +
'that it is running on the compositor after delay phase');
});
}, 'animation with fill:backwards in delay phase is running on the ' +
' main-thread while it is in delay phase');
promise_test(function(t) {
var div = addDiv(t);
var animation = div.animate([{ opacity: 1, offset: 0 },
{ opacity: 1, offset: 0.99 },
{ opacity: 0, offset: 1 }], 100 * MS_PER_SEC);
var another = addDiv(t);
return animation.ready.then(function() {
assert_animation_is_running_on_compositor(animation,
'Opacity animation on a 100% opacity keyframe reports ' +
'that it is running on the compositor from the begining');
animation.effect.target = another;
return waitForFrame();
}).then(function() {
assert_animation_is_running_on_compositor(animation,
'Opacity animation on a 100% opacity keyframe keeps ' +
'running on the compositor after changing the target ' +
'element');
});
}, '100% opacity animations with keeps running on the ' +
'compositor after changing the target element');
promise_test(function(t) {
var div = addDiv(t);
var animation = div.animate({ color: ['red', 'black'] }, 100 * MS_PER_SEC);
return animation.ready.then(function() {
assert_animation_is_not_running_on_compositor(animation,
'Color animation reports that it is not running on the ' +
'compositor');
animation.effect.setKeyframes([{ opacity: 1, offset: 0 },
{ opacity: 1, offset: 0.99 },
{ opacity: 0, offset: 1 }]);
return waitForFrame();
}).then(function() {
assert_animation_is_running_on_compositor(animation,
'100% opacity animation set by using setKeyframes reports ' +
'that it is running on the compositor');
});
}, '100% opacity animation set up by converting an existing animation with ' +
'cannot be run on the compositor, is running on the compositor');
promise_test(function(t) {
var div = addDiv(t);
var animation = div.animate({ color: ['red', 'black'] }, 100 * MS_PER_SEC);
var effect = new KeyframeEffect(div,
[{ opacity: 1, offset: 0 },
{ opacity: 1, offset: 0.99 },
{ opacity: 0, offset: 1 }],
100 * MS_PER_SEC);
return animation.ready.then(function() {
assert_animation_is_not_running_on_compositor(animation,
'Color animation reports that it is not running on the ' +
'compositor');
animation.effect = effect;
return waitForFrame();
}).then(function() {
assert_animation_is_running_on_compositor(animation,
'100% opacity animation set up by changing effects reports ' +
'that it is running on the compositor');
});
}, '100% opacity animation set up by changing the effects on an existing ' +
'animation which cannot be run on the compositor, is running on the ' +
'compositor');
promise_test(function(t) {
var div = addDiv(t, { style: "opacity: 1 ! important" });
var animation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
return animation.ready.then(function() {
assert_animation_is_not_running_on_compositor(animation,
'Opacity animation on an element which has 100% opacity style with ' +
'!important flag reports that it is not running on the compositor');
// Clear important flag from the opacity style on the target element.
div.style.setProperty("opacity", "1", "");
return waitForFrame();
}).then(function() {
assert_animation_is_running_on_compositor(animation,
'Opacity animation reports that it is running on the compositor after '
+ 'clearing the !important flag');
});
}, 'Clearing *important* opacity style on the target element sends the ' +
'animation to the compositor');
promise_test(function(t) {
var div = addDiv(t);
var lowerAnimation = div.animate({ opacity: [1, 0] }, 100 * MS_PER_SEC);
var higherAnimation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
return Promise.all([lowerAnimation.ready, higherAnimation.ready]).then(function() {
assert_animation_is_running_on_compositor(higherAnimation,
'A higher-priority opacity animation on an element ' +
'reports that it is running on the compositor');
assert_animation_is_running_on_compositor(lowerAnimation,
'A lower-priority opacity animation on the same ' +
'element also reports that it is running on the compositor');
});
}, 'Opacity animations on the same element run on the compositor');
promise_test(function(t) {
var div = addDiv(t, { style: 'transition: opacity 100s; opacity: 1' });
getComputedStyle(div).opacity;
div.style.opacity = 0;
getComputedStyle(div).opacity;
var transition = div.getAnimations()[0];
var animation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
return Promise.all([transition.ready, animation.ready]).then(function() {
assert_animation_is_running_on_compositor(animation,
'An opacity animation on an element reports that' +
'that it is running on the compositor');
assert_animation_is_running_on_compositor(transition,
'An opacity transition on the same element reports that ' +
'it is running on the compositor');
});
}, 'Both of transition and script animation on the same element run on the ' +
'compositor');
promise_test(function(t) {
var div = addDiv(t);
var importantOpacityElement = addDiv(t, { style: "opacity: 1 ! important" });
var animation = div.animate({ opacity: [1, 0] }, 100 * MS_PER_SEC);
return animation.ready.then(function() {
assert_animation_is_running_on_compositor(animation,
'Opacity animation on an element reports ' +
'that it is running on the compositor');
animation.effect.target = null;
return waitForFrame();
}).then(function() {
assert_animation_is_not_running_on_compositor(animation,
'Animation is no longer running on the compositor after ' +
'removing from the element');
animation.effect.target = importantOpacityElement;
return waitForFrame();
}).then(function() {
assert_animation_is_not_running_on_compositor(animation,
'Animation is NOT running on the compositor even after ' +
'being applied to a different element which has an ' +
'!important opacity declaration');
});
}, 'Animation continues not running on the compositor after being ' +
'applied to an element which has an important declaration and ' +
'having previously been temporarily associated with no target element');
promise_test(function(t) {
var div = addDiv(t);
var another = addDiv(t);
var lowerAnimation = div.animate({ opacity: [1, 0] }, 100 * MS_PER_SEC);
var higherAnimation = another.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
return Promise.all([lowerAnimation.ready, higherAnimation.ready]).then(function() {
assert_animation_is_running_on_compositor(lowerAnimation,
'An opacity animation on an element reports that ' +
'it is running on the compositor');
assert_animation_is_running_on_compositor(higherAnimation,
'Opacity animation on a different element reports ' +
'that it is running on the compositor');
lowerAnimation.effect.target = null;
return waitForFrame();
}).then(function() {
assert_animation_is_not_running_on_compositor(lowerAnimation,
'Animation is no longer running on the compositor after ' +
'being removed from the element');
lowerAnimation.effect.target = another;
return waitForFrame();
}).then(function() {
assert_animation_is_running_on_compositor(lowerAnimation,
'A lower-priority animation begins running ' +
'on the compositor after being applied to an element ' +
'which has a higher-priority animation');
assert_animation_is_running_on_compositor(higherAnimation,
'A higher-priority animation continues to run on the ' +
'compositor even after a lower-priority animation is ' +
'applied to the same element');
});
}, 'Animation begins running on the compositor after being applied ' +
'to an element which has a higher-priority animation and after ' +
'being temporarily associated with no target element');
promise_test(function(t) {
var div = addDiv(t);
var another = addDiv(t);
var lowerAnimation = div.animate({ opacity: [1, 0] }, 100 * MS_PER_SEC);
var higherAnimation = another.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
return Promise.all([lowerAnimation.ready, higherAnimation.ready]).then(function() {
assert_animation_is_running_on_compositor(lowerAnimation,
'An opacity animation on an element reports that ' +
'it is running on the compositor');
assert_animation_is_running_on_compositor(higherAnimation,
'Opacity animation on a different element reports ' +
'that it is running on the compositor');
higherAnimation.effect.target = null;
return waitForFrame();
}).then(function() {
assert_animation_is_not_running_on_compositor(higherAnimation,
'Animation is no longer running on the compositor after ' +
'being removed from the element');
higherAnimation.effect.target = div;
return waitForFrame();
}).then(function() {
assert_animation_is_running_on_compositor(lowerAnimation,
'Animation continues running on the compositor after ' +
'a higher-priority animation applied to the same element');
assert_animation_is_running_on_compositor(higherAnimation,
'A higher-priority animation begins to running on the ' +
'compositor after being applied to an element which has ' +
'a lower-priority-animation');
});
}, 'Animation begins running on the compositor after being applied ' +
'to an element which has a lower-priority animation once after ' +
'disassociating with an element');
var delayPhaseTests = [
{
desc: 'script animation of opacity',
setupAnimation: function(t) {
return addDiv(t).animate(
{ opacity: [0, 1] },
{ delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC });
},
},
{
desc: 'script animation of transform',
setupAnimation: function(t) {
return addDiv(t).animate(
{ transform: ['translateX(0px)', 'translateX(100px)'] },
{ delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC });
},
},
{
desc: 'CSS animation of opacity',
setupAnimation: function(t) {
return addDiv(t, { style: 'animation: opacity 100s 100s' })
.getAnimations()[0];
},
},
{
desc: 'CSS animation of transform',
setupAnimation: function(t) {
return addDiv(t, { style: 'animation: anim 100s 100s' })
.getAnimations()[0];
},
},
{
desc: 'CSS transition of opacity',
setupAnimation: function(t) {
var div = addDiv(t, { style: 'transition: opacity 100s 100s' });
getComputedStyle(div).opacity;
div.style.opacity = 0;
return div.getAnimations()[0];
},
},
{
desc: 'CSS transition of transform',
setupAnimation: function(t) {
var div = addDiv(t, { style: 'transition: transform 100s 100s' });
getComputedStyle(div).transform;
div.style.transform = 'translateX(100px)';
return div.getAnimations()[0];
},
},
];
delayPhaseTests.forEach(function(test) {
promise_test(function(t) {
var animation = test.setupAnimation(t);
return animation.ready.then(function() {
assert_animation_is_running_on_compositor(animation,
test.desc + ' reports that it is running on the '
+ 'compositor even though it is in the delay phase');
});
}, 'isRunningOnCompositor for ' + test.desc + ' is true even though ' +
'it is in the delay phase');
});
// The purpose of thie test cases is to check that
// NS_FRAME_MAY_BE_TRANSFORMED flag on the associated nsIFrame persists
// after transform style on the frame is removed.
var delayPhaseWithTransformStyleTests = [
{
desc: 'script animation of transform with transform style',
setupAnimation: function(t) {
return addDiv(t, { style: 'transform: translateX(10px)' }).animate(
{ transform: ['translateX(0px)', 'translateX(100px)'] },
{ delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC });
},
},
{
desc: 'CSS animation of transform with transform style',
setupAnimation: function(t) {
return addDiv(t, { style: 'animation: anim 100s 100s;' +
'transform: translateX(10px)' })
.getAnimations()[0];
},
},
{
desc: 'CSS transition of transform with transform style',
setupAnimation: function(t) {
var div = addDiv(t, { style: 'transition: transform 100s 100s;' +
'transform: translateX(10px)'});
getComputedStyle(div).transform;
div.style.transform = 'translateX(100px)';
return div.getAnimations()[0];
},
},
];
delayPhaseWithTransformStyleTests.forEach(function(test) {
promise_test(function(t) {
var animation = test.setupAnimation(t);
return animation.ready.then(function() {
assert_animation_is_running_on_compositor(animation,
test.desc + ' reports that it is running on the '
+ 'compositor even though it is in the delay phase');
}).then(function() {
// Remove the initial transform style during delay phase.
animation.effect.target.style.transform = 'none';
return animation.ready;
}).then(function() {
assert_animation_is_running_on_compositor(animation,
test.desc + ' reports that it keeps running on the '
+ 'compositor after removing the initial transform style');
});
}, 'isRunningOnCompositor for ' + test.desc + ' is true after removing ' +
'the initial transform style during the delay phase');
});
var startsWithNoneTests = [
{
desc: 'script animation of transform starts with transform:none segment',
setupAnimation: function(t) {
return addDiv(t).animate(
{ transform: ['none', 'none', 'translateX(100px)'] }, 100 * MS_PER_SEC);
},
},
{
desc: 'CSS animation of transform starts with transform:none segment',
setupAnimation: function(t) {
return addDiv(t,
{ style: 'animation: transform-starts-with-none 100s 100s' })
.getAnimations()[0];
},
},
];
startsWithNoneTests.forEach(function(test) {
promise_test(function(t) {
var animation = test.setupAnimation(t);
return animation.ready.then(function() {
assert_animation_is_running_on_compositor(animation,
test.desc + ' reports that it is running on the '
+ 'compositor even though it is in transform:none segment');
});
}, 'isRunningOnCompositor for ' + test.desc + ' is true even though ' +
'it is in transform:none segment');
});
promise_test(function(t) {
var div = addDiv(t, { style: 'opacity: 1 ! important' });
var animation = div.animate(
{ opacity: [0, 1] },
{ delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC });
return animation.ready.then(function() {
assert_animation_is_not_running_on_compositor(animation,
'Opacity animation on an element which has opacity:1 important style'
+ 'reports that it is not running on the compositor');
// Clear the opacity style on the target element.
div.style.setProperty("opacity", "1", "");
return waitForFrame();
}).then(function() {
assert_animation_is_running_on_compositor(animation,
'Opacity animations reports that it is running on the compositor after '
+ 'clearing the opacity style on the element');
});
}, 'Clearing *important* opacity style on the target element sends the ' +
'animation to the compositor even if the animation is in the delay phase');
promise_test(function(t) {
var opaqueDiv = addDiv(t, { style: 'opacity: 1 ! important' });
var anotherDiv = addDiv(t);
var animation = opaqueDiv.animate(
{ opacity: [0, 1] },
{ delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC });
return animation.ready.then(function() {
assert_animation_is_not_running_on_compositor(animation,
'Opacity animation on an element which has opacity:1 important style'
+ 'reports that it is not running on the compositor');
// Changing target element to another element which has no opacity style.
animation.effect.target = anotherDiv;
return waitForFrame();
}).then(function() {
assert_animation_is_running_on_compositor(animation,
'Opacity animations reports that it is running on the compositor after '
+ 'changing the target element to another elemenent having no '
+ 'opacity style');
});
}, 'Changing target element of opacity animation sends the animation to the ' +
'the compositor even if the animation is in the delay phase');
promise_test(function(t) {
var animation =
addDivAndAnimate(t,
{},
{ width: ['100px', '200px'] },
{ duration: 100 * MS_PER_SEC, delay: 100 * MS_PER_SEC });
return animation.ready.then(function() {
assert_animation_is_not_running_on_compositor(animation,
'Width animation reports that it is not running on the compositor '
+ 'in the delay phase');
// Changing to property runnable on the compositor.
animation.effect.setKeyframes({ opacity: [0, 1] });
return waitForFrame();
}).then(function() {
assert_animation_is_running_on_compositor(animation,
'Opacity animation reports that it is running on the compositor '
+ 'after changing the property from width property in the delay phase');
});
}, 'Dynamic change to a property runnable on the compositor ' +
'in the delay phase');
promise_test(function(t) {
var div = addDiv(t, { style: 'transition: opacity 100s; ' +
'opacity: 0 !important' });
getComputedStyle(div).opacity;
div.style.setProperty('opacity', '1', 'important');
getComputedStyle(div).opacity;
var animation = div.getAnimations()[0];
return animation.ready.then(function() {
assert_animation_is_running_on_compositor(animation,
'Transition reports that it is running on the compositor even if the ' +
'property is overridden by an !important rule');
});
}, 'Transitions override important rules');
promise_test(function(t) {
var div = addDiv(t, { style: 'transition: opacity 100s; ' +
'opacity: 0 !important' });
getComputedStyle(div).opacity;
div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
div.style.setProperty('opacity', '1', 'important');
getComputedStyle(div).opacity;
var [transition, animation] = div.getAnimations();
return Promise.all([transition.ready, animation.ready]).then(function() {
assert_animation_is_not_running_on_compositor(transition,
'Transition suppressed by an animation which is overridden by an ' +
'!important rule reports that it is NOT running on the compositor');
assert_animation_is_not_running_on_compositor(animation,
'Animation overridden by an !important rule reports that it is ' +
'NOT running on the compositor');
});
}, 'Neither transition nor animation does run on the compositor if the ' +
'property is overridden by an !important rule');
</script>
</body>