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-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.indexOf("webkitTransform")) {
394 computedValue = window.getComputedStyle(element)[property.split(".")[0]];
396 var computedStyle = window.getComputedStyle(element).getPropertyCSSValue(property);
398 computedValue = computedStyle.getFloatValue(CSSPrimitiveValue.CSS_NUMBER);
400 computedValue = computedStyle.cssText;
404 return computedValue;
407 function comparePropertyValue(property, computedValue, expectedValue, tolerance)
411 if (!property.indexOf("webkitTransform")) {
412 if (typeof expectedValue == "string")
413 result = (computedValue == expectedValue);
414 else if (typeof expectedValue == "number") {
415 var m = matrixStringToArray(computedValue);
416 result = isCloseEnough(parseFloat(m[parseInt(property.substring(16))]), expectedValue, tolerance);
418 var m = matrixStringToArray(computedValue);
419 for (i = 0; i < expectedValue.length; ++i) {
420 result = isCloseEnough(parseFloat(m[i]), expectedValue[i], tolerance);
425 } else if (property == "webkitFilter") {
426 var filterParameters = getFilterParameters(computedValue);
427 var filter2Parameters = getFilterParameters(expectedValue);
428 result = filterParametersMatch(filterParameters, filter2Parameters, tolerance);
429 } else if (property == "webkitClipPath") {
430 var clipPathParameters = parseBasicShape(computedValue);
431 var clipPathParameters2 = parseBasicShape(expectedValue);
432 if (!clipPathParameters || !clipPathParameters2)
434 result = basicShapeParametersMatch(clipPathParameters, clipPathParameters2, tolerance);
435 } else if (property == "backgroundImage"
436 || property == "borderImageSource"
437 || property == "listStyleImage"
438 || property == "webkitMaskImage"
439 || property == "webkitMaskBoxImage") {
440 var computedCrossFade = parseCrossFade(computedValue);
442 if (!computedCrossFade) {
445 if (typeof expectedValue == "string") {
446 var computedCrossFade2 = parseCrossFade(expectedValue);
447 result = isCloseEnough(computedCrossFade.percent, computedCrossFade2.percent, tolerance) && computedCrossFade.from == computedCrossFade2.from && computedCrossFade.to == computedCrossFade2.to;
449 result = isCloseEnough(computedCrossFade.percent, expectedValue, tolerance)
453 if (typeof expectedValue == "string")
454 result = (computedValue == expectedValue);
456 result = isCloseEnough(computedValue, expectedValue, tolerance);
464 var resultElement = useResultElement ? document.getElementById('result') : document.documentElement;
465 if (ENABLE_ERROR_LOGGING && result.indexOf('FAIL') >= 0)
466 result += '<br>Log:<br>' + logMessages.join('<br>');
467 resultElement.innerHTML = result;
469 if (window.testRunner)
470 testRunner.notifyDone();
473 function runChecksWithRAF(checks)
476 var time = performance.now() - animStartTime;
478 log('RAF callback, animation time: ' + time);
479 for (var k in checks) {
480 var checkTime = Number(k);
481 if (checkTime > time) {
485 log('Running checks for time: ' + checkTime + ', delay: ' + (time - checkTime));
486 checks[k].forEach(function(check) { check(); });
493 requestAnimationFrame(runChecksWithRAF.bind(null, checks));
496 function runChecksWithPauseAPI(checks) {
497 for (var k in checks) {
498 var timeMs = Number(k);
499 log('Pausing at time: ' + timeMs + ', active animations: ' + internals.numberOfActiveAnimations());
500 internals.pauseAnimations(timeMs / 1000);
501 checks[k].forEach(function(check) { check(); });
506 function startTest(checks)
508 if (hasPauseAnimationAPI)
509 runChecksWithPauseAPI(checks);
511 result += 'Warning this test is running in real-time and may be flaky.<br>';
512 runChecksWithRAF(checks);
516 var logMessages = [];
517 var useResultElement = false;
519 var hasPauseAnimationAPI;
521 var isTransitionsTest = false;
523 function log(message)
525 logMessages.push(performance.now() + ' - ' + message);
528 function waitForAnimationsToStart(callback)
530 if (!window.internals || internals.numberOfActiveAnimations() > 0) {
533 setTimeout(waitForAnimationsToStart.bind(this, callback), 0);
537 // FIXME: disablePauseAnimationAPI and doPixelTest
538 function runAnimationTest(expected, callbacks, trigger, disablePauseAnimationAPI, doPixelTest, startTestImmediately)
540 log('runAnimationTest');
542 throw "Expected results are missing!";
544 hasPauseAnimationAPI = 'internals' in window;
545 if (disablePauseAnimationAPI)
546 hasPauseAnimationAPI = false;
550 if (typeof callbacks == 'function') {
551 checks[0] = [callbacks];
552 } else for (var time in callbacks) {
553 timeMs = Math.round(time * 1000);
554 checks[timeMs] = [callbacks[time]];
557 for (var i = 0; i < expected.length; i++) {
558 var expectation = expected[i];
559 var timeMs = Math.round(expectation[0] * 1000);
562 if (isTransitionsTest)
563 checks[timeMs].push(checkExpectedTransitionValue.bind(null, expected, i));
565 checks[timeMs].push(checkExpectedValue.bind(null, expected, i));
568 var doPixelTest = Boolean(doPixelTest);
569 useResultElement = doPixelTest;
571 if (window.testRunner) {
573 testRunner.dumpAsTextWithPixelResults();
575 testRunner.dumpAsText();
577 testRunner.waitUntilDone();
584 log('First ' + event + ' event fired');
588 document.documentElement.offsetTop
590 waitForAnimationsToStart(function() {
591 log('Finished waiting for animations to start');
592 animStartTime = performance.now();
598 var startTestImmediately = Boolean(startTestImmediately);
599 if (startTestImmediately) {
602 var target = isTransitionsTest ? window : document;
603 var event = isTransitionsTest ? 'load' : 'webkitAnimationStart';
604 target.addEventListener(event, begin, false);
608 /* This is the helper function to run transition tests:
610 Test page requirements:
611 - The body must contain an empty div with id "result"
612 - Call this function directly from the <script> inside the test page
615 expected [required]: an array of arrays defining a set of CSS properties that must have given values at specific times (see below)
616 trigger [optional]: a function to be executed just before the test starts (none by default)
617 callbacks [optional]: an object in the form {timeS: function} specifing callbacks to be made during the test
618 doPixelTest [optional]: whether to dump pixels during the test (false by default)
619 disablePauseAnimationAPI [optional]: whether to disable the pause API and run a RAF-based test (false by default)
621 Each sub-array must contain these items in this order:
622 - the time in seconds at which to snapshot the CSS property
623 - the id of the element on which to get the CSS property value
624 - the name of the CSS property to get [1]
625 - the expected value for the CSS property
626 - the tolerance to use when comparing the effective CSS property value with its expected value
628 [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,
629 or a string which will be compared directly (useful if the expected value is "none")
630 If the CSS property name is "-webkit-transform.N", expected value must be a number corresponding to the Nth element of the matrix
633 function runTransitionTest(expected, trigger, callbacks, doPixelTest, disablePauseAnimationAPI) {
634 isTransitionsTest = true;
635 runAnimationTest(expected, callbacks, trigger, disablePauseAnimationAPI, doPixelTest);