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 startEvent = webkitPrefix ? 'webkitAnimationStart' : 'animationstart';
57 var endEvent = webkitPrefix ? 'webkitAnimationEnd' : 'animationend';
59 var animationEventCount = 0;
60 // FIXME: This should be 0, but 0 duration animations are broken in at least
61 // pre-Web-Animations Blink, WebKit and Gecko.
62 var durationSeconds = 0.001;
63 var iterationCount = 0.5;
65 var cssText = '.test:hover:before {\n' +
66 ' content: attr(description);\n' +
67 ' position: absolute;\n' +
69 ' background: gold;\n' +
71 var fragment = document.createDocumentFragment();
72 var style = document.createElement('style');
73 var afterTestCallback = null;
74 fragment.appendChild(style);
76 var updateScheduled = false;
77 function maybeScheduleUpdate() {
78 if (updateScheduled) {
81 updateScheduled = true;
82 setTimeout(function() {
83 updateScheduled = false;
84 style.innerHTML = cssText;
85 document.body.appendChild(fragment);
89 function dumpResults() {
90 var targets = document.querySelectorAll('.target.active');
92 // Convert back to reference to avoid cases where the computed style is
93 // out of sync with the compositor.
94 for (var i = 0; i < targets.length; i++) {
95 targets[i].convertToReference();
97 style.parentNode.removeChild(style);
99 var resultString = '';
100 for (var i = 0; i < targets.length; i++) {
101 resultString += targets[i].getResultString() + '\n';
103 var results = document.createElement('div');
104 results.style.whiteSpace = 'pre';
105 results.textContent = resultString;
106 results.id = 'results';
107 document.body.appendChild(results);
111 function convertToReference() {
112 console.assert(isRefTest);
113 var scripts = document.querySelectorAll('script');
114 for (var i = 0; i < scripts.length; i++) {
115 scripts[i].parentNode.removeChild(scripts[i]);
117 style.parentNode.removeChild(style);
118 var html = document.documentElement.outerHTML;
119 document.documentElement.style.whiteSpace = 'pre';
120 document.documentElement.textContent = html;
123 function afterTest(callback) {
124 afterTestCallback = callback;
127 function runAsRefTest() {
128 console.assert(!isRefTest);
132 // Constructs a timing function which produces 'y' at x = 0.5
133 function createEasing(y) {
134 // FIXME: if 'y' is > 0 and < 1 use a linear timing function and allow
135 // 'x' to vary. Use a bezier only for values < 0 or > 1.
137 return 'steps(1, end)';
140 return 'steps(1, start)';
143 return 'steps(2, end)';
145 // Approximate using a bezier.
146 var b = (8 * y - 1) / 6;
147 return 'cubic-bezier(0, ' + b + ', 1, ' + b + ')';
150 function testInterpolationAt(fractions, params) {
151 if (!Array.isArray(fractions)) {
152 fractions = [fractions];
154 assertInterpolation(params, fractions.map(function(fraction) {
155 return {at: fraction};
159 function describeTest(params) {
160 return params.property + ': from [' + params.from + '] to [' + params.to + ']';
163 function assertInterpolation(params, expectations) {
164 // If the prefixed property is not supported, try to unprefix it.
165 if (/^-[^-]+-/.test(params.property) && !CSS.supports(params.property, 'initial')) {
166 var unprefixed = params.property.replace(/^-[^-]+-/, '');
167 if (CSS.supports(unprefixed, 'initial')) {
168 params.property = unprefixed;
171 var testId = defineKeyframes(params);
173 var testContainer = document.createElement('div');
174 testContainer.setAttribute('description', describeTest(params));
175 testContainer.classList.add('test');
176 testContainer.classList.add(testId);
177 fragment.appendChild(testContainer);
178 expectations.forEach(function(expectation) {
179 testContainer.appendChild(makeInterpolationTest(
180 expectation.at, testId, 'case-' + ++nextCaseId, params, expectation.is));
182 maybeScheduleUpdate();
185 var nextKeyframeId = 0;
186 function defineKeyframes(params) {
187 var testId = 'test-' + ++nextKeyframeId;
188 cssText += '@' + webkitPrefix + 'keyframes ' + testId + ' { \n' +
189 ' 0% { ' + params.property + ': ' + params.from + '; }\n' +
190 ' 100% { ' + params.property + ': ' + params.to + '; }\n' +
195 function normalizeValue(value) {
197 // Round numbers to two decimal places.
198 replace(/-?\d*\.\d+/g, function(n) {
199 return (parseFloat(n).toFixed(2)).
200 replace(/\.0*$/, '').
201 replace(/^-0$/, '0');
203 // Place whitespace between tokens.
204 replace(/([\w\d.]+|[^\s])/g, '$1 ').
205 replace(/\s+/g, ' ');
208 function createTargetContainer(id) {
209 var targetContainer = document.createElement('div');
210 var template = document.querySelector('#target-template');
212 targetContainer.appendChild(template.content.cloneNode(true));
213 // Remove whitespace text nodes at start / end.
214 while (targetContainer.firstChild.nodeType != Node.ELEMENT_NODE && !/\S/.test(targetContainer.firstChild.nodeValue)) {
215 targetContainer.removeChild(targetContainer.firstChild);
217 while (targetContainer.lastChild.nodeType != Node.ELEMENT_NODE && !/\S/.test(targetContainer.lastChild.nodeValue)) {
218 targetContainer.removeChild(targetContainer.lastChild);
220 // If the template contains just one element, use that rather than a wrapper div.
221 if (targetContainer.children.length == 1 && targetContainer.childNodes.length == 1) {
222 targetContainer = targetContainer.firstChild;
223 targetContainer.remove();
226 var target = targetContainer.querySelector('.target') || targetContainer;
227 target.classList.add('target');
228 target.classList.add(id);
229 return targetContainer;
232 function sanitizeUrls(value) {
233 var matches = value.match(/url\([^\)]*\)/g);
234 if (matches !== null) {
235 for (var i = 0; i < matches.length; ++i) {
236 var url = /url\(([^\)]*)\)/g.exec(matches[i])[1];
237 var anchor = document.createElement('a');
239 anchor.pathname = '...' + anchor.pathname.substring(anchor.pathname.lastIndexOf('/'));
240 value = value.replace(matches[i], 'url(' + anchor.href + ')');
246 function makeInterpolationTest(fraction, testId, caseId, params, expectation) {
247 console.assert(expectation === undefined || !isRefTest);
248 var targetContainer = createTargetContainer(caseId);
249 var target = targetContainer.querySelector('.target') || targetContainer;
250 target.classList.add('active');
251 var replicaContainer, replica;
252 if (expectation !== undefined) {
253 replicaContainer = createTargetContainer(caseId);
254 replica = replicaContainer.querySelector('.target') || replicaContainer;
255 replica.classList.add('replica');
256 replica.style.setProperty(params.property, expectation);
258 target.getResultString = function() {
259 if (!CSS.supports(params.property, expectation)) {
260 return 'FAIL: [' + params.property + ': ' + expectation + '] is not supported';
262 var value = getComputedStyle(this).getPropertyValue(params.property);
265 if (expectation !== undefined) {
266 var parsedExpectation = getComputedStyle(replica).getPropertyValue(params.property);
267 var pass = normalizeValue(value) === normalizeValue(parsedExpectation);
268 result = pass ? 'PASS: ' : 'FAIL: ';
269 reason = pass ? '' : ', expected [' + expectation + ']' +
270 (expectation === parsedExpectation ? '' : ' (parsed as [' + sanitizeUrls(parsedExpectation) + '])');
271 value = pass ? expectation : sanitizeUrls(value);
273 return result + params.property + ' from [' + params.from + '] to ' +
274 '[' + params.to + '] was [' + value + ']' +
275 ' at ' + fraction + reason;
277 target.convertToReference = function() {
278 this.style[params.property] = getComputedStyle(this).getPropertyValue(params.property);
280 var easing = createEasing(fraction);
281 cssText += '.' + testId + ' .' + caseId + '.active {\n' +
282 ' ' + webkitPrefix + 'animation: ' + testId + ' ' + durationSeconds + 's forwards;\n' +
283 ' ' + webkitPrefix + 'animation-timing-function: ' + easing + ';\n' +
284 ' ' + webkitPrefix + 'animation-iteration-count: ' + iterationCount + ';\n' +
285 ' ' + webkitPrefix + 'animation-delay: ' + delaySeconds + 's;\n' +
288 var testFragment = document.createDocumentFragment();
289 testFragment.appendChild(targetContainer);
290 replica && testFragment.appendChild(replicaContainer);
291 testFragment.appendChild(document.createTextNode('\n'));
295 var finished = false;
296 function finishTest() {
299 if (afterTestCallback) {
302 if (window.testRunner) {
304 var results = document.querySelector('#results');
305 document.documentElement.textContent = '';
306 document.documentElement.appendChild(results);
307 testRunner.dumpAsText();
309 testRunner.notifyDone();
313 if (window.testRunner) {
314 testRunner.waitUntilDone();
317 function isLastAnimationEvent() {
318 return !finished && animationEventCount === testCount;
321 function endEventListener() {
322 animationEventCount++;
323 if (!isLastAnimationEvent()) {
329 if (window.internals) {
331 document.documentElement.addEventListener(endEvent, endEventListener);
332 } else if (webkitPrefix) {
333 durationSeconds = 1e9;
335 delaySeconds = -durationSeconds / 2;
336 document.documentElement.addEventListener(startEvent, function() {
337 animationEventCount++;
338 if (!isLastAnimationEvent()) {
341 setTimeout(finishTest, 0);
344 document.documentElement.addEventListener(endEvent, endEventListener);
347 if (!window.testRunner) {
348 setTimeout(function() {
356 window.runAsRefTest = runAsRefTest;
357 window.testInterpolationAt = testInterpolationAt;
358 window.assertInterpolation = assertInterpolation;
359 window.convertToReference = convertToReference;
360 window.afterTest = afterTest;