[Web Animations] Stop using internals.pauseTransitionAtTimeOnElement() in favor of...
[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 var usePauseAPI = true;
25 var dontUsePauseAPI = false;
26
27 var shouldBeTransitioning = true;
28 var 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 extractPathValues(path)
73 {
74     var components = path.split(' ');
75     var result = [];
76     for (component of components) {
77         var compMatch;
78         if (compMatch = component.match(/[0-9.-]+/)) {
79             result.push(parseFloat(component))
80         }
81     }
82     return result;
83 }
84
85 function parseClipPath(s)
86 {
87     var pathMatch;
88     if (pathMatch = s.match(/path\(((evenodd|nonzero), ?)?\'(.+)\'\)/))
89         return extractPathValues(pathMatch[pathMatch.length - 1]);
90
91     if (pathMatch = s.match(/path\(((evenodd|nonzero), ?)?\"(.+)\"\)/))
92         return extractPathValues(pathMatch[pathMatch.length - 1]);
93
94     // FIXME: This only matches a subset of the shape syntax, and the polygon expects 4 points.
95     var patterns = [
96         /inset\(([\d.]+)\w+ ([\d.]+)\w+\)/,
97         /circle\(([\d.]+)\w+ at ([\d.]+)\w+ ([\d.]+)\w+\)/,
98         /ellipse\(([\d.]+)\w+ ([\d.]+)\w+ at ([\d.]+)\w+ ([\d.]+)\w+\)/,
99         /polygon\(([\d.]+)\w* ([\d.]+)\w*\, ([\d.]+)\w* ([\d.]+)\w*\, ([\d.]+)\w* ([\d.]+)\w*\, ([\d.]+)\w* ([\d.]+)\w*\)/,
100     ];
101     
102     for (pattern of patterns) {
103         var matchResult;
104         if (matchResult = s.match(pattern)) {
105             var result = [];
106             for (var i = 1; i < matchResult.length; ++i)
107                 result.push(parseFloat(matchResult[i]));
108             return result;
109         }
110     }
111
112     window.console.log('failed to match ' + s);
113     return null;
114 }
115
116 function hasFloatValue(value)
117 {
118     switch (value.primitiveType) {
119     case CSSPrimitiveValue.CSS_FR:
120     case CSSPrimitiveValue.CSS_NUMBER:
121     case CSSPrimitiveValue.CSS_PARSER_INTEGER:
122     case CSSPrimitiveValue.CSS_PERCENTAGE:
123     case CSSPrimitiveValue.CSS_EMS:
124     case CSSPrimitiveValue.CSS_EXS:
125     case CSSPrimitiveValue.CSS_CHS:
126     case CSSPrimitiveValue.CSS_REMS:
127     case CSSPrimitiveValue.CSS_PX:
128     case CSSPrimitiveValue.CSS_CM:
129     case CSSPrimitiveValue.CSS_MM:
130     case CSSPrimitiveValue.CSS_IN:
131     case CSSPrimitiveValue.CSS_PT:
132     case CSSPrimitiveValue.CSS_PC:
133     case CSSPrimitiveValue.CSS_DEG:
134     case CSSPrimitiveValue.CSS_RAD:
135     case CSSPrimitiveValue.CSS_GRAD:
136     case CSSPrimitiveValue.CSS_TURN:
137     case CSSPrimitiveValue.CSS_MS:
138     case CSSPrimitiveValue.CSS_S:
139     case CSSPrimitiveValue.CSS_HZ:
140     case CSSPrimitiveValue.CSS_KHZ:
141     case CSSPrimitiveValue.CSS_DIMENSION:
142     case CSSPrimitiveValue.CSS_VW:
143     case CSSPrimitiveValue.CSS_VH:
144     case CSSPrimitiveValue.CSS_VMIN:
145     case CSSPrimitiveValue.CSS_VMAX:
146     case CSSPrimitiveValue.CSS_DPPX:
147     case CSSPrimitiveValue.CSS_DPI:
148     case CSSPrimitiveValue.CSS_DPCM:
149         return true;
150     }
151     return false;
152 }
153
154 function getNumericValue(cssValue)
155 {
156     if (hasFloatValue(cssValue.primitiveType))
157         return cssValue.getFloatValue(cssValue.primitiveType);
158
159     return -1;
160 }
161
162 function isCalcPrimitiveValue(value)
163 {
164     switch (value.primitiveType) {
165     case 113: // CSSPrimitiveValue.CSS_CALC:
166     case 114: // CSSPrimitiveValue.CSS_CALC_PERCENTAGE_WITH_NUMBER:
167     case 115: // CSSPrimitiveValue.CSS_CALC_PERCENTAGE_WITH_LENGTH:
168     return true;
169     }
170     return false;
171 }
172
173 function extractNumbersFromCalcExpression(value, values)
174 {
175     var calcRegexp = /^calc\((.+)\)$/;
176     var result = calcRegexp.exec(value.cssText);
177     var numberMatch = /([^\.\-0-9]*)(-?[\.0-9]+)/;
178     var remainder = result[1];
179     var match;
180     while ((match = numberMatch.exec(remainder)) !== null) {
181         var skipLength = match[1].length + match[2].length;
182         values.push(parseFloat(match[2]))
183         remainder = remainder.substr(skipLength + 1);
184     }
185 }
186
187 function checkExpectedValue(expected, index)
188 {
189     var time = expected[index][0];
190     var elementId = expected[index][1];
191     var property = expected[index][2];
192     var expectedValue = expected[index][3];
193     var tolerance = expected[index][4];
194     var postCompletionCallback = expected[index][5];
195
196     var computedValue;
197     var pass = false;
198     var transformRegExp = /^-webkit-transform(\.\d+)?$/;
199     if (transformRegExp.test(property)) {
200         computedValue = window.getComputedStyle(document.getElementById(elementId)).webkitTransform;
201         if (typeof expectedValue == "string")
202             pass = (computedValue == expectedValue);
203         else if (typeof expectedValue == "number") {
204             var m = computedValue.split("(");
205             var m = m[1].split(",");
206             pass = isCloseEnough(parseFloat(m[parseInt(property.substring(18))]), expectedValue, tolerance);
207         } else {
208             var m = computedValue.split("(");
209             var m = m[1].split(",");
210             for (i = 0; i < expectedValue.length; ++i) {
211                 pass = isCloseEnough(parseFloat(m[i]), expectedValue[i], tolerance);
212                 if (!pass)
213                     break;
214             }
215         }
216     } else if (property == "fill" || property == "stroke") {
217         computedValue = window.getComputedStyle(document.getElementById(elementId)).getPropertyCSSValue(property).rgbColor;
218         if (compareRGB([computedValue.red.cssText, computedValue.green.cssText, computedValue.blue.cssText], expectedValue, tolerance))
219             pass = true;
220         else {
221             // We failed. Make sure computed value is something we can read in the error message
222             computedValue = window.getComputedStyle(document.getElementById(elementId)).getPropertyCSSValue(property).cssText;
223         }
224     } else if (property == "stop-color" || property == "flood-color" || property == "lighting-color") {
225         computedValue = window.getComputedStyle(document.getElementById(elementId)).getPropertyCSSValue(property);
226         // The computedValue cssText is rgb(num, num, num)
227         var components = computedValue.cssText.split("(")[1].split(")")[0].split(",");
228         if (compareRGB(components, expectedValue, tolerance))
229             pass = true;
230         else {
231             // We failed. Make sure computed value is something we can read in the error message
232             computedValue = computedValue.cssText;
233         }
234     } else if (property == "lineHeight") {
235         computedValue = parseInt(window.getComputedStyle(document.getElementById(elementId)).lineHeight);
236         pass = isCloseEnough(computedValue, expectedValue, tolerance);
237     } else if (property == "background-image"
238                || property == "border-image-source"
239                || property == "border-image"
240                || property == "list-style-image"
241                || property == "-webkit-mask-image"
242                || property == "-webkit-mask-box-image") {
243         if (property == "border-image" || property == "-webkit-mask-image" || property == "-webkit-mask-box-image")
244             property += "-source";
245         
246         computedValue = window.getComputedStyle(document.getElementById(elementId)).getPropertyCSSValue(property).cssText;
247         computedCrossFade = parseCrossFade(computedValue);
248
249         if (!computedCrossFade) {
250             pass = false;
251         } else {
252             pass = isCloseEnough(computedCrossFade.percent, expectedValue, tolerance);
253         }
254     } else if (property == "-webkit-clip-path" || property == "-webkit-shape-outside") {
255         computedValue = window.getComputedStyle(document.getElementById(elementId)).getPropertyCSSValue(property).cssText;
256
257         var expectedValues = parseClipPath(expectedValue);
258         var values = parseClipPath(computedValue);
259         
260         pass = false;
261         if (values && values.length == expectedValues.length) {
262             pass = true
263             for (var i = 0; i < values.length; ++i)
264                 pass &= isCloseEnough(values[i], expectedValues[i], tolerance);
265         }
266     } else {
267         var computedStyle = window.getComputedStyle(document.getElementById(elementId)).getPropertyCSSValue(property);
268         if (computedStyle.cssValueType == CSSValue.CSS_VALUE_LIST) {
269             var values = [];
270             for (var i = 0; i < computedStyle.length; ++i) {
271                 var styleValue = computedStyle[i];
272                 switch (styleValue.cssValueType) {
273                   case CSSValue.CSS_PRIMITIVE_VALUE:
274                     if (hasFloatValue(styleValue))
275                         values.push(styleValue.getFloatValue(CSSPrimitiveValue.CSS_NUMBER));
276                     else if (isCalcPrimitiveValue(styleValue))
277                         extractNumbersFromCalcExpression(styleValue, values);
278                     break;
279                   case CSSValue.CSS_CUSTOM:
280                     // arbitrarily pick shadow-x and shadow-y
281                     if (isShadow) {
282                       var shadowXY = getShadowXY(styleValue);
283                       values.push(shadowXY[0]);
284                       values.push(shadowXY[1]);
285                     } else
286                       values.push(styleValue.cssText);
287                     break;
288                 }
289             }
290             computedValue = values.join(',');
291             pass = values.length > 0;
292             for (var i = 0; i < values.length; ++i)
293                 pass &= isCloseEnough(values[i], expectedValue[i], tolerance);
294         } else if (computedStyle.cssValueType == CSSValue.CSS_PRIMITIVE_VALUE) {
295             switch (computedStyle.primitiveType) {
296                 case CSSPrimitiveValue.CSS_STRING:
297                 case CSSPrimitiveValue.CSS_IDENT:
298                     computedValue = computedStyle.getStringValue();
299                     pass = computedValue == expectedValue;
300                     break;
301                 case CSSPrimitiveValue.CSS_RGBCOLOR:
302                     var rgbColor = computedStyle.getRGBColorValue();
303                     computedValue = [rgbColor.red.getFloatValue(CSSPrimitiveValue.CSS_NUMBER),
304                                      rgbColor.green.getFloatValue(CSSPrimitiveValue.CSS_NUMBER),
305                                      rgbColor.blue.getFloatValue(CSSPrimitiveValue.CSS_NUMBER)]; // alpha is not exposed to JS
306                     pass = true;
307                     for (var i = 0; i < 3; ++i)
308                         pass &= isCloseEnough(computedValue[i], expectedValue[i], tolerance);
309                     break;
310                 case CSSPrimitiveValue.CSS_RECT:
311                     computedValue = computedStyle.getRectValue();
312                     computedValue = [computedValue.top.getFloatValue(CSSPrimitiveValue.CSS_NUMBER),
313                                      computedValue.right.getFloatValue(CSSPrimitiveValue.CSS_NUMBER),
314                                      computedValue.bottom.getFloatValue(CSSPrimitiveValue.CSS_NUMBER),
315                                      computedValue.left.getFloatValue(CSSPrimitiveValue.CSS_NUMBER)];
316                      pass = true;
317                      for (var i = 0; i < 4; ++i)
318                          pass &= isCloseEnough(computedValue[i], expectedValue[i], tolerance);
319                     break;
320                 case CSSPrimitiveValue.CSS_PERCENTAGE:
321                     computedValue = parseFloat(computedStyle.cssText);
322                     pass = isCloseEnough(computedValue, expectedValue, tolerance);
323                     break;
324                 default:
325                     computedValue = computedStyle.getFloatValue(CSSPrimitiveValue.CSS_NUMBER);
326                     pass = isCloseEnough(computedValue, expectedValue, tolerance);
327             }
328         }
329     }
330
331     if (pass)
332         result += "PASS - \"" + property + "\" property for \"" + elementId + "\" element at " + time + "s saw something close to: " + expectedValue + "<br>";
333     else
334         result += "FAIL - \"" + property + "\" property for \"" + elementId + "\" element at " + time + "s expected: " + expectedValue + " but saw: " + computedValue + "<br>";
335
336     if (postCompletionCallback)
337       result += postCompletionCallback();
338 }
339
340 function endTest()
341 {
342     document.getElementById('result').innerHTML = result;
343
344     if (window.testRunner)
345         testRunner.notifyDone();
346 }
347
348 function checkExpectedValueCallback(expected, index)
349 {
350     return function() { checkExpectedValue(expected, index); };
351 }
352
353 const prefix = "-webkit-";
354 const propertiesRequiringPrefix = ["-webkit-text-stroke-color", "-webkit-text-fill-color"];
355
356 function pauseTransitionAtTimeOnElement(transitionProperty, time, element)
357 {
358     // If we haven't opted into CSS Animations and CSS Transitions as Web Animations, use the internal API.
359     if ('internals' in window && !internals.settings.cssAnimationsAndCSSTransitionsBackedByWebAnimationsEnabled())
360         return internals.pauseTransitionAtTimeOnElement(transitionProperty, time, element);
361
362     if (transitionProperty.startsWith(prefix) && !propertiesRequiringPrefix.includes(transitionProperty))
363         transitionProperty = transitionProperty.substr(prefix.length);
364
365     // Otherwise, use the Web Animations API.
366     const animations = element.getAnimations();
367     for (let animation of animations) {
368         if (animation instanceof CSSTransition && animation.transitionProperty == transitionProperty) {
369             animation.currentTime = time * 1000;
370             animation.pause();
371             return true;
372         }
373     }
374     console.log(`A transition for property ${transitionProperty} could not be found`);
375     return false;
376 }
377
378 function runTest(expected, usePauseAPI)
379 {
380     var maxTime = 0;
381     for (var i = 0; i < expected.length; ++i) {
382         var time = expected[i][0];
383         var elementId = expected[i][1];
384         var property = expected[i][2];
385         if (!property.indexOf("-webkit-transform."))
386             property = "-webkit-transform";
387
388         var tryToPauseTransition = expected[i][6];
389         if (tryToPauseTransition === undefined)
390           tryToPauseTransition = shouldBeTransitioning;
391
392         if (hasPauseTransitionAPI && usePauseAPI) {
393             if (tryToPauseTransition) {
394               var element = document.getElementById(elementId);
395               if (!pauseTransitionAtTimeOnElement(property, time, element))
396                 window.console.log("Failed to pause '" + property + "' transition on element '" + elementId + "'");
397             }
398             checkExpectedValue(expected, i);
399         } else {
400             if (time > maxTime)
401                 maxTime = time;
402
403             window.setTimeout(checkExpectedValueCallback(expected, i), time * 1000);
404         }
405     }
406
407     if (maxTime > 0)
408         window.setTimeout(endTest, maxTime * 1000 + 50);
409     else
410         endTest();
411 }
412
413 function waitForAnimationStart(callback, delay)
414 {
415     var delayTimeout = delay ? 1000 * delay + 10 : 0;
416     // Why the two setTimeouts? Well, for hardware animations we need to ensure that the hardware animation
417     // has started before we try to pause it, and timers fire before animations get committed in the runloop.
418     window.setTimeout(function() {
419         window.setTimeout(function() {
420             callback();
421         }, 0);
422     }, delayTimeout);
423 }
424
425 function startTest(expected, usePauseAPI, callback)
426 {
427     if (callback)
428         callback();
429
430     waitForAnimationStart(function() {
431         runTest(expected, usePauseAPI);
432     });
433 }
434
435 var result = "";
436 var hasPauseTransitionAPI = true;
437
438 function runTransitionTest(expected, callback, usePauseAPI, doPixelTest)
439 {
440     if (window.testRunner) {
441         if (!doPixelTest)
442             testRunner.dumpAsText();
443         testRunner.waitUntilDone();
444     }
445     
446     if (!expected)
447         throw("Expected results are missing!");
448     
449     window.addEventListener("load", function() { startTest(expected, usePauseAPI, callback); }, false);
450 }