Enable animations of CSS images using -webkit-cross-fade
[WebKit-https.git] / LayoutTests / transitions / resources / transition-test-helpers.js
1 /* This is the helper function to run transition 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     
11     Each sub-array must contain these items in this order:
12     - the time in seconds at which to snapshot the CSS property
13     - the id of the element on which to get the CSS property value
14     - the name of the CSS property to get [1]
15     - the expected value for the CSS property
16     - the tolerance to use when comparing the effective CSS property value with its expected value
17     
18     [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,
19     or a string which will be compared directly (useful if the expected value is "none")
20     If the CSS property name is "-webkit-transform.N", expected value must be a number corresponding to the Nth element of the matrix
21
22 */
23
24 const usePauseAPI = true;
25 const dontUsePauseAPI = false;
26
27 const shouldBeTransitioning = true;
28 const shouldNotBeTransitioning = false;
29
30 function roundNumber(num, decimalPlaces)
31 {
32   return Math.round(num * Math.pow(10, decimalPlaces)) / Math.pow(10, decimalPlaces);
33 }
34
35 function isCloseEnough(actual, desired, tolerance)
36 {
37     var diff = Math.abs(actual - desired);
38     return diff <= tolerance;
39 }
40
41 function isShadow(property)
42 {
43   return (property == '-webkit-box-shadow' || property == 'text-shadow');
44 }
45
46 function getShadowXY(cssValue)
47 {
48     var text = cssValue.cssText;
49     // Shadow cssText looks like "rgb(0, 0, 255) 0px -3px 10px 0px"
50     var shadowPositionRegExp = /\)\s*(-?\d+)px\s*(-?\d+)px/;
51     var result = shadowPositionRegExp.exec(text);
52     return [parseInt(result[1]), parseInt(result[2])];
53 }
54
55 function compareRGB(rgb, expected, tolerance)
56 {
57     return (isCloseEnough(parseInt(rgb[0]), expected[0], tolerance) &&
58             isCloseEnough(parseInt(rgb[1]), expected[1], tolerance) &&
59             isCloseEnough(parseInt(rgb[2]), expected[2], tolerance));
60 }
61
62 function parseCrossFade(s)
63 {
64     var matches = s.match("-webkit-cross-fade\\((.*)\\s*,\\s*(.*)\\s*,\\s*(.*)\\)");
65
66     if (!matches)
67         return null;
68
69     return {"from": matches[1], "to": matches[2], "percent": parseFloat(matches[3])}
70 }
71
72 function checkExpectedValue(expected, index)
73 {
74     var time = expected[index][0];
75     var elementId = expected[index][1];
76     var property = expected[index][2];
77     var expectedValue = expected[index][3];
78     var tolerance = expected[index][4];
79     var postCompletionCallback = expected[index][5];
80
81     var computedValue;
82     var pass = false;
83     var transformRegExp = /^-webkit-transform(\.\d+)?$/;
84     if (transformRegExp.test(property)) {
85         computedValue = window.getComputedStyle(document.getElementById(elementId)).webkitTransform;
86         if (typeof expectedValue == "string")
87             pass = (computedValue == expectedValue);
88         else if (typeof expectedValue == "number") {
89             var m = computedValue.split("(");
90             var m = m[1].split(",");
91             pass = isCloseEnough(parseFloat(m[parseInt(property.substring(18))]), expectedValue, tolerance);
92         } else {
93             var m = computedValue.split("(");
94             var m = m[1].split(",");
95             for (i = 0; i < expectedValue.length; ++i) {
96                 pass = isCloseEnough(parseFloat(m[i]), expectedValue[i], tolerance);
97                 if (!pass)
98                     break;
99             }
100         }
101     } else if (property == "fill" || property == "stroke") {
102         computedValue = window.getComputedStyle(document.getElementById(elementId)).getPropertyCSSValue(property).rgbColor;
103         if (compareRGB([computedValue.red.cssText, computedValue.green.cssText, computedValue.blue.cssText], expectedValue, tolerance))
104             pass = true;
105         else {
106             // We failed. Make sure computed value is something we can read in the error message
107             computedValue = window.getComputedStyle(document.getElementById(elementId)).getPropertyCSSValue(property).cssText;
108         }
109     } else if (property == "stop-color" || property == "flood-color" || property == "lighting-color") {
110         computedValue = window.getComputedStyle(document.getElementById(elementId)).getPropertyCSSValue(property);
111         // The computedValue cssText is rgb(num, num, num)
112         var components = computedValue.cssText.split("(")[1].split(")")[0].split(",");
113         if (compareRGB(components, expectedValue, tolerance))
114             pass = true;
115         else {
116             // We failed. Make sure computed value is something we can read in the error message
117             computedValue = computedValue.cssText;
118         }
119     } else if (property == "lineHeight") {
120         computedValue = parseInt(window.getComputedStyle(document.getElementById(elementId)).lineHeight);
121         pass = isCloseEnough(computedValue, expectedValue, tolerance);
122     } else if (property == "background-image"
123                || property == "border-image-source"
124                || property == "border-image"
125                || property == "list-style-image"
126                || property == "-webkit-mask-image"
127                || property == "-webkit-mask-box-image") {
128         if (property == "border-image" || property == "-webkit-mask-image" || property == "-webkit-mask-box-image")
129             property += "-source";
130         
131         computedValue = window.getComputedStyle(document.getElementById(elementId)).getPropertyCSSValue(property).cssText;
132         computedCrossFade = parseCrossFade(computedValue);
133
134         if (!computedCrossFade) {
135             pass = false;
136         } else {
137             pass = isCloseEnough(computedCrossFade.percent, expectedValue, tolerance);
138         }
139     } else {
140         var computedStyle = window.getComputedStyle(document.getElementById(elementId)).getPropertyCSSValue(property);
141         if (computedStyle.cssValueType == CSSValue.CSS_VALUE_LIST) {
142             var values = [];
143             for (var i = 0; i < computedStyle.length; ++i) {
144                 switch (computedStyle[i].cssValueType) {
145                   case CSSValue.CSS_PRIMITIVE_VALUE:
146                     values.push(computedStyle[i].getFloatValue(CSSPrimitiveValue.CSS_NUMBER));
147                     break;
148                   case CSSValue.CSS_CUSTOM:
149                     // arbitrarily pick shadow-x and shadow-y
150                     if (isShadow) {
151                       var shadowXY = getShadowXY(computedStyle[i]);
152                       values.push(shadowXY[0]);
153                       values.push(shadowXY[1]);
154                     } else
155                       values.push(computedStyle[i].cssText);
156                     break;
157                 }
158             }
159             computedValue = values.join(',');
160             pass = true;
161             for (var i = 0; i < values.length; ++i)
162                 pass &= isCloseEnough(values[i], expectedValue[i], tolerance);
163         } else if (computedStyle.cssValueType == CSSValue.CSS_PRIMITIVE_VALUE) {
164             switch (computedStyle.primitiveType) {
165                 case CSSPrimitiveValue.CSS_STRING:
166                     computedValue = computedStyle.getStringValue();
167                     pass = computedValue == expectedValue;
168                     break;
169                 case CSSPrimitiveValue.CSS_RGBCOLOR:
170                     var rgbColor = computedStyle.getRGBColorValue();
171                     computedValue = [rgbColor.red.getFloatValue(CSSPrimitiveValue.CSS_NUMBER),
172                                      rgbColor.green.getFloatValue(CSSPrimitiveValue.CSS_NUMBER),
173                                      rgbColor.blue.getFloatValue(CSSPrimitiveValue.CSS_NUMBER)]; // alpha is not exposed to JS
174                     pass = true;
175                     for (var i = 0; i < 3; ++i)
176                         pass &= isCloseEnough(computedValue[i], expectedValue[i], tolerance);
177                     break;
178                 case CSSPrimitiveValue.CSS_RECT:
179                     computedValue = computedStyle.getRectValue();
180                     computedValue = [computedValue.top.getFloatValue(CSSPrimitiveValue.CSS_NUMBER),
181                                      computedValue.right.getFloatValue(CSSPrimitiveValue.CSS_NUMBER),
182                                      computedValue.bottom.getFloatValue(CSSPrimitiveValue.CSS_NUMBER),
183                                      computedValue.left.getFloatValue(CSSPrimitiveValue.CSS_NUMBER)];
184                      pass = true;
185                      for (var i = 0; i < 4; ++i)
186                          pass &= isCloseEnough(computedValue[i], expectedValue[i], tolerance);
187                     break;
188                 case CSSPrimitiveValue.CSS_PERCENTAGE:
189                     computedValue = parseFloat(computedStyle.cssText);
190                     pass = isCloseEnough(computedValue, expectedValue, tolerance);
191                     break;
192                 default:
193                     computedValue = computedStyle.getFloatValue(CSSPrimitiveValue.CSS_NUMBER);
194                     pass = isCloseEnough(computedValue, expectedValue, tolerance);
195             }
196         }
197     }
198
199     if (pass)
200         result += "PASS - \"" + property + "\" property for \"" + elementId + "\" element at " + time + "s saw something close to: " + expectedValue + "<br>";
201     else
202         result += "FAIL - \"" + property + "\" property for \"" + elementId + "\" element at " + time + "s expected: " + expectedValue + " but saw: " + computedValue + "<br>";
203
204     if (postCompletionCallback)
205       result += postCompletionCallback();
206 }
207
208 function endTest()
209 {
210     document.getElementById('result').innerHTML = result;
211
212     if (window.layoutTestController)
213         layoutTestController.notifyDone();
214 }
215
216 function checkExpectedValueCallback(expected, index)
217 {
218     return function() { checkExpectedValue(expected, index); };
219 }
220
221 function runTest(expected, usePauseAPI)
222 {
223     var maxTime = 0;
224     for (var i = 0; i < expected.length; ++i) {
225         var time = expected[i][0];
226         var elementId = expected[i][1];
227         var property = expected[i][2];
228         if (!property.indexOf("-webkit-transform."))
229             property = "-webkit-transform";
230
231         var tryToPauseTransition = expected[i][6];
232         if (tryToPauseTransition === undefined)
233           tryToPauseTransition = shouldBeTransitioning;
234
235         // We can only use the transition fast-forward mechanism if DRT implements pauseTransitionAtTimeOnElementWithId()
236         if (hasPauseTransitionAPI && usePauseAPI) {
237             if (tryToPauseTransition) {
238               if (!layoutTestController.pauseTransitionAtTimeOnElementWithId(property, time, elementId))
239                 window.console.log("Failed to pause '" + property + "' transition on element '" + elementId + "'");
240             }
241             checkExpectedValue(expected, i);
242         } else {
243             if (time > maxTime)
244                 maxTime = time;
245
246             window.setTimeout(checkExpectedValueCallback(expected, i), time * 1000);
247         }
248     }
249
250     if (maxTime > 0)
251         window.setTimeout(endTest, maxTime * 1000 + 50);
252     else
253         endTest();
254 }
255
256 function waitForAnimationStart(callback, delay)
257 {
258     var delayTimeout = delay ? 1000 * delay + 10 : 0;
259     // Why the two setTimeouts? Well, for hardware animations we need to ensure that the hardware animation
260     // has started before we try to pause it, and timers fire before animations get committed in the runloop.
261     window.setTimeout(function() {
262         window.setTimeout(function() {
263             callback();
264         }, 0);
265     }, delayTimeout);
266 }
267
268 function startTest(expected, usePauseAPI, callback)
269 {
270     if (callback)
271         callback();
272
273     waitForAnimationStart(function() {
274         runTest(expected, usePauseAPI);
275     });
276 }
277
278 var result = "";
279 var hasPauseTransitionAPI;
280
281 function runTransitionTest(expected, callback, usePauseAPI, doPixelTest)
282 {
283     hasPauseTransitionAPI = ('layoutTestController' in window) && ('pauseTransitionAtTimeOnElementWithId' in layoutTestController);
284     
285     if (window.layoutTestController) {
286         if (!doPixelTest)
287             layoutTestController.dumpAsText();
288         layoutTestController.waitUntilDone();
289     }
290     
291     if (!expected)
292         throw("Expected results are missing!");
293     
294     window.addEventListener("load", function() { startTest(expected, usePauseAPI, callback); }, false);
295 }