1 /* This is the helper function to run animation tests:
3 Test page requirements:
4 - The body must contain an empty div with id "result"
5 - Call this function directly from the <script> inside the test page
8 expected [required]: an array of arrays defining a set of CSS properties that must have given values at specific times (see below)
9 callbacks [optional]: a function to be executed immediately after animation starts;
10 or, an object in the form {time: function} containing functions to be
11 called at the specified times (in seconds) during animation.
12 trigger [optional]: a function to trigger transitions at the start of the test
14 Each sub-array must contain these items in this order:
15 - the time in seconds at which to snapshot the CSS property
16 - the id of the element on which to get the CSS property value [1]
17 - the name of the CSS property to get [2]
18 - the expected value for the CSS property
19 - the tolerance to use when comparing the effective CSS property value with its expected value
21 [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
22 are the ids of 2 elements, whose values are compared for equality. In this case the expected value is ignored
23 but the tolerance is used in the comparison. If the second element is prefixed with "static:", no animation on that
24 element is required, allowing comparison with an unanimated "expected value" element.
26 If a string with a '.' is passed, this is an element in an iframe. The string before the dot is the iframe id
27 and the string after the dot is the element name in that iframe.
29 [2] If the CSS property name is "webkitTransform", expected value must be an array of 1 or more numbers corresponding to the matrix elements,
30 or a string which will be compared directly (useful if the expected value is "none")
31 If the CSS property name is "webkitTransform.N", expected value must be a number corresponding to the Nth element of the matrix
35 // Set to true to log debug information in failing tests. Note that these logs
36 // contain timestamps, so are non-deterministic and will introduce flakiness if
37 // any expected results include failures.
38 var ENABLE_ERROR_LOGGING = false;
40 function isCloseEnough(actual, desired, tolerance)
42 if (typeof desired === "string")
43 return actual === desired;
44 var diff = Math.abs(actual - desired);
45 return diff <= tolerance;
48 function roundNumber(num, decimalPlaces)
50 return Math.round(num * Math.pow(10, decimalPlaces)) / Math.pow(10, decimalPlaces);
53 function matrixStringToArray(s)
56 return [ 1, 0, 0, 1, 0, 0 ];
59 return m[0].split(",");
62 function parseCrossFade(s)
64 var matches = s.match("-webkit-cross-fade\\((.*)\\s*,\\s*(.*)\\s*,\\s*(.*)\\)");
69 return {"from": matches[1], "to": matches[2], "percent": parseFloat(matches[3])}
72 function parseBasicShape(s)
74 var functionParse = s.match(/(\w+)\((.+)\)/);
78 var name = functionParse[1];
79 var params = functionParse[2];
80 params = params.split(/\s*[,\s]\s*/);
82 // Parse numbers and normalize percentages
83 for (var i = 0; i < params.length; ++i) {
84 var param = params[i];
85 if (!/$\d/.test(param))
87 params[i] = parseFloat(params[i]);
88 if (param.indexOf('%') != -1)
89 params[i] = params[i] / 100;
92 return {"shape": name, "params": params};
95 function basicShapeParametersMatch(paramList1, paramList2, tolerance)
97 if (paramList1.shape != paramList2.shape
98 || paramList1.params.length != paramList2.params.length)
100 for (var i = 0; i < paramList1.params.length; ++i) {
101 var param1 = paramList1.params[i],
102 param2 = paramList2.params[i];
103 var match = isCloseEnough(param1, param2, tolerance);
110 // Return an array of numeric filter params in 0-1.
111 function getFilterParameters(s)
113 var filterResult = s.match(/(\w+)\((.+)\)/);
115 throw new Error("There's no filter in \"" + s + "\"");
116 var filterParams = filterResult[2];
117 var paramList = filterParams.split(' '); // FIXME: the spec may allow comma separation at some point.
119 // Normalize percentage values.
120 for (var i = 0; i < paramList.length; ++i) {
121 var param = paramList[i];
122 paramList[i] = parseFloat(paramList[i]);
123 if (param.indexOf('%') != -1)
124 paramList[i] = paramList[i] / 100;
130 function filterParametersMatch(paramList1, paramList2, tolerance)
132 if (paramList1.length != paramList2.length)
134 for (var i = 0; i < paramList1.length; ++i) {
135 var param1 = paramList1[i],
136 param2 = paramList2[i];
137 var match = isCloseEnough(param1, param2, tolerance);
144 function checkExpectedValue(expected, index)
146 log('Checking expectation: ' + JSON.stringify(expected[index]));
147 var time = expected[index][0];
148 var elementId = expected[index][1];
149 var property = expected[index][2];
150 var expectedValue = expected[index][3];
151 var tolerance = expected[index][4];
153 // Check for a pair of element Ids
154 var compareElements = false;
155 var element2Static = false;
157 if (typeof elementId != "string") {
158 if (elementId.length != 2)
161 elementId2 = elementId[1];
162 elementId = elementId[0];
164 if (elementId2.indexOf("static:") == 0) {
165 elementId2 = elementId2.replace("static:", "");
166 element2Static = true;
169 compareElements = true;
172 // Check for a dot separated string
174 if (!compareElements) {
175 var array = elementId.split('.');
176 if (array.length == 2) {
178 elementId = array[1];
182 var computedValue, computedValue2;
183 if (compareElements) {
184 computedValue = getPropertyValue(property, elementId, iframeId);
185 computedValue2 = getPropertyValue(property, elementId2, iframeId);
187 if (comparePropertyValue(property, computedValue, computedValue2, tolerance))
188 result += "PASS - \"" + property + "\" property for \"" + elementId + "\" and \"" + elementId2 +
189 "\" elements at " + time + "s are close enough to each other" + "<br>";
191 result += "FAIL - \"" + property + "\" property for \"" + elementId + "\" and \"" + elementId2 +
192 "\" elements at " + time + "s saw: \"" + computedValue + "\" and \"" + computedValue2 +
193 "\" which are not close enough to each other" + "<br>";
197 elementName = iframeId + '.' + elementId;
199 elementName = elementId;
201 computedValue = getPropertyValue(property, elementId, iframeId);
203 if (comparePropertyValue(property, computedValue, expectedValue, tolerance))
204 result += "PASS - \"" + property + "\" property for \"" + elementName + "\" element at " + time +
205 "s saw something close to: " + expectedValue + "<br>";
207 result += "FAIL - \"" + property + "\" property for \"" + elementName + "\" element at " + time +
208 "s expected: " + expectedValue + " but saw: " + computedValue + "<br>";
212 function compareRGB(rgb, expected, tolerance)
214 return (isCloseEnough(parseInt(rgb[0]), expected[0], tolerance) &&
215 isCloseEnough(parseInt(rgb[1]), expected[1], tolerance) &&
216 isCloseEnough(parseInt(rgb[2]), expected[2], tolerance));
219 function checkExpectedTransitionValue(expected, index)
221 log('Checking expectation: ' + JSON.stringify(expected[index]));
222 var time = expected[index][0];
223 var elementId = expected[index][1];
224 var property = expected[index][2];
225 var expectedValue = expected[index][3];
226 var tolerance = expected[index][4];
227 var postCompletionCallback = expected[index][5];
231 var transformRegExp = /^-webkit-transform(\.\d+)?$/;
232 if (transformRegExp.test(property)) {
233 computedValue = window.getComputedStyle(document.getElementById(elementId)).webkitTransform;
234 if (typeof expectedValue === "string" || computedValue === "none")
235 pass = (computedValue == expectedValue);
236 else if (typeof expectedValue === "number") {
237 var m = computedValue.split("(");
238 var m = m[1].split(",");
239 pass = isCloseEnough(parseFloat(m[parseInt(property.substring(18))]), expectedValue, tolerance);
241 var m = computedValue.split("(");
242 var m = m[1].split(",");
243 for (i = 0; i < expectedValue.length; ++i) {
244 pass = isCloseEnough(parseFloat(m[i]), expectedValue[i], tolerance);
249 } else if (property == "fill" || property == "stroke" || property == "stop-color" || property == "flood-color" || property == "lighting-color") {
250 computedValue = window.getComputedStyle(document.getElementById(elementId)).getPropertyCSSValue(property);
251 // The computedValue cssText is rgb(num, num, num)
252 var components = computedValue.cssText.split("(")[1].split(")")[0].split(",");
253 if (compareRGB(components, expectedValue, tolerance))
256 // We failed. Make sure computed value is something we can read in the error message
257 computedValue = computedValue.cssText;
259 } else if (property == "lineHeight") {
260 computedValue = parseInt(window.getComputedStyle(document.getElementById(elementId)).lineHeight);
261 pass = isCloseEnough(computedValue, expectedValue, tolerance);
262 } else if (property == "background-image"
263 || property == "border-image-source"
264 || property == "border-image"
265 || property == "list-style-image"
266 || property == "-webkit-mask-image"
267 || property == "-webkit-mask-box-image") {
268 if (property == "border-image" || property == "-webkit-mask-image" || property == "-webkit-mask-box-image")
269 property += "-source";
271 computedValue = window.getComputedStyle(document.getElementById(elementId)).getPropertyCSSValue(property).cssText;
272 computedCrossFade = parseCrossFade(computedValue);
274 if (!computedCrossFade) {
277 pass = isCloseEnough(computedCrossFade.percent, expectedValue, tolerance);
279 } else if (property == "object-position") {
280 computedValue = window.getComputedStyle(document.getElementById(elementId)).objectPosition;
281 var actualArray = computedValue.split(" ");
282 var expectedArray = expectedValue.split(" ");
283 if (actualArray.length != expectedArray.length) {
286 for (i = 0; i < expectedArray.length; ++i) {
287 pass = isCloseEnough(parseFloat(actualArray[i]), parseFloat(expectedArray[i]), tolerance);
292 } else if (property === "shape-inside" || property === "shape-outside") {
293 computedValue = window.getComputedStyle(document.getElementById(elementId)).getPropertyValue(property);
294 var actualShape = parseBasicShape(computedValue);
295 var expectedShape = parseBasicShape(expectedValue);
296 pass = basicShapeParametersMatch(actualShape, expectedShape, tolerance);
298 var computedStyle = window.getComputedStyle(document.getElementById(elementId)).getPropertyCSSValue(property);
299 if (computedStyle.cssValueType == CSSValue.CSS_VALUE_LIST) {
301 for (var i = 0; i < computedStyle.length; ++i) {
302 switch (computedStyle[i].cssValueType) {
303 case CSSValue.CSS_PRIMITIVE_VALUE:
304 if (computedStyle[i].primitiveType == CSSPrimitiveValue.CSS_STRING)
305 values.push(computedStyle[i].getStringValue());
307 values.push(computedStyle[i].getFloatValue(CSSPrimitiveValue.CSS_NUMBER));
309 case CSSValue.CSS_CUSTOM:
310 // arbitrarily pick shadow-x and shadow-y
311 if (property == 'box-shadow' || property == 'text-shadow') {
312 var text = computedStyle[i].cssText;
313 // Shadow cssText looks like "rgb(0, 0, 255) 0px -3px 10px 0px" and can be fractional.
314 var shadowPositionRegExp = /\)\s*(-?[\d.]+)px\s*(-?[\d.]+)px/;
315 var match = shadowPositionRegExp.exec(text);
316 var shadowXY = [parseInt(match[1]), parseInt(match[2])];
317 values.push(shadowXY[0]);
318 values.push(shadowXY[1]);
320 values.push(computedStyle[i].cssText);
324 computedValue = values.join(',');
326 for (var i = 0; i < values.length; ++i)
327 pass &= isCloseEnough(values[i], expectedValue[i], tolerance);
328 } else if (computedStyle.cssValueType == CSSValue.CSS_PRIMITIVE_VALUE) {
329 switch (computedStyle.primitiveType) {
330 case CSSPrimitiveValue.CSS_STRING:
331 case CSSPrimitiveValue.CSS_IDENT:
332 computedValue = computedStyle.getStringValue();
333 pass = computedValue == expectedValue;
335 case CSSPrimitiveValue.CSS_RGBCOLOR:
336 var rgbColor = computedStyle.getRGBColorValue();
337 computedValue = [rgbColor.red.getFloatValue(CSSPrimitiveValue.CSS_NUMBER),
338 rgbColor.green.getFloatValue(CSSPrimitiveValue.CSS_NUMBER),
339 rgbColor.blue.getFloatValue(CSSPrimitiveValue.CSS_NUMBER)]; // alpha is not exposed to JS
341 for (var i = 0; i < 3; ++i)
342 pass &= isCloseEnough(computedValue[i], expectedValue[i], tolerance);
344 case CSSPrimitiveValue.CSS_RECT:
345 computedValue = computedStyle.getRectValue();
346 computedValue = [computedValue.top.getFloatValue(CSSPrimitiveValue.CSS_NUMBER),
347 computedValue.right.getFloatValue(CSSPrimitiveValue.CSS_NUMBER),
348 computedValue.bottom.getFloatValue(CSSPrimitiveValue.CSS_NUMBER),
349 computedValue.left.getFloatValue(CSSPrimitiveValue.CSS_NUMBER)];
351 for (var i = 0; i < 4; ++i)
352 pass &= isCloseEnough(computedValue[i], expectedValue[i], tolerance);
354 case CSSPrimitiveValue.CSS_PERCENTAGE:
355 computedValue = parseFloat(computedStyle.cssText);
356 pass = isCloseEnough(computedValue, expectedValue, tolerance);
359 computedValue = computedStyle.getFloatValue(CSSPrimitiveValue.CSS_NUMBER);
360 pass = isCloseEnough(computedValue, expectedValue, tolerance);
366 result += "PASS - \"" + property + "\" property for \"" + elementId + "\" element at " + time + "s saw something close to: " + expectedValue + "<br>";
368 result += "FAIL - \"" + property + "\" property for \"" + elementId + "\" element at " + time + "s expected: " + expectedValue + " but saw: " + computedValue + "<br>";
370 if (postCompletionCallback)
371 result += postCompletionCallback();
375 function getPropertyValue(property, elementId, iframeId)
380 element = document.getElementById(iframeId).contentDocument.getElementById(elementId);
382 element = document.getElementById(elementId);
384 if (property == "lineHeight")
385 computedValue = parseInt(window.getComputedStyle(element).lineHeight);
386 else if (property == "backgroundImage"
387 || property == "borderImageSource"
388 || property == "listStyleImage"
389 || property == "webkitMaskImage"
390 || property == "webkitMaskBoxImage"
391 || property == "webkitFilter"
392 || property == "webkitClipPath"
393 || property == "shapeInside"
394 || !property.indexOf("webkitTransform")) {
395 computedValue = window.getComputedStyle(element)[property.split(".")[0]];
397 var computedStyle = window.getComputedStyle(element).getPropertyCSSValue(property);
399 computedValue = computedStyle.getFloatValue(CSSPrimitiveValue.CSS_NUMBER);
401 computedValue = computedStyle.cssText;
405 return computedValue;
408 function comparePropertyValue(property, computedValue, expectedValue, tolerance)
412 if (!property.indexOf("webkitTransform")) {
413 if (typeof expectedValue == "string")
414 result = (computedValue == expectedValue);
415 else if (typeof expectedValue == "number") {
416 var m = matrixStringToArray(computedValue);
417 result = isCloseEnough(parseFloat(m[parseInt(property.substring(16))]), expectedValue, tolerance);
419 var m = matrixStringToArray(computedValue);
420 for (i = 0; i < expectedValue.length; ++i) {
421 result = isCloseEnough(parseFloat(m[i]), expectedValue[i], tolerance);
426 } else if (property == "webkitFilter") {
427 var filterParameters = getFilterParameters(computedValue);
428 var filter2Parameters = getFilterParameters(expectedValue);
429 result = filterParametersMatch(filterParameters, filter2Parameters, tolerance);
430 } else if (property == "webkitClipPath" || property == "shapeInside") {
431 var clipPathParameters = parseBasicShape(computedValue);
432 var clipPathParameters2 = parseBasicShape(expectedValue);
433 if (!clipPathParameters || !clipPathParameters2)
435 result = basicShapeParametersMatch(clipPathParameters, clipPathParameters2, tolerance);
436 } else if (property == "backgroundImage"
437 || property == "borderImageSource"
438 || property == "listStyleImage"
439 || property == "webkitMaskImage"
440 || property == "webkitMaskBoxImage") {
441 var computedCrossFade = parseCrossFade(computedValue);
443 if (!computedCrossFade) {
446 if (typeof expectedValue == "string") {
447 var computedCrossFade2 = parseCrossFade(expectedValue);
448 result = isCloseEnough(computedCrossFade.percent, computedCrossFade2.percent, tolerance) && computedCrossFade.from == computedCrossFade2.from && computedCrossFade.to == computedCrossFade2.to;
450 result = isCloseEnough(computedCrossFade.percent, expectedValue, tolerance)
454 if (typeof expectedValue == "string")
455 result = (computedValue == expectedValue);
457 result = isCloseEnough(computedValue, expectedValue, tolerance);
465 var resultElement = useResultElement ? document.getElementById('result') : document.documentElement;
466 if (ENABLE_ERROR_LOGGING && result.indexOf('FAIL') >= 0)
467 result += '<br>Log:<br>' + logMessages.join('<br>');
468 resultElement.innerHTML = result;
470 if (window.testRunner)
471 testRunner.notifyDone();
474 function runChecksWithRAF(checks)
477 var time = performance.now() - animStartTime;
479 log('RAF callback, animation time: ' + time);
480 for (var k in checks) {
481 var checkTime = Number(k);
482 if (checkTime > time) {
486 log('Running checks for time: ' + checkTime + ', delay: ' + (time - checkTime));
487 checks[k].forEach(function(check) { check(); });
494 requestAnimationFrame(runChecksWithRAF.bind(null, checks));
497 function runChecksWithPauseAPI(checks) {
498 for (var k in checks) {
499 var timeMs = Number(k);
500 log('Pausing at time: ' + timeMs + ', active animations: ' + internals.numberOfActiveAnimations());
501 internals.pauseAnimations(timeMs / 1000);
502 checks[k].forEach(function(check) { check(); });
507 function startTest(checks)
509 if (hasPauseAnimationAPI)
510 runChecksWithPauseAPI(checks);
512 result += 'Warning this test is running in real-time and may be flaky.<br>';
513 runChecksWithRAF(checks);
517 var logMessages = [];
518 var useResultElement = false;
520 var hasPauseAnimationAPI;
522 var isTransitionsTest = false;
524 function log(message)
526 logMessages.push(performance.now() + ' - ' + message);
529 function waitForAnimationsToStart(callback)
531 if (!window.internals || internals.numberOfActiveAnimations() > 0) {
534 setTimeout(waitForAnimationsToStart.bind(this, callback), 0);
538 // FIXME: disablePauseAnimationAPI and doPixelTest
539 function runAnimationTest(expected, callbacks, trigger, disablePauseAnimationAPI, doPixelTest, startTestImmediately)
541 log('runAnimationTest');
543 throw "Expected results are missing!";
545 hasPauseAnimationAPI = 'internals' in window;
546 if (disablePauseAnimationAPI)
547 hasPauseAnimationAPI = false;
551 if (typeof callbacks == 'function') {
552 checks[0] = [callbacks];
553 } else for (var time in callbacks) {
554 timeMs = Math.round(time * 1000);
555 checks[timeMs] = [callbacks[time]];
558 for (var i = 0; i < expected.length; i++) {
559 var expectation = expected[i];
560 var timeMs = Math.round(expectation[0] * 1000);
563 if (isTransitionsTest)
564 checks[timeMs].push(checkExpectedTransitionValue.bind(null, expected, i));
566 checks[timeMs].push(checkExpectedValue.bind(null, expected, i));
569 var doPixelTest = Boolean(doPixelTest);
570 useResultElement = doPixelTest;
572 if (window.testRunner) {
574 testRunner.dumpAsTextWithPixelResults();
576 testRunner.dumpAsText();
578 testRunner.waitUntilDone();
585 log('First ' + event + ' event fired');
589 document.documentElement.offsetTop
591 waitForAnimationsToStart(function() {
592 log('Finished waiting for animations to start');
593 animStartTime = performance.now();
599 var startTestImmediately = Boolean(startTestImmediately);
600 if (startTestImmediately) {
603 var target = isTransitionsTest ? window : document;
604 var event = isTransitionsTest ? 'load' : 'webkitAnimationStart';
605 target.addEventListener(event, begin, false);
609 /* This is the helper function to run transition tests:
611 Test page requirements:
612 - The body must contain an empty div with id "result"
613 - Call this function directly from the <script> inside the test page
616 expected [required]: an array of arrays defining a set of CSS properties that must have given values at specific times (see below)
617 trigger [optional]: a function to be executed just before the test starts (none by default)
618 callbacks [optional]: an object in the form {timeS: function} specifing callbacks to be made during the test
619 doPixelTest [optional]: whether to dump pixels during the test (false by default)
620 disablePauseAnimationAPI [optional]: whether to disable the pause API and run a RAF-based test (false by default)
622 Each sub-array must contain these items in this order:
623 - the time in seconds at which to snapshot the CSS property
624 - the id of the element on which to get the CSS property value
625 - the name of the CSS property to get [1]
626 - the expected value for the CSS property
627 - the tolerance to use when comparing the effective CSS property value with its expected value
629 [1] If the CSS property name is "-webkit-transform", expected value must be an array of 1 or more numbers corresponding to the matrix elements,
630 or a string which will be compared directly (useful if the expected value is "none")
631 If the CSS property name is "-webkit-transform.N", expected value must be a number corresponding to the Nth element of the matrix
634 function runTransitionTest(expected, trigger, callbacks, doPixelTest, disablePauseAnimationAPI) {
635 isTransitionsTest = true;
636 runAnimationTest(expected, callbacks, trigger, disablePauseAnimationAPI, doPixelTest);