2 * Copyright (C) 2013 Google Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
8 * * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 * * Redistributions in binary form must reproduce the above
11 * copyright notice, this list of conditions and the following disclaimer
12 * in the documentation and/or other materials provided with the
14 * * Neither the name of Google Inc. nor the names of its
15 * contributors may be used to endorse or promote products derived from
16 * this software without specific prior written permission.
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32 * This script is intended to be used for constructing layout tests which
33 * exercise the interpolation functionaltiy of the animation system.
34 * Tests which run using this script should be portable across browsers.
36 * The following functions are exported:
37 * * runAsRefTest - indicates that the test is a ref test and disables
38 * dumping of textual output.
39 * * testInterpolationAt([timeFractions], {property: x, from: y, to: z})
40 * Constructs a test case for the interpolation of property x from
41 * value y to value z at each of the times in timeFractions.
42 * * assertInterpolation({property: x, from: y, to: z}, [{at: fraction, is: value}])
43 * Constructs a test case which for each fraction will output a PASS
44 * or FAIL depending on whether the interpolated result matches
45 * 'value'. Replica elements are constructed to aid eyeballing test
46 * results. This function may not be used in a ref test.
47 * * convertToReference - This is intended to be used interactively to
48 * construct a reference given the results of a test. To build a
49 * reference, run the test, open the inspector and trigger this
50 * function, then copy/paste the results.
54 var webkitPrefix = 'webkitAnimation' in document.documentElement.style ? '-webkit-' : '';
55 var isRefTest = false;
56 var webAnimationsTest = typeof Element.prototype.animate === 'function';
57 var startEvent = webkitPrefix ? 'webkitAnimationStart' : 'animationstart';
58 var endEvent = webkitPrefix ? 'webkitAnimationEnd' : 'animationend';
60 var animationEventCount = 0;
61 // FIXME: This should be 0, but 0 duration animations are broken in at least
62 // pre-Web-Animations Blink, WebKit and Gecko.
63 var durationSeconds = 0.001;
64 var iterationCount = 0.5;
66 var cssText = '.test:hover:before {\n' +
67 ' content: attr(description);\n' +
68 ' position: absolute;\n' +
70 ' background: gold;\n' +
72 var fragment = document.createDocumentFragment();
73 var fragmentAttachedListeners = [];
74 var style = document.createElement('style');
75 var cssTests = document.createElement('div');
76 cssTests.id = 'css-tests';
77 cssTests.textContent = 'CSS Animations:';
78 var afterTestCallback = null;
79 fragment.appendChild(style);
80 fragment.appendChild(cssTests);
82 if (webAnimationsTest) {
83 var waTests = document.createElement('div');
84 waTests.id = 'web-animations-tests';
85 waTests.textContent = 'Web Animations API:';
86 fragment.appendChild(waTests);
89 var updateScheduled = false;
90 function maybeScheduleUpdate() {
91 if (updateScheduled) {
94 updateScheduled = true;
95 setTimeout(function() {
96 updateScheduled = false;
97 style.innerHTML = cssText;
98 document.body.appendChild(fragment);
99 fragmentAttachedListeners.forEach(function(listener) {listener();});
103 function dumpResults() {
104 var targets = document.querySelectorAll('.target.active');
106 // Convert back to reference to avoid cases where the computed style is
107 // out of sync with the compositor.
108 for (var i = 0; i < targets.length; i++) {
109 targets[i].convertToReference();
111 style.parentNode.removeChild(style);
113 var cssResultString = 'CSS Animations:\n';
114 var waResultString = 'Web Animations API:\n';
115 for (var i = 0; i < targets.length; i++) {
116 if (targets[i].testType === 'css') {
117 cssResultString += targets[i].getResultString() + '\n';
119 waResultString += targets[i].getResultString() + '\n';
122 var results = document.createElement('pre');
123 results.textContent = cssResultString + (webAnimationsTest ? '\n' + waResultString : '');
124 results.id = 'results';
125 document.body.appendChild(results);
129 function convertToReference() {
130 console.assert(isRefTest);
131 var scripts = document.querySelectorAll('script');
132 for (var i = 0; i < scripts.length; i++) {
133 scripts[i].parentNode.removeChild(scripts[i]);
135 style.parentNode.removeChild(style);
136 var html = document.documentElement.outerHTML;
137 document.documentElement.style.whiteSpace = 'pre';
138 document.documentElement.textContent = html;
141 function afterTest(callback) {
142 afterTestCallback = callback;
145 function runAsRefTest() {
146 console.assert(!isRefTest);
150 // Constructs a timing function which produces 'y' at x = 0.5
151 function createEasing(y) {
152 // FIXME: if 'y' is > 0 and < 1 use a linear timing function and allow
153 // 'x' to vary. Use a bezier only for values < 0 or > 1.
155 return 'steps(1, end)';
158 return 'steps(1, start)';
161 return 'steps(2, end)';
163 // Approximate using a bezier.
164 var b = (8 * y - 1) / 6;
165 return 'cubic-bezier(0, ' + b + ', 1, ' + b + ')';
168 function testInterpolationAt(fractions, params) {
169 if (!Array.isArray(fractions)) {
170 fractions = [fractions];
172 assertInterpolation(params, fractions.map(function(fraction) {
173 return {at: fraction};
177 function createTestContainer(description, className) {
178 var testContainer = document.createElement('div');
179 testContainer.setAttribute('description', description);
180 testContainer.classList.add('test');
182 testContainer.classList.add(className);
184 return testContainer;
187 function convertPropertyToCamelCase(property) {
188 return property.replace(/^-/, '').replace(/-\w/g, function(m) {return m[1].toUpperCase();});
191 function describeCSSTest(params) {
192 return 'CSS ' + params.property + ': from [' + params.from + '] to [' + params.to + ']';
195 function describeWATest(params) {
196 return 'element.animate() ' + convertPropertyToCamelCase(params.property) + ': from [' + params.from + '] to [' + params.to + ']';
199 function assertInterpolation(params, expectations) {
200 var testId = defineKeyframes(params);
202 var cssTestContainer = createTestContainer(describeCSSTest(params), testId);
203 cssTests.appendChild(cssTestContainer);
204 if (webAnimationsTest) {
205 var waTestContainer = createTestContainer(describeWATest(params), testId);
206 waTests.appendChild(waTestContainer);
208 expectations.forEach(function(expectation) {
209 cssTestContainer.appendChild(makeInterpolationTest(
210 'css', expectation.at, testId, 'case-' + ++nextCaseId, params, expectation.is));
212 if (webAnimationsTest) {
213 expectations.forEach(function(expectation) {
214 waTestContainer.appendChild(makeInterpolationTest(
215 'web-animations', expectation.at, testId, 'case-' + ++nextCaseId, params, expectation.is));
218 maybeScheduleUpdate();
221 var nextKeyframeId = 0;
222 function defineKeyframes(params) {
223 var testId = 'test-' + ++nextKeyframeId;
224 cssText += '@' + webkitPrefix + 'keyframes ' + testId + ' { \n' +
225 ' 0% { ' + params.property + ': ' + params.from + '; }\n' +
226 ' 100% { ' + params.property + ': ' + params.to + '; }\n' +
231 function roundNumbers(value) {
233 // Round numbers to two decimal places.
234 replace(/-?\d*\.\d+/g, function(n) {
235 return (parseFloat(n).toFixed(2)).
236 replace(/\.\d+/, function(m) {
237 return m.replace(/0+$/, '');
240 replace(/^-0$/, '0');
244 function normalizeValue(value) {
245 return roundNumbers(value).
246 // Place whitespace between tokens.
247 replace(/([\w\d.]+|[^\s])/g, '$1 ').
248 replace(/\s+/g, ' ');
251 function createTargetContainer(id) {
252 var targetContainer = document.createElement('div');
253 var template = document.querySelector('#target-template');
255 targetContainer.appendChild(template.content.cloneNode(true));
256 // Remove whitespace text nodes at start / end.
257 while (targetContainer.firstChild.nodeType != Node.ELEMENT_NODE && !/\S/.test(targetContainer.firstChild.nodeValue)) {
258 targetContainer.removeChild(targetContainer.firstChild);
260 while (targetContainer.lastChild.nodeType != Node.ELEMENT_NODE && !/\S/.test(targetContainer.lastChild.nodeValue)) {
261 targetContainer.removeChild(targetContainer.lastChild);
263 // If the template contains just one element, use that rather than a wrapper div.
264 if (targetContainer.children.length == 1 && targetContainer.childNodes.length == 1) {
265 targetContainer = targetContainer.firstChild;
266 targetContainer.remove();
269 var target = targetContainer.querySelector('.target') || targetContainer;
270 target.classList.add('target');
271 target.classList.add(id);
272 return targetContainer;
275 function sanitizeUrls(value) {
276 var matches = value.match(/url\([^\)]*\)/g);
277 if (matches !== null) {
278 for (var i = 0; i < matches.length; ++i) {
279 var url = /url\(([^\)]*)\)/g.exec(matches[i])[1];
280 var anchor = document.createElement('a');
282 anchor.pathname = '...' + anchor.pathname.substring(anchor.pathname.lastIndexOf('/'));
283 value = value.replace(matches[i], 'url(' + anchor.href + ')');
289 function makeInterpolationTest(testType, fraction, testId, caseId, params, expectation) {
290 console.assert(expectation === undefined || !isRefTest);
291 var targetContainer = createTargetContainer(caseId);
292 var target = targetContainer.querySelector('.target') || targetContainer;
293 target.classList.add('active');
294 var replicaContainer, replica;
295 if (expectation !== undefined) {
296 replicaContainer = createTargetContainer(caseId);
297 replica = replicaContainer.querySelector('.target') || replicaContainer;
298 replica.classList.add('replica');
299 replica.style.setProperty(params.property, expectation);
301 target.testType = testType;
302 target.getResultString = function() {
303 if (!CSS.supports(params.property, expectation)) {
304 return 'FAIL: [' + params.property + ': ' + expectation + '] is not supported';
306 var value = getComputedStyle(this).getPropertyValue(params.property);
309 var property = testType === 'css' ? params.property : convertPropertyToCamelCase(params.property);
310 if (expectation !== undefined) {
311 var parsedExpectation = getComputedStyle(replica).getPropertyValue(params.property);
312 var pass = normalizeValue(value) === normalizeValue(parsedExpectation);
313 result = pass ? 'PASS: ' : 'FAIL: ';
314 reason = pass ? '' : ', expected [' + expectation + ']' +
315 (expectation === parsedExpectation ? '' : ' (parsed as [' + sanitizeUrls(roundNumbers(parsedExpectation)) + '])');
316 value = pass ? expectation : sanitizeUrls(value);
318 return result + property + ' from [' + params.from + '] to ' +
319 '[' + params.to + '] was [' + value + ']' +
320 ' at ' + fraction + reason;
322 target.convertToReference = function() {
323 this.style[params.property] = getComputedStyle(this).getPropertyValue(params.property);
325 var easing = createEasing(fraction);
327 if (testType === 'css') {
328 cssText += '.' + testId + ' .' + caseId + '.active {\n' +
329 ' ' + webkitPrefix + 'animation: ' + testId + ' ' + durationSeconds + 's forwards;\n' +
330 ' ' + webkitPrefix + 'animation-timing-function: ' + easing + ';\n' +
331 ' ' + webkitPrefix + 'animation-iteration-count: ' + iterationCount + ';\n' +
332 ' ' + webkitPrefix + 'animation-delay: ' + delaySeconds + 's;\n' +
335 var keyframes = [{}, {}];
336 keyframes[0][convertPropertyToCamelCase(params.property)] = params.from;
337 keyframes[1][convertPropertyToCamelCase(params.property)] = params.to;
338 fragmentAttachedListeners.push(function() {
339 target.animate(keyframes, {
349 var testFragment = document.createDocumentFragment();
350 testFragment.appendChild(targetContainer);
351 replica && testFragment.appendChild(replicaContainer);
352 testFragment.appendChild(document.createTextNode('\n'));
356 var finished = false;
357 function finishTest() {
360 if (afterTestCallback) {
363 if (window.testRunner) {
365 var results = document.querySelector('#results');
366 document.documentElement.textContent = '';
367 document.documentElement.appendChild(results);
368 testRunner.dumpAsText();
370 testRunner.notifyDone();
374 if (window.testRunner) {
375 testRunner.waitUntilDone();
378 function isLastAnimationEvent() {
379 return !finished && animationEventCount === testCount;
382 function animationEnded() {
383 animationEventCount++;
384 if (!isLastAnimationEvent()) {
390 if (window.internals) {
392 document.documentElement.addEventListener(endEvent, animationEnded);
393 } else if (webkitPrefix) {
394 durationSeconds = 1e9;
396 delaySeconds = -durationSeconds / 2;
397 document.documentElement.addEventListener(startEvent, function() {
398 animationEventCount++;
399 if (!isLastAnimationEvent()) {
402 setTimeout(finishTest, 0);
405 document.documentElement.addEventListener(endEvent, animationEnded);
408 if (!window.testRunner) {
409 setTimeout(function() {
417 function disableWebAnimationsTest() {
418 if (webAnimationsTest) {
419 fragment.querySelector('#web-animations-tests').remove();
420 webAnimationsTest = false;
424 window.runAsRefTest = runAsRefTest;
425 window.testInterpolationAt = testInterpolationAt;
426 window.assertInterpolation = assertInterpolation;
427 window.convertToReference = convertToReference;
428 window.afterTest = afterTest;
429 window.disableWebAnimationsTest = disableWebAnimationsTest;