Unprefix -webkit-cross-fade()
[WebKit-https.git] / LayoutTests / animations / resources / animation-test-helpers.js
1 /* This is the helper function to run animation tests:
2
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
6
7 Function parameters:
8     expected [required]: an array of arrays defining a set of CSS properties that must have given values at specific times (see below)
9     callback [optional]: a function to be executed just before the test starts (none by default)
10     event [optional]: which DOM event to wait for before starting the test ("webkitAnimationStart" by default)
11
12     Each sub-array must contain these items in this order:
13     - the name of the CSS animation (may be null) [1]
14     - the time in seconds at which to snapshot the CSS property
15     - the id of the element on which to get the CSS property value [2]
16     - the name of the CSS property to get [3]
17     - the expected value for the CSS property
18     - the tolerance to use when comparing the effective CSS property value with its expected value
19
20     [1] If null is passed, a regular setTimeout() will be used instead to snapshot the animated property in the future,
21     instead of fast forwarding using the pauseAnimationAtTimeOnElement() JS API from Internals.
22     
23     [2] If a single string is passed, it is the id of the element to test. If an array with 2 elements is passed they
24     are the ids of 2 elements, whose values are compared for equality. In this case the expected value is ignored
25     but the tolerance is used in the comparison. If the second element is prefixed with "static:", no animation on that
26     element is required, allowing comparison with an unanimated "expected value" element.
27     
28     If a string with a '.' is passed, this is an element in an iframe. The string before the dot is the iframe id
29     and the string after the dot is the element name in that iframe.
30
31     [3] If the CSS property name is "webkitTransform", expected value must be an array of 1 or more numbers corresponding to the matrix elements,
32     or a string which will be compared directly (useful if the expected value is "none")
33     If the CSS property name is "webkitTransform.N", expected value must be a number corresponding to the Nth element of the matrix
34
35 */
36
37 function isCloseEnough(actual, expected, tolerance)
38 {
39     if (isNaN(actual) || isNaN(expected))
40         return false;
41     var diff = Math.abs(actual - expected);
42     return diff <= tolerance;
43 }
44
45 function matrixStringToArray(s)
46 {
47     if (s == "none")
48         return [ 1, 0, 0, 1, 0, 0 ];
49     var m = s.split("(");
50     m = m[1].split(")");
51     return m[0].split(",");
52 }
53
54 function parseCSSImage(s)
55 {
56     // Special case none.
57     if (s == "none")
58         return ["none"];
59
60     // Separate function name from function value.
61     var matches = s.match("([\\w\\-]+)\\((.*)\\)");
62     if (!matches){
63         console.error("Parsing error. Value not a CSS Image function ", s);
64         return false;
65     }
66
67     var functionName = matches[1];
68     var functionValue = matches[2];
69
70     // Generator functions can have CSS images as values themself.
71     // These functions will call parseCSSImage for each CSS Image.
72     switch (functionName) {
73     case "filter":
74         return parseFilterImage(functionValue);
75     case "cross-fade":
76     case "-webkit-cross-fade":
77         return parseCrossFade(functionValue);
78     case "url":
79         return ["url", functionValue];
80     // FIXME: Add support for linear and redial gradient.
81     default:
82         // All supported filter functions must be listed above.
83         return false;
84     }
85 }
86
87 // This should just be called by parseCSSImage.
88 function parseCrossFade(s)
89 {
90     var matches = s.match("(.*)\\s*,\\s*(.*)\\s*,\\s*(.*)\\s*");
91     if (!matches) {
92         console.error("Parsing error on 'cross-fade()'.");
93         return null;
94     }
95
96     var from = parseCSSImage(matches[1]);
97     var to = parseCSSImage(matches[2]);
98     if (!from || !to) {
99         console.error("Parsing error on images passed to 'cross-fade()' ", s);
100         return null;
101     }
102
103     var fadeValue = matches[3];
104     var percent;
105     if (isNaN(fadeValue)) {
106         // Check if last char is '%' and rip it off.
107         // Normalize it to number.
108         if (fadeValue.search('%') != fadeValue.length - 1) {
109             console.error("Passed value to 'cross-fade()' is not a number or percentage ", fadeValue);
110             return null;
111         }
112         fadeValue = fadeValue.slice(0, fadeValue.length - 1);
113         if (isNaN(fadeValue)) {
114             console.error("Passed value to 'cross-fade()' is not a number or percentage ", fadeValue);
115             return null;
116         }
117         percent = parseFloat(fadeValue) / 100;
118     } else
119         percent = parseFloat(fadeValue);
120
121     return ["cross-fade", from, to, percent];
122 }
123
124 // This should just be called by parseCSSImage.
125 function parseFilterImage(s)
126 {
127     // Separate image value from filter function list.
128     var matches = s.match("([\\-\\w]+\\(.*\\))\\s*,\\s*(.*)\\s*");
129     if (!matches) {
130         console.error("Parsing error on 'filter()' ", s);
131         return false;
132     }
133
134     var image = parseCSSImage(matches[1]);
135     if (!image) {
136         console.error("Parsing error on image passed to 'filter()' ", s);
137         return false;
138     }
139
140     var filterFunctionList = parseFilterFunctionList(matches[2]);
141     if (!filterFunctionList) {
142         console.error("Parsing error on filter function list passed to 'filter()' ", s);
143         return false;
144     }
145
146     return ["-webkit-filter", image, filterFunctionList];
147 }
148
149 function parseFilterFunctionList(s)
150 {
151     var reg = /\)*\s*(blur|brightness|contrast|drop\-shadow|grayscale|hue\-rotate|invert|opacity|saturate|sepia|url)\(/
152     var matches = s.split(reg);
153
154     // First item must be empty. All other items are of functionName, functionValue.
155     if (!matches || matches.shift() != "")
156         return null;
157
158     // Odd items are the function name, even items the function value.
159     if (!matches.length || matches.length % 2)
160         return null;
161
162     var functionList = [];
163     for (var i = 0; i < matches.length; i += 2) {
164         var functionName = matches[i];
165         var functionValue = matches[i+1];
166         functionList.push(functionName);
167         if (functionName == "drop-shadow" || functionName == "url") {
168             // FIXME: Support parsing of drop-shadow.
169             functionList.push(functionValue);
170             continue;
171         }
172         functionList.push(parseFloat(functionValue));
173     }
174     return functionList;
175 }
176
177 function parseBasicShape(s)
178 {
179     var shapeFunction = s.match(/(\w+)\((.+)\)/);
180     if (!shapeFunction)
181         return null;
182
183     var matches;
184     switch (shapeFunction[1]) {
185     case "inset":
186         matches = s.match("inset\\(\\s*(.*)\\s*\\)");
187         matches = matches[1].split(/\s+/);
188         matches.unshift(s);
189         break;
190     case "circle":
191         matches = s.match("circle\\((.*)\\s+at\\s+(.*)\\s+(.*)\\)");
192         break;
193     case "ellipse":
194         matches = s.match("ellipse\\((.*)\\s+(.*)\\s+at\\s+(.*)\\s+(.*)\\)");
195         break;
196     case "polygon":
197         matches = s.match("polygon\\(\\s*(.*)\\s*\\)");
198         matches = matches[1].split(/\s*,\s*/);
199         matches = matches.map(function(match) {
200             return match.split(/\s+/);
201         });
202         matches = Array.prototype.concat.apply([s], matches);
203         break;
204     default:
205         return null;
206     }
207
208     if (!matches)
209         return null;
210
211     matches.shift();
212     var i = 0;
213     if (shapeFunction[1] == "polygon")
214         i++; // skip nonzero|evenodd below
215
216     // Normalize percentage values.
217     for (; i < matches.length; ++i) {
218         var param = parseFloat(matches[i]);
219
220         if (isNaN(param))
221             continue;
222
223         if (matches[i].indexOf('%') != -1)
224             matches[i] = param / 100;
225         else
226             matches[i] = param;
227     }
228
229     return {"shape": shapeFunction[1], "params": matches};
230 }
231
232 function compareCSSImages(computedValue, expectedValue, tolerance)
233 {
234     var actual = typeof computedValue === "string" ? parseCSSImage(computedValue) : computedValue;
235     var expected = typeof expectedValue === "string" ? parseCSSImage(expectedValue) : expectedValue;
236     if (!actual || !Array.isArray(actual)         // Check that: actual is an array,
237         || !expected || !Array.isArray(expected)  // expected is an array,
238         || actual[0] != expected[0]) {            // and image function names are the same.
239         console.error("Unexpected mismatch between CSS Image functions.");
240         return false;
241     }
242     switch (actual[0]) {
243     case "none":
244         return true;
245     case "-webkit-filter":
246         return compareCSSImages(actual[1], expected[1], tolerance)
247             && compareFilterFunctions(actual[2], expected[2], tolerance);
248     case "cross-fade":
249     case "-webkit-cross-fade":
250         return compareCSSImages(actual[1], expected[1], tolerance)
251             && compareCSSImages(actual[2], expected[2], tolerance)
252             && isCloseEnough(actual[3], expected[3], tolerance);
253     case "url":
254         return actual[1].search(expected[1]) >= 0;
255     default:
256         console.error("Unknown CSS Image function ", actual[0]);
257         return false;
258     }
259 }
260
261 // Called by CSS Image function filter() as well as filter property.
262 function compareFilterFunctions(computedValue, expectedValue, tolerance)
263 {
264     var actual = typeof computedValue === "string" ? parseFilterFunctionList(computedValue) : computedValue;
265     var expected = typeof expectedValue === "string" ? parseFilterFunctionList(expectedValue) : expectedValue;
266     if (!actual || !Array.isArray(actual)         // Check that: actual is an array,
267         || !expected || !Array.isArray(expected)  // expected is an array,
268         || !actual.length                         // actual array has entries,
269         || actual.length != expected.length       // actual and expected have same length
270         || actual.length % 2 == 1)                // and image function names are the same.
271         return false;
272
273     for (var i = 0; i < actual.length; i += 2) {
274         if (actual[i] != expected[i]) {
275             console.error("Filter functions do not match.");
276             return false;
277         }
278         if (!isCloseEnough(actual[i+1], expected[i+1], tolerance)) {
279             console.error("Filter function values do not match.");
280             return false;
281         }
282     }
283     return true;
284 }
285
286 function basicShapeParametersMatch(paramList1, paramList2, tolerance)
287 {
288     if (paramList1.shape != paramList2.shape
289         || paramList1.params.length != paramList2.params.length)
290         return false;
291     var i = 0;
292     for (; i < paramList1.params.length; ++i) {
293         var param1 = paramList1.params[i], 
294             param2 = paramList2.params[i];
295         if (param1 === param2)
296             continue;
297         var match = isCloseEnough(param1, param2, tolerance);
298         if (!match)
299             return false;
300     }
301     return true;
302 }
303
304 function checkExpectedValue(expected, index)
305 {
306     var animationName = expected[index][0];
307     var time = expected[index][1];
308     var elementId = expected[index][2];
309     var property = expected[index][3];
310     var expectedValue = expected[index][4];
311     var tolerance = expected[index][5];
312
313     // Check for a pair of element Ids
314     var compareElements = false;
315     var element2Static = false;
316     var elementId2;
317     if (typeof elementId != "string") {
318         if (elementId.length != 2)
319             return;
320             
321         elementId2 = elementId[1];
322         elementId = elementId[0];
323
324         if (elementId2.indexOf("static:") == 0) {
325             elementId2 = elementId2.replace("static:", "");
326             element2Static = true;
327         }
328
329         compareElements = true;
330     }
331     
332     // Check for a dot separated string
333     var iframeId;
334     if (!compareElements) {
335         var array = elementId.split('.');
336         if (array.length == 2) {
337             iframeId = array[0];
338             elementId = array[1];
339         }
340     }
341
342     if (animationName && hasPauseAnimationAPI && !internals.pauseAnimationAtTimeOnElement(animationName, time, document.getElementById(elementId))) {
343         result += "FAIL - animation \"" + animationName + "\" is not running" + "<br>";
344         return;
345     }
346     
347     if (compareElements && !element2Static && animationName && hasPauseAnimationAPI && !internals.pauseAnimationAtTimeOnElement(animationName, time, document.getElementById(elementId2))) {
348         result += "FAIL - animation \"" + animationName + "\" is not running" + "<br>";
349         return;
350     }
351     
352     var computedValue, computedValue2;
353     if (compareElements) {
354         computedValue = getPropertyValue(property, elementId, iframeId);
355         computedValue2 = getPropertyValue(property, elementId2, iframeId);
356
357         if (comparePropertyValue(property, computedValue, computedValue2, tolerance))
358             result += "PASS - \"" + property + "\" property for \"" + elementId + "\" and \"" + elementId2 + 
359                             "\" elements at " + time + "s are close enough to each other" + "<br>";
360         else
361             result += "FAIL - \"" + property + "\" property for \"" + elementId + "\" and \"" + elementId2 + 
362                             "\" elements at " + time + "s saw: \"" + computedValue + "\" and \"" + computedValue2 + 
363                                             "\" which are not close enough to each other" + "<br>";
364     } else {
365         var elementName;
366         if (iframeId)
367             elementName = iframeId + '.' + elementId;
368         else
369             elementName = elementId;
370
371         computedValue = getPropertyValue(property, elementId, iframeId);
372
373         if (comparePropertyValue(property, computedValue, expectedValue, tolerance))
374             result += "PASS - \"" + property + "\" property for \"" + elementName + "\" element at " + time + 
375                             "s saw something close to: " + expectedValue + "<br>";
376         else
377             result += "FAIL - \"" + property + "\" property for \"" + elementName + "\" element at " + time + 
378                             "s expected: " + expectedValue + " but saw: " + computedValue + "<br>";
379     }
380 }
381
382
383 function getPropertyValue(property, elementId, iframeId)
384 {
385     var computedValue;
386     var element;
387     if (iframeId)
388         element = document.getElementById(iframeId).contentDocument.getElementById(elementId);
389     else
390         element = document.getElementById(elementId);
391
392     if (property == "lineHeight")
393         computedValue = parseInt(window.getComputedStyle(element).lineHeight);
394     else if (property == "backgroundImage"
395                || property == "borderImageSource"
396                || property == "listStyleImage"
397                || property == "webkitMaskImage"
398                || property == "webkitMaskBoxImage"
399                || property == "webkitFilter"
400                || property == "webkitBackdropFilter"
401                || property == "webkitClipPath"
402                || property == "webkitShapeInside"
403                || property == "webkitShapeOutside"
404                || !property.indexOf("webkitTransform")
405                || !property.indexOf("transform")) {
406         computedValue = window.getComputedStyle(element)[property.split(".")[0]];
407     } else {
408         var computedStyle = window.getComputedStyle(element).getPropertyCSSValue(property);
409         computedValue = computedStyle.getFloatValue(CSSPrimitiveValue.CSS_NUMBER);
410     }
411
412     return computedValue;
413 }
414
415 function comparePropertyValue(property, computedValue, expectedValue, tolerance)
416 {
417     var result = true;
418
419     if (!property.indexOf("webkitTransform") || !property.indexOf("transform")) {
420         if (typeof expectedValue == "string")
421             result = (computedValue == expectedValue);
422         else if (typeof expectedValue == "number") {
423             var m = matrixStringToArray(computedValue);
424             result = isCloseEnough(parseFloat(m[parseInt(property.substring(16))]), expectedValue, tolerance);
425         } else {
426             var m = matrixStringToArray(computedValue);
427             for (i = 0; i < expectedValue.length; ++i) {
428                 result = isCloseEnough(parseFloat(m[i]), expectedValue[i], tolerance);
429                 if (!result)
430                     break;
431             }
432         }
433     } else if (property == "webkitFilter" || property == "webkitBackdropFilter") {
434         var filterParameters = parseFilterFunctionList(computedValue);
435         var filter2Parameters = parseFilterFunctionList(expectedValue);
436         result = compareFilterFunctions(filterParameters, filter2Parameters, tolerance);
437     } else if (property == "webkitClipPath" || property == "webkitShapeInside" || property == "webkitShapeOutside") {
438         var clipPathParameters = parseBasicShape(computedValue);
439         var clipPathParameters2 = parseBasicShape(expectedValue);
440         if (!clipPathParameters || !clipPathParameters2)
441             result = false;
442         result = basicShapeParametersMatch(clipPathParameters, clipPathParameters2, tolerance);
443     } else if (property == "backgroundImage"
444                || property == "borderImageSource"
445                || property == "listStyleImage"
446                || property == "webkitMaskImage"
447                || property == "webkitMaskBoxImage")
448         result = compareCSSImages(computedValue, expectedValue, tolerance);
449     else {
450         result = isCloseEnough(computedValue, expectedValue, tolerance);
451     }
452     return result;
453 }
454
455 function endTest(finishCallback)
456 {
457     document.getElementById('result').innerHTML = result;
458
459     if (finishCallback)
460         finishCallback();
461
462     if (window.testRunner)
463         testRunner.notifyDone();
464 }
465
466 function checkExpectedValueCallback(expected, index)
467 {
468     return function() { checkExpectedValue(expected, index); };
469 }
470
471 var testStarted = false;
472 function startTest(expected, startCallback, finishCallback)
473 {
474     if (testStarted) return;
475     testStarted = true;
476
477     if (startCallback)
478         startCallback();
479
480     var maxTime = 0;
481
482     for (var i = 0; i < expected.length; ++i) {
483         var animationName = expected[i][0];
484         var time = expected[i][1];
485
486         // We can only use the animation fast-forward mechanism if there's an animation name
487         // and Internals implements pauseAnimationAtTimeOnElement()
488         if (animationName && hasPauseAnimationAPI)
489             checkExpectedValue(expected, i);
490         else {
491             if (time > maxTime)
492                 maxTime = time;
493
494             window.setTimeout(checkExpectedValueCallback(expected, i), time * 1000);
495         }
496     }
497
498     if (maxTime > 0)
499         window.setTimeout(function () {
500             endTest(finishCallback);
501         }, maxTime * 1000 + 50);
502     else
503         endTest(finishCallback);
504 }
505
506 var result = "";
507 var hasPauseAnimationAPI;
508
509 function runAnimationTest(expected, startCallback, event, disablePauseAnimationAPI, doPixelTest, finishCallback)
510 {
511     hasPauseAnimationAPI = 'internals' in window;
512     if (disablePauseAnimationAPI)
513         hasPauseAnimationAPI = false;
514
515     if (window.testRunner) {
516         if (!doPixelTest)
517             testRunner.dumpAsText();
518         testRunner.waitUntilDone();
519     }
520     
521     if (!expected)
522         throw("Expected results are missing!");
523     
524     var target = document;
525     if (event == undefined)
526         waitForAnimationToStart(target, function() { startTest(expected, startCallback, finishCallback); });
527     else if (event == "load")
528         window.addEventListener(event, function() {
529             startTest(expected, startCallback, finishCallback);
530         }, false);
531 }
532
533 function waitForAnimationToStart(element, callback)
534 {
535     element.addEventListener('webkitAnimationStart', function() {
536         window.setTimeout(callback, 0); // delay to give hardware animations a chance to start
537     }, false);
538 }