Support reverse and alternate-reverse in CA animations
[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 pauseAnimationAtTimeOnElementWithId() JS API from DRT
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, desired, tolerance)
38 {
39     var diff = Math.abs(actual - desired);
40     return diff <= tolerance;
41 }
42
43 function matrixStringToArray(s)
44 {
45     if (s == "none")
46         return [ 1, 0, 0, 1, 0, 0 ];
47     var m = s.split("(");
48     m = m[1].split(")");
49     return m[0].split(",");
50 }
51
52 function parseCrossFade(s)
53 {
54     var matches = s.match("-webkit-cross-fade\\((.*)\\s*,\\s*(.*)\\s*,\\s*(.*)\\)");
55
56     if (!matches)
57         return null;
58
59     return {"from": matches[1], "to": matches[2], "percent": parseFloat(matches[3])}
60 }
61
62 // Return an array of numeric filter params in 0-1.
63 function getFilterParameters(s)
64 {
65     var filterResult = s.match(/(\w+)\((.+)\)/);
66     if (!filterResult)
67         throw new Error("There's no filter in \"" + s + "\"");
68     var filterParams = filterResult[2];
69     if (filterResult[1] == "custom") {
70         if (!window.getCustomFilterParameters)
71             throw new Error("getCustomFilterParameters not found. Did you include custom-filter-parser.js?");
72         return getCustomFilterParameters(filterParams);
73     }
74     var paramList = filterParams.split(' '); // FIXME: the spec may allow comma separation at some point.
75     
76     // Normalize percentage values.
77     for (var i = 0; i < paramList.length; ++i) {
78         var param = paramList[i];
79         paramList[i] = parseFloat(paramList[i]);
80         if (param.indexOf('%') != -1)
81             paramList[i] = paramList[i] / 100;
82     }
83
84     return paramList;
85 }
86
87 function filterParametersMatch(paramList1, paramList2, tolerance)
88 {
89     if (paramList1.length != paramList2.length)
90         return false;
91     for (var i = 0; i < paramList1.length; ++i) {
92         var param1 = paramList1[i], 
93             param2 = paramList2[i];
94         if (typeof param1 == "object") {
95             // This is a custom filter parameter.
96             if (param1.type != "parameter") {
97                 // Checking for shader uris and other keywords. They need to be exactly the same.
98                 if (param1.type != param2.type
99                     || param1.value != param2.value)
100                     return false;
101                 continue;
102             }
103             if (param1.name != param2.name
104                 || param1.value.length != param2.value.length)
105                 return false;
106             // For now we only support floats.
107             for (var j = 0; j < param1.value.length; ++j) {
108                 if (!isCloseEnough(param1.value[j].value, param2.value[j].value, tolerance))
109                     return false;
110             }
111             continue;
112         }
113         var match = isCloseEnough(param1, param2, tolerance);
114         if (!match)
115             return false;
116     }
117     return true;
118 }
119
120 function checkExpectedValue(expected, index)
121 {
122     var animationName = expected[index][0];
123     var time = expected[index][1];
124     var elementId = expected[index][2];
125     var property = expected[index][3];
126     var expectedValue = expected[index][4];
127     var tolerance = expected[index][5];
128
129     // Check for a pair of element Ids
130     var compareElements = false;
131     var element2Static = false;
132     var elementId2;
133     if (typeof elementId != "string") {
134         if (elementId.length != 2)
135             return;
136             
137         elementId2 = elementId[1];
138         elementId = elementId[0];
139
140         if (elementId2.indexOf("static:") == 0) {
141             elementId2 = elementId2.replace("static:", "");
142             element2Static = true;
143         }
144
145         compareElements = true;
146     }
147     
148     // Check for a dot separated string
149     var iframeId;
150     if (!compareElements) {
151         var array = elementId.split('.');
152         if (array.length == 2) {
153             iframeId = array[0];
154             elementId = array[1];
155         }
156     }
157
158     if (animationName && hasPauseAnimationAPI && !layoutTestController.pauseAnimationAtTimeOnElementWithId(animationName, time, elementId)) {
159         result += "FAIL - animation \"" + animationName + "\" is not running" + "<br>";
160         return;
161     }
162     
163     if (compareElements && !element2Static && animationName && hasPauseAnimationAPI && !layoutTestController.pauseAnimationAtTimeOnElementWithId(animationName, time, elementId2)) {
164         result += "FAIL - animation \"" + animationName + "\" is not running" + "<br>";
165         return;
166     }
167     
168     var computedValue, computedValue2;
169     if (compareElements) {
170         computedValue = getPropertyValue(property, elementId, iframeId);
171         computedValue2 = getPropertyValue(property, elementId2, iframeId);
172
173         if (comparePropertyValue(property, computedValue, computedValue2, tolerance))
174             result += "PASS - \"" + property + "\" property for \"" + elementId + "\" and \"" + elementId2 + 
175                             "\" elements at " + time + "s are close enough to each other" + "<br>";
176         else
177             result += "FAIL - \"" + property + "\" property for \"" + elementId + "\" and \"" + elementId2 + 
178                             "\" elements at " + time + "s saw: \"" + computedValue + "\" and \"" + computedValue2 + 
179                                             "\" which are not close enough to each other" + "<br>";
180     } else {
181         var elementName;
182         if (iframeId)
183             elementName = iframeId + '.' + elementId;
184         else
185             elementName = elementId;
186
187         computedValue = getPropertyValue(property, elementId, iframeId);
188
189         if (comparePropertyValue(property, computedValue, expectedValue, tolerance))
190             result += "PASS - \"" + property + "\" property for \"" + elementName + "\" element at " + time + 
191                             "s saw something close to: " + expectedValue + "<br>";
192         else
193             result += "FAIL - \"" + property + "\" property for \"" + elementName + "\" element at " + time + 
194                             "s expected: " + expectedValue + " but saw: " + computedValue + "<br>";
195     }
196 }
197
198
199 function getPropertyValue(property, elementId, iframeId)
200 {
201     var computedValue;
202     var element;
203     if (iframeId)
204         element = document.getElementById(iframeId).contentDocument.getElementById(elementId);
205     else
206         element = document.getElementById(elementId);
207
208     if (property == "lineHeight")
209         computedValue = parseInt(window.getComputedStyle(element).lineHeight);
210     else if (property == "backgroundImage"
211                || property == "borderImageSource"
212                || property == "listStyleImage"
213                || property == "webkitMaskImage"
214                || property == "webkitMaskBoxImage"
215                || property == "webkitFilter"
216                || !property.indexOf("webkitTransform")) {
217         computedValue = window.getComputedStyle(element)[property.split(".")[0]];
218     } else {
219         var computedStyle = window.getComputedStyle(element).getPropertyCSSValue(property);
220         computedValue = computedStyle.getFloatValue(CSSPrimitiveValue.CSS_NUMBER);
221     }
222
223     return computedValue;
224 }
225
226 function comparePropertyValue(property, computedValue, expectedValue, tolerance)
227 {
228     var result = true;
229
230     if (!property.indexOf("webkitTransform")) {
231         if (typeof expectedValue == "string")
232             result = (computedValue == expectedValue);
233         else if (typeof expectedValue == "number") {
234             var m = matrixStringToArray(computedValue);
235             result = isCloseEnough(parseFloat(m[parseInt(property.substring(16))]), expectedValue, tolerance);
236         } else {
237             var m = matrixStringToArray(computedValue);
238             for (i = 0; i < expectedValue.length; ++i) {
239                 result = isCloseEnough(parseFloat(m[i]), expectedValue[i], tolerance);
240                 if (!result)
241                     break;
242             }
243         }
244     } else if (property == "webkitFilter") {
245         var filterParameters = getFilterParameters(computedValue);
246         var filter2Parameters = getFilterParameters(expectedValue);
247         result = filterParametersMatch(filterParameters, filter2Parameters, tolerance);
248     } else if (property == "backgroundImage"
249                || property == "borderImageSource"
250                || property == "listStyleImage"
251                || property == "webkitMaskImage"
252                || property == "webkitMaskBoxImage") {
253         var computedCrossFade = parseCrossFade(computedValue);
254
255         if (!computedCrossFade) {
256             result = false;
257         } else {
258             if (typeof expectedValue == "string") {
259                 var computedCrossFade2 = parseCrossFade(expectedValue);
260                 result = isCloseEnough(computedCrossFade.percent, computedCrossFade2.percent, tolerance) && computedCrossFade.from == computedCrossFade2.from && computedCrossFade.to == computedCrossFade2.to;
261             } else {
262                 result = isCloseEnough(computedCrossFade.percent, expectedValue, tolerance)
263             }
264         }
265     } else {
266         result = isCloseEnough(computedValue, expectedValue, tolerance);
267     }
268     return result;
269 }
270
271 function endTest()
272 {
273     document.getElementById('result').innerHTML = result;
274
275     if (window.layoutTestController)
276         layoutTestController.notifyDone();
277 }
278
279 function checkExpectedValueCallback(expected, index)
280 {
281     return function() { checkExpectedValue(expected, index); };
282 }
283
284 var testStarted = false;
285 function startTest(expected, callback)
286 {
287     if (testStarted) return;
288     testStarted = true;
289
290     if (callback)
291         callback();
292
293     var maxTime = 0;
294
295     for (var i = 0; i < expected.length; ++i) {
296         var animationName = expected[i][0];
297         var time = expected[i][1];
298
299         // We can only use the animation fast-forward mechanism if there's an animation name
300         // and DRT implements pauseAnimationAtTimeOnElementWithId()
301         if (animationName && hasPauseAnimationAPI)
302             checkExpectedValue(expected, i);
303         else {
304             if (time > maxTime)
305                 maxTime = time;
306
307             window.setTimeout(checkExpectedValueCallback(expected, i), time * 1000);
308         }
309     }
310
311     if (maxTime > 0)
312         window.setTimeout(endTest, maxTime * 1000 + 50);
313     else
314         endTest();
315 }
316
317 var result = "";
318 var hasPauseAnimationAPI;
319
320 function runAnimationTest(expected, callback, event, disablePauseAnimationAPI, doPixelTest)
321 {
322     hasPauseAnimationAPI = ('layoutTestController' in window) && ('pauseAnimationAtTimeOnElementWithId' in layoutTestController);
323     if (disablePauseAnimationAPI)
324         hasPauseAnimationAPI = false;
325
326     if (window.layoutTestController) {
327         if (!doPixelTest)
328             layoutTestController.dumpAsText();
329         layoutTestController.waitUntilDone();
330     }
331     
332     if (!expected)
333         throw("Expected results are missing!");
334     
335     var target = document;
336     if (event == undefined)
337         waitForAnimationToStart(target, function() { startTest(expected, callback); });
338     else if (event == "load")
339         window.addEventListener(event, function() {
340             startTest(expected, callback);
341         }, false);
342 }
343
344 function waitForAnimationToStart(element, callback)
345 {
346     element.addEventListener('webkitAnimationStart', function() {
347         window.setTimeout(callback, 0); // delay to give hardware animations a chance to start
348     }, false);
349 }