blob: 4d4c0b554f3d6b983036715fc1fed6ead577dd2f [file] [log] [blame]
/* This is the helper script for animation tests:
Test page requirements:
- The body must contain an empty div with id "result"
- Call this function directly from the <script> inside the test page
runAnimationTest and runTransitionTest parameters:
expected [required]: an array of arrays defining a set of CSS properties that must have given values at specific times (see below)
callbacks [optional]: a function to be executed immediately after animation starts;
or, an object in the form {time: function} containing functions to be
called at the specified times (in seconds) during animation.
trigger [optional]: a function to be executed just before the test starts (none by default)
doPixelTest [optional]: whether to dump pixels during the test (false by default)
disablePauseAnimationAPI [optional]: whether to disable the pause API and run a RAF-based test (false by default)
Each sub-array must contain these items in this order:
- the time in seconds at which to snapshot the CSS property
- the id of the element on which to get the CSS property value [1]
- the name of the CSS property to get [2]
- the expected value for the CSS property [3]
- the tolerance to use when comparing the effective CSS property value with its expected value
[1] If a single string is passed, it is the id of the element to test. If an array with 2 elements is passed they
are the ids of 2 elements, whose values are compared for equality. In this case the expected value is ignored
but the tolerance is used in the comparison.
If a string with a '.' is passed, this is an element in an iframe. The string before the dot is the iframe id
and the string after the dot is the element name in that iframe.
[2] Appending ".N" to the CSS property name (e.g. "transform.N") makes
us only check the Nth numeric substring of the computed style.
[3] The expected value has several supported formats. If an array is given,
we extract numeric substrings from the computed style and compare the
number of these as well as the values. If a number is given, we treat it as
an array of length one. If a string is given, we compare numeric substrings
with the computed style with the given tolerance and also the remaining
non-numeric substrings.
*/
// Set to true to log debug information in failing tests. Note that these logs
// contain timestamps, so are non-deterministic and will introduce flakiness if
// any expected results include failures.
var ENABLE_ERROR_LOGGING = false;
function isCloseEnough(actual, expected, tolerance)
{
return Math.abs(actual - expected) <= tolerance;
}
function roundNumber(num, decimalPlaces)
{
return Math.round(num * Math.pow(10, decimalPlaces)) / Math.pow(10, decimalPlaces);
}
function checkExpectedValue(expected, index)
{
log('Checking expectation: ' + JSON.stringify(expected[index]));
var time = expected[index][0];
var elementId = expected[index][1];
var specifiedProperty = expected[index][2];
var expectedValue = expected[index][3];
var tolerance = expected[index][4];
var postCompletionCallback = expected[index][5];
var expectedIndex = specifiedProperty.split(".")[1];
var property = specifiedProperty.split(".")[0];
// Check for a pair of element Ids
if (typeof elementId != "string") {
if (elementId.length != 2)
return;
var elementId2 = elementId[1];
elementId = elementId[0];
var computedValue = getPropertyValue(property, elementId);
var computedValue2 = getPropertyValue(property, elementId2);
if (comparePropertyValue(computedValue, computedValue2, tolerance, expectedIndex))
result += "PASS - \"" + specifiedProperty + "\" property for \"" + elementId + "\" and \"" + elementId2 +
"\" elements at " + time + "s are close enough to each other" + "<br>";
else
result += "FAIL - \"" + specifiedProperty + "\" property for \"" + elementId + "\" and \"" + elementId2 +
"\" elements at " + time + "s saw: \"" + computedValue + "\" and \"" + computedValue2 +
"\" which are not close enough to each other" + "<br>";
} else {
var elementName = elementId;
var array = elementId.split('.');
var iframeId;
if (array.length == 2) {
iframeId = array[0];
elementId = array[1];
}
var computedValue = getPropertyValue(property, elementId, iframeId);
if (comparePropertyValue(computedValue, expectedValue, tolerance, expectedIndex))
result += "PASS - \"" + specifiedProperty + "\" property for \"" + elementName + "\" element at " + time +
"s saw something close to: " + expectedValue + "<br>";
else
result += "FAIL - \"" + specifiedProperty + "\" property for \"" + elementName + "\" element at " + time +
"s expected: " + expectedValue + " but saw: " + computedValue + "<br>";
}
if (postCompletionCallback)
result += postCompletionCallback();
}
function getPropertyValue(property, elementId, iframeId)
{
var context = iframeId ? document.getElementById(iframeId).contentDocument : document;
var element = context.getElementById(elementId);
return window.getComputedStyle(element)[property];
}
// splitValue("calc(12.5px + 10%)") -> [["calc(", "px + ", "%)"], [12.5, 10]]
function splitValue(value)
{
var substrings = value.split(/(-?\d+(?:\.\d+)?(?:e-?\d+)?)/g);
var strings = [];
var numbers = [];
for (var i = 0; i < substrings.length; i++) {
if (i % 2 == 0)
strings.push(substrings[i]);
else
numbers.push(parseFloat(substrings[i]));
}
return [strings, numbers];
}
function comparePropertyValue(computedValue, expectedValue, tolerance, expectedIndex)
{
computedValue = splitValue(computedValue);
var computedStrings = computedValue[0];
var computedNumbers = computedValue[1];
var expectedStrings, expectedNumbers;
if (expectedIndex !== undefined)
return isCloseEnough(computedNumbers[expectedIndex], expectedValue, tolerance);
if (typeof expectedValue === "string") {
expectedValue = splitValue(expectedValue);
expectedStrings = expectedValue[0];
if (computedStrings.length !== expectedStrings.length)
return false;
for (var i = 0; i < expectedStrings.length; i++)
if (expectedStrings[i] !== computedStrings[i])
return false;
expectedNumbers = expectedValue[1];
} else if (typeof expectedValue === "number") {
expectedNumbers = [expectedValue];
} else {
expectedNumbers = expectedValue;
}
if (computedNumbers.length !== expectedNumbers.length)
return false;
for (var i = 0; i < expectedNumbers.length; i++)
if (!isCloseEnough(computedNumbers[i], expectedNumbers[i], tolerance))
return false;
return true;
}
function waitForCompositor() {
return document.body.animate({opacity: [1, 1]}, 1).finished;
}
function endTest()
{
log('Ending test');
var resultElement = useResultElement ? document.getElementById('result') : document.documentElement;
if (ENABLE_ERROR_LOGGING && result.indexOf('FAIL') >= 0)
result += '<br>Log:<br>' + logMessages.join('<br>');
resultElement.innerHTML = result;
if (window.testRunner) {
waitForCompositor().then(() => {
testRunner.notifyDone();
});
}
}
function runChecksWithRAF(checks)
{
var finished = true;
var time = performance.now() - animStartTime;
log('RAF callback, animation time: ' + time);
for (var k in checks) {
var checkTime = Number(k);
if (checkTime > time) {
finished = false;
break;
}
log('Running checks for time: ' + checkTime + ', delay: ' + (time - checkTime));
checks[k].forEach(function(check) { check(); });
delete checks[k];
}
if (finished)
endTest();
else
requestAnimationFrame(runChecksWithRAF.bind(null, checks));
}
function runChecksWithPauseAPI(checks) {
for (var k in checks) {
var timeMs = Number(k);
log('Pausing at time: ' + timeMs + ', current animations: ' + document.getAnimations().length);
internals.pauseAnimations(timeMs / 1000);
checks[k].forEach(function(check) { check(); });
}
endTest();
}
function startTest(checks)
{
if (hasPauseAnimationAPI)
runChecksWithPauseAPI(checks);
else {
result += 'Warning this test is running in real-time and may be flaky.<br>';
runChecksWithRAF(checks);
}
}
var logMessages = [];
var useResultElement = false;
var result = "";
var hasPauseAnimationAPI;
var animStartTime;
var isTransitionsTest = false;
function log(message)
{
logMessages.push(performance.now() + ' - ' + message);
}
function waitForAnimationsToStart(callback)
{
if (document.getAnimations().length > 0) {
callback();
} else {
requestAnimationFrame(waitForAnimationsToStart.bind(this, callback));
}
}
function convertExpectationsToChecks(expected, callbacks) {
var checks = {};
if (typeof callbacks == 'function') {
checks[0] = [callbacks];
} else for (var time in callbacks) {
timeMs = Math.round(time * 1000);
checks[timeMs] = [callbacks[time]];
}
for (var i = 0; i < expected.length; i++) {
var expectation = expected[i];
var timeMs = Math.round(expectation[0] * 1000);
if (!checks[timeMs])
checks[timeMs] = [];
checks[timeMs].push(checkExpectedValue.bind(null, expected, i));
}
return checks;
}
// FIXME: disablePauseAnimationAPI and doPixelTest
function runAnimationTest(expected, callbacks, trigger, disablePauseAnimationAPI, doPixelTest, startTestImmediately)
{
log('runAnimationTest');
if (!expected)
throw "Expected results are missing!";
if (!document.getAnimations)
throw "Animation tests require document.getAnimations";
hasPauseAnimationAPI = 'internals' in window;
if (disablePauseAnimationAPI)
hasPauseAnimationAPI = false;
var checks = convertExpectationsToChecks(expected, callbacks);
var doPixelTest = Boolean(doPixelTest);
useResultElement = doPixelTest;
if (window.testRunner) {
if (!doPixelTest)
testRunner.dumpAsText();
testRunner.waitUntilDone();
}
var started = false;
function begin() {
if (!started) {
log('First ' + event + ' event fired');
started = true;
if (trigger) {
trigger();
document.documentElement.offsetTop
}
waitForAnimationsToStart(function() {
log('Finished waiting for animations to start');
animStartTime = performance.now();
startTest(checks);
});
}
}
var startTestImmediately = Boolean(startTestImmediately);
if (startTestImmediately) {
begin();
} else {
var target = isTransitionsTest ? window : document;
var event = isTransitionsTest ? 'load' : 'webkitAnimationStart';
target.addEventListener(event, begin, false);
}
}
function runTransitionTest(expected, trigger, callbacks, doPixelTest, disablePauseAnimationAPI) {
isTransitionsTest = true;
runAnimationTest(expected, callbacks, trigger, disablePauseAnimationAPI, doPixelTest);
}