85c98288474b31b31b40c95194dd68c4fc9952d8
[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() function.
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 function compareFontVariationSettings(computedValue, expectedValue, tolerance)
262 {
263     if (!computedValue)
264         return false;
265     if (computedValue == "normal" || expectedValue == "normal")
266         return computedValue == expectedValue;
267     var computed = computedValue.split(", ");
268     var expected = expectedValue.split(", ");
269     if (computed.length != expected.length)
270         return false;
271     for (var i = 0; i < computed.length; ++i) {
272         var computedPieces = computed[i].split(" ");
273         var expectedPieces = expected[i].split(" ");
274         if (computedPieces.length != 2 || expectedPieces.length != 2)
275             return false;
276         if (computedPieces[0] != expectedPieces[0])
277             return false;
278         var computedNumber = Number.parseFloat(computedPieces[1]);
279         var expectedNumber = Number.parseFloat(expectedPieces[1]);
280         if (Math.abs(computedNumber - expectedNumber) > tolerance)
281             return false;
282     }
283     return true;
284 }
285
286 function compareFontStyle(computedValue, expectedValue, tolerance)
287 {
288         var computed = computedValue.split(" ");
289         var expected = expectedValue.split(" ");
290         var computedAngle = computed[1].split("deg");
291         var expectedAngle = expected[1].split("deg");
292         return computed[0] == expected[0] && Math.abs(computedAngle[0] - expectedAngle[0]) <= tolerance;
293 }
294
295 // Called by CSS Image function filter() as well as filter property.
296 function compareFilterFunctions(computedValue, expectedValue, tolerance)
297 {
298     var actual = typeof computedValue === "string" ? parseFilterFunctionList(computedValue) : computedValue;
299     var expected = typeof expectedValue === "string" ? parseFilterFunctionList(expectedValue) : expectedValue;
300     if (!actual || !Array.isArray(actual)         // Check that: actual is an array,
301         || !expected || !Array.isArray(expected)  // expected is an array,
302         || !actual.length                         // actual array has entries,
303         || actual.length != expected.length       // actual and expected have same length
304         || actual.length % 2 == 1)                // and image function names are the same.
305         return false;
306
307     for (var i = 0; i < actual.length; i += 2) {
308         if (actual[i] != expected[i]) {
309             console.error("Filter functions do not match.");
310             return false;
311         }
312         if (!isCloseEnough(actual[i+1], expected[i+1], tolerance)) {
313             console.error("Filter function values do not match.");
314             return false;
315         }
316     }
317     return true;
318 }
319
320 function basicShapeParametersMatch(paramList1, paramList2, tolerance)
321 {
322     if (paramList1.shape != paramList2.shape
323         || paramList1.params.length != paramList2.params.length)
324         return false;
325     var i = 0;
326     for (; i < paramList1.params.length; ++i) {
327         var param1 = paramList1.params[i], 
328             param2 = paramList2.params[i];
329         if (param1 === param2)
330             continue;
331         var match = isCloseEnough(param1, param2, tolerance);
332         if (!match)
333             return false;
334     }
335     return true;
336 }
337
338 function checkExpectedValue(expected, index)
339 {
340     var animationName = expected[index][0];
341     var time = expected[index][1];
342     var elementId = expected[index][2];
343     var property = expected[index][3];
344     var expectedValue = expected[index][4];
345     var tolerance = expected[index][5];
346
347     // Check for a pair of element Ids
348     var compareElements = false;
349     var element2Static = false;
350     var elementId2;
351     if (typeof elementId != "string") {
352         if (elementId.length != 2)
353             return;
354             
355         elementId2 = elementId[1];
356         elementId = elementId[0];
357
358         if (elementId2.indexOf("static:") == 0) {
359             elementId2 = elementId2.replace("static:", "");
360             element2Static = true;
361         }
362
363         compareElements = true;
364     }
365     
366     // Check for a dot separated string
367     var iframeId;
368     if (!compareElements) {
369         var array = elementId.split('.');
370         if (array.length == 2) {
371             iframeId = array[0];
372             elementId = array[1];
373         }
374     }
375
376     if (animationName && hasPauseAnimationAPI && !pauseAnimationAtTimeOnElement(animationName, time, document.getElementById(elementId))) {
377         result += "FAIL - animation \"" + animationName + "\" is not running" + "<br>";
378         return;
379     }
380     
381     if (compareElements && !element2Static && animationName && hasPauseAnimationAPI && !pauseAnimationAtTimeOnElement(animationName, time, document.getElementById(elementId2))) {
382         result += "FAIL - animation \"" + animationName + "\" is not running" + "<br>";
383         return;
384     }
385     
386     var computedValue, computedValue2;
387     if (compareElements) {
388         computedValue = getPropertyValue(property, elementId, iframeId);
389         computedValue2 = getPropertyValue(property, elementId2, iframeId);
390
391         if (comparePropertyValue(property, computedValue, computedValue2, tolerance))
392             result += "PASS - \"" + property + "\" property for \"" + elementId + "\" and \"" + elementId2 + 
393                             "\" elements at " + time + "s are close enough to each other" + "<br>";
394         else
395             result += "FAIL - \"" + property + "\" property for \"" + elementId + "\" and \"" + elementId2 + 
396                             "\" elements at " + time + "s saw: \"" + computedValue + "\" and \"" + computedValue2 + 
397                                             "\" which are not close enough to each other" + "<br>";
398     } else {
399         var elementName;
400         if (iframeId)
401             elementName = iframeId + '.' + elementId;
402         else
403             elementName = elementId;
404
405         computedValue = getPropertyValue(property, elementId, iframeId);
406
407         if (comparePropertyValue(property, computedValue, expectedValue, tolerance))
408             result += "PASS - \"" + property + "\" property for \"" + elementName + "\" element at " + time + 
409                             "s saw something close to: " + expectedValue + "<br>";
410         else
411             result += "FAIL - \"" + property + "\" property for \"" + elementName + "\" element at " + time + 
412                             "s expected: " + expectedValue + " but saw: " + computedValue + "<br>";
413     }
414 }
415
416
417 function getPropertyValue(property, elementId, iframeId)
418 {
419     var computedValue;
420     var element;
421     if (iframeId)
422         element = document.getElementById(iframeId).contentDocument.getElementById(elementId);
423     else
424         element = document.getElementById(elementId);
425
426     if (property == "lineHeight")
427         computedValue = parseInt(window.getComputedStyle(element).lineHeight);
428     else if (property == "backgroundImage"
429                || property == "borderImageSource"
430                || property == "listStyleImage"
431                || property == "webkitMaskImage"
432                || property == "webkitMaskBoxImage"
433                || property == "webkitFilter"
434                || property == "webkitBackdropFilter"
435                || property == "webkitClipPath"
436                || property == "webkitShapeInside"
437                || property == "webkitShapeOutside"
438                || property == "font-variation-settings"
439                || property == "font-style"
440                || !property.indexOf("webkitTransform")
441                || !property.indexOf("transform")) {
442         computedValue = window.getComputedStyle(element)[property.split(".")[0]];
443     } else if (property == "font-stretch") {
444         var computedStyle = window.getComputedStyle(element).getPropertyCSSValue(property);
445         computedValue = computedStyle.getFloatValue(CSSPrimitiveValue.CSS_PERCENTAGE);
446     } else {
447         var computedStyle = window.getComputedStyle(element).getPropertyCSSValue(property);
448         computedValue = computedStyle.getFloatValue(CSSPrimitiveValue.CSS_NUMBER);
449     }
450
451     return computedValue;
452 }
453
454 function comparePropertyValue(property, computedValue, expectedValue, tolerance)
455 {
456     var result = true;
457
458     if (!property.indexOf("webkitTransform") || !property.indexOf("transform")) {
459         if (typeof expectedValue == "string")
460             result = (computedValue == expectedValue);
461         else if (typeof expectedValue == "number") {
462             var m = matrixStringToArray(computedValue);
463             result = isCloseEnough(parseFloat(m[parseInt(property.substring(16))]), expectedValue, tolerance);
464         } else {
465             var m = matrixStringToArray(computedValue);
466             for (i = 0; i < expectedValue.length; ++i) {
467                 result = isCloseEnough(parseFloat(m[i]), expectedValue[i], tolerance);
468                 if (!result)
469                     break;
470             }
471         }
472     } else if (property == "webkitFilter" || property == "webkitBackdropFilter") {
473         var filterParameters = parseFilterFunctionList(computedValue);
474         var filter2Parameters = parseFilterFunctionList(expectedValue);
475         result = compareFilterFunctions(filterParameters, filter2Parameters, tolerance);
476     } else if (property == "webkitClipPath" || property == "webkitShapeInside" || property == "webkitShapeOutside") {
477         var clipPathParameters = parseBasicShape(computedValue);
478         var clipPathParameters2 = parseBasicShape(expectedValue);
479         if (!clipPathParameters || !clipPathParameters2)
480             result = false;
481         result = basicShapeParametersMatch(clipPathParameters, clipPathParameters2, tolerance);
482     } else if (property == "backgroundImage"
483                || property == "borderImageSource"
484                || property == "listStyleImage"
485                || property == "webkitMaskImage"
486                || property == "webkitMaskBoxImage")
487         result = compareCSSImages(computedValue, expectedValue, tolerance);
488     else if (property == "font-variation-settings")
489         result = compareFontVariationSettings(computedValue, expectedValue, tolerance);
490     else if (property == "font-style")
491         result = compareFontStyle(computedValue, expectedValue, tolerance);
492     else
493         result = isCloseEnough(computedValue, expectedValue, tolerance);
494     return result;
495 }
496
497 function endTest(finishCallback)
498 {
499     document.getElementById('result').innerHTML = result;
500
501     if (finishCallback)
502         finishCallback();
503
504     if (window.testRunner)
505         testRunner.notifyDone();
506 }
507
508 function checkExpectedValueCallback(expected, index)
509 {
510     return function() { checkExpectedValue(expected, index); };
511 }
512
513 function pauseAnimationAtTimeOnElement(animationName, time, element)
514 {
515     // If we haven't opted into CSS Animations and CSS Transitions as Web Animations, use the internal API.
516     if ('internals' in window && !internals.settings.cssAnimationsAndCSSTransitionsBackedByWebAnimationsEnabled())
517         return internals.pauseAnimationAtTimeOnElement(animationName, time, element);
518
519     // Otherwise, use the Web Animations API.
520     const animations = element.getAnimations();
521     for (let animation of animations) {
522         if (animation instanceof CSSAnimation && animation.animationName == animationName) {
523             animation.currentTime = time * 1000;
524             animation.pause();
525             return true;
526         }
527     }
528     return false;
529 }
530
531 var testStarted = false;
532 function startTest(expected, startCallback, finishCallback)
533 {
534     if (testStarted) return;
535     testStarted = true;
536
537     if (startCallback)
538         startCallback();
539
540     var maxTime = 0;
541
542     for (var i = 0; i < expected.length; ++i) {
543         var animationName = expected[i][0];
544         var time = expected[i][1];
545
546         // We can only use the animation fast-forward mechanism if there's an animation name
547         if (animationName && hasPauseAnimationAPI)
548             checkExpectedValue(expected, i);
549         else {
550             if (time > maxTime)
551                 maxTime = time;
552
553             window.setTimeout(checkExpectedValueCallback(expected, i), time * 1000);
554         }
555     }
556
557     if (maxTime > 0)
558         window.setTimeout(function () {
559             endTest(finishCallback);
560         }, maxTime * 1000 + 50);
561     else
562         endTest(finishCallback);
563 }
564
565 var result = "";
566 var hasPauseAnimationAPI = true;
567
568 if (window.testRunner)
569     testRunner.waitUntilDone();
570
571 function runAnimationTest(expected, startCallback, event, disablePauseAnimationAPI, doPixelTest, finishCallback)
572 {
573     if (disablePauseAnimationAPI)
574         hasPauseAnimationAPI = false;
575
576     if (window.testRunner) {
577         if (!doPixelTest)
578             testRunner.dumpAsText();
579     }
580     
581     if (!expected)
582         throw("Expected results are missing!");
583     
584     var target = document;
585     if (event == undefined)
586         waitForAnimationToStart(target, function() { startTest(expected, startCallback, finishCallback); });
587     else if (event == "load")
588         window.addEventListener(event, function() {
589             startTest(expected, startCallback, finishCallback);
590         }, false);
591 }
592
593 function waitForAnimationToStart(element, callback)
594 {
595     element.addEventListener('webkitAnimationStart', function() {
596         window.setTimeout(callback, 0); // delay to give hardware animations a chance to start
597     }, false);
598 }