Bug 1301305 - Add tests for transform animations synchronized with geometric animations: r=hiro

MozReview-Commit-ID: Ay7xqfyW0N2

--HG--
extra : rebase_source : ec757beec11a107272e69ab40323fac4e3980911
extra : histedit_source : 05086a0b20003d28a689c5f7be040e3b9cd569f8
This commit is contained in:
Brian Birtles 2016-12-05 21:56:05 -10:00
parent 3dba0a16a9
commit 28b0b81256
2 changed files with 273 additions and 0 deletions

View File

@ -885,6 +885,269 @@ function testSmallElements() {
});
}
function testSynchronizedAnimations() {
promise_test(function(t) {
const elemA = addDiv(t, { class: 'compositable' });
const elemB = addDiv(t, { class: 'compositable' });
const animA = elemA.animate({ transform: [ 'translate(0px)',
'translate(100px)' ] },
100 * MS_PER_SEC);
const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
100 * MS_PER_SEC);
return Promise.all([animA.ready, animB.ready])
.then(() => {
assert_animation_property_state_equals(
animA.effect.getProperties(),
[ { property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
} ]);
});
}, 'Animations created within the same tick are synchronized'
+ ' (compositor animation created first)');
promise_test(function(t) {
const elemA = addDiv(t, { class: 'compositable' });
const elemB = addDiv(t, { class: 'compositable' });
const animA = elemA.animate({ marginLeft: [ '0px', '100px' ] },
100 * MS_PER_SEC);
const animB = elemB.animate({ transform: [ 'translate(0px)',
'translate(100px)' ] },
100 * MS_PER_SEC);
return Promise.all([animA.ready, animB.ready])
.then(() => {
assert_animation_property_state_equals(
animB.effect.getProperties(),
[ { property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
} ]);
});
}, 'Animations created within the same tick are synchronized'
+ ' (compositor animation created second)');
promise_test(function(t) {
const attrs = { class: 'compositable',
style: 'transition: all 100s' };
const elemA = addDiv(t, attrs);
const elemB = addDiv(t, attrs);
elemA.style.transform = 'translate(0px)';
elemB.style.marginLeft = '0px';
getComputedStyle(elemA).transform;
getComputedStyle(elemB).marginLeft;
// Generally the sequence of steps is as follows:
//
// Tick -> requestAnimationFrame -> Style -> Paint -> Events (-> Tick...)
//
// In this test we want to set up two transitions during the "Events"
// stage but only flush style for one such that the second one is actually
// generated during the "Style" stage of the *next* tick.
//
// Web content often generates transitions in this way (that is, it doesn't
// pay regard to when style is flushed and nor should it). However, we
// still want transitions generated in this way to be synchronized.
let timeForFirstFrame;
return waitForIdleCallback()
.then(() => {
timeForFirstFrame = document.timeline.currentTime;
elemA.style.transform = 'translate(100px)';
// Flush style to trigger first transition
getComputedStyle(elemA).transform;
elemB.style.marginLeft = '100px';
// DON'T flush style here (this includes calling getAnimations!)
return waitForFrame();
}).then(() => {
assert_not_equals(timeForFirstFrame, document.timeline.currentTime,
'Should be on the other side of a tick');
// Wait another tick so we can let the transition be started
// by regular style resolution.
return waitForFrame();
}).then(() => {
const transitionA = elemA.getAnimations()[0];
assert_animation_property_state_equals(
transitionA.effect.getProperties(),
[ { property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
} ]);
});
}, 'Transitions created before and after a tick are synchronized');
promise_test(function(t) {
const elemA = addDiv(t, { class: 'compositable' });
const elemB = addDiv(t, { class: 'compositable' });
const animA = elemA.animate({ transform: [ 'translate(0px)',
'translate(100px)' ],
opacity: [ 0, 1 ] },
100 * MS_PER_SEC);
const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
100 * MS_PER_SEC);
return Promise.all([animA.ready, animB.ready])
.then(() => {
assert_animation_property_state_equals(
animA.effect.getProperties(),
[ { property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
},
{ property: 'opacity',
runningOnCompositor: true
} ]);
});
}, 'Opacity animations on the same element continue running on the'
+ ' compositor when transform animations are synchronized with geometric'
+ ' animations');
promise_test(function(t) {
const elemA = addDiv(t, { class: 'compositable' });
const elemB = addDiv(t, { class: 'compositable' });
const animA = elemA.animate({ marginLeft: [ '0px', '100px' ] },
100 * MS_PER_SEC);
let animB;
return waitForFrame()
.then(() => {
animB = elemB.animate({ transform: [ 'translate(0px)',
'translate(100px)' ] },
100 * MS_PER_SEC);
return animB.ready;
}).then(() => {
assert_animation_property_state_equals(
animB.effect.getProperties(),
[ { property: 'transform',
runningOnCompositor: true } ]);
});
}, 'Transform animations are NOT synchronized with geometric animations'
+ ' started in the previous frame');
promise_test(function(t) {
const elemA = addDiv(t, { class: 'compositable' });
const elemB = addDiv(t, { class: 'compositable' });
const animA = elemA.animate({ transform: [ 'translate(0px)',
'translate(100px)' ] },
100 * MS_PER_SEC);
let animB;
return waitForFrame()
.then(() => {
animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
100 * MS_PER_SEC);
return animB.ready;
}).then(() => {
assert_animation_property_state_equals(
animA.effect.getProperties(),
[ { property: 'transform',
runningOnCompositor: true } ]);
});
}, 'Transform animations are NOT synchronized with geometric animations'
+ ' started in the next frame');
promise_test(function(t) {
const elemA = addDiv(t, { class: 'compositable' });
const elemB = addDiv(t, { class: 'compositable' });
const animA = elemA.animate({ transform: [ 'translate(0px)',
'translate(100px)' ] },
100 * MS_PER_SEC);
const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
100 * MS_PER_SEC);
animB.pause();
return Promise.all([animA.ready, animB.ready])
.then(() => {
assert_animation_property_state_equals(
animA.effect.getProperties(),
[ { property: 'transform', runningOnCompositor: true } ]);
});
}, 'Paused animations are not synchronized');
promise_test(function(t) {
const elemA = addDiv(t, { class: 'compositable' });
const elemB = addDiv(t, { class: 'compositable' });
const animA = elemA.animate({ transform: [ 'translate(0px)',
'translate(100px)' ] },
100 * MS_PER_SEC);
const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
100 * MS_PER_SEC);
// Seek one of the animations so that their start times will differ
animA.currentTime = 5000;
return Promise.all([animA.ready, animB.ready])
.then(() => {
assert_not_equals(animA.startTime, animB.startTime,
'Animations should have different start times');
assert_animation_property_state_equals(
animA.effect.getProperties(),
[ { property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
} ]);
});
}, 'Animations are synchronized based on when they are started'
+ ' and NOT their start time');
promise_test(function(t) {
const elemA = addDiv(t, { class: 'compositable' });
const elemB = addDiv(t, { class: 'compositable' });
const animA = elemA.animate({ transform: [ 'translate(0px)',
'translate(100px)' ] },
100 * MS_PER_SEC);
const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
100 * MS_PER_SEC);
return Promise.all([animA.ready, animB.ready])
.then(() => {
assert_animation_property_state_equals(
animA.effect.getProperties(),
[ { property: 'transform',
runningOnCompositor: false } ]);
// Restart animation
animA.pause();
animA.play();
return animA.ready;
}).then(() => {
assert_animation_property_state_equals(
animA.effect.getProperties(),
[ { property: 'transform',
runningOnCompositor: true } ]);
});
}, 'An initially synchronized animation may be unsynchronized if restarted');
promise_test(function(t) {
const elemA = addDiv(t, { class: 'compositable' });
const elemB = addDiv(t, { class: 'compositable' });
const animA = elemA.animate({ transform: [ 'translate(0px)',
'translate(100px)' ] },
100 * MS_PER_SEC);
const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
100 * MS_PER_SEC);
// Clear target effect
animB.effect.target = null;
return Promise.all([animA.ready, animB.ready])
.then(() => {
assert_animation_property_state_equals(
animA.effect.getProperties(),
[ { property: 'transform',
runningOnCompositor: true } ]);
});
}, 'A geometric animation with no target element is not synchronized');
}
function start() {
var bundleService = SpecialPowers.Cc['@mozilla.org/intl/stringbundle;1']
.getService(SpecialPowers.Ci.nsIStringBundleService);
@ -900,6 +1163,7 @@ function start() {
testMultipleAnimationsWithGeometricKeyframes();
testMultipleAnimationsWithGeometricAnimations();
testSmallElements();
testSynchronizedAnimations();
promise_test(function(t) {
var animation = addDivAndAnimate(t,

View File

@ -162,6 +162,15 @@ function waitForFrame() {
});
}
/**
* Promise wrapper for requestIdleCallback.
*/
function waitForIdleCallback() {
return new Promise(function(resolve, reject) {
window.requestIdleCallback(resolve);
});
}
/**
* Returns a Promise that is resolved after the given number of consecutive
* animation frames have occured (using requestAnimationFrame callbacks).