1/* 2 * Copyright (C) 2013 Google Inc. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions are 6 * met: 7 * 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 13 * distribution. 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. 17 * 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. 29 */ 30 31/* 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. 35 * 36 * The following function is exported: 37 * * assertInterpolation({property: x, from: y, to: z}, [{at: fraction, is: value}]) 38 * Constructs a test case which for each fraction will output a PASS 39 * or FAIL depending on whether the interpolated result matches 40 * 'value'. Replica elements are constructed to aid eyeballing test 41 * results. 42 */ 43'use strict'; 44(function() { 45 var endEvent = 'animationend'; 46 var testCount = 0; 47 var animationEventCount = 0; 48 var durationSeconds = 0; 49 var iterationCount = 0.5; 50 var delaySeconds = 0; 51 var fragment = document.createDocumentFragment(); 52 var fragmentAttachedListeners = []; 53 var style = document.createElement('style'); 54 var afterTestCallback = null; 55 fragment.appendChild(style); 56 57 var tests = document.createElement('div'); 58 tests.id = 'interpolation-tests'; 59 tests.textContent = 'Interpolation Tests:'; 60 fragment.appendChild(tests); 61 62 var updateScheduled = false; 63 function maybeScheduleUpdate() { 64 if (updateScheduled) { 65 return; 66 } 67 updateScheduled = true; 68 setTimeout(function() { 69 updateScheduled = false; 70 document.body.appendChild(fragment); 71 fragmentAttachedListeners.forEach(function(listener) {listener();}); 72 }, 0); 73 } 74 75 function evaluateTests() { 76 var targets = document.querySelectorAll('.target.active'); 77 for (var i = 0; i < targets.length; i++) { 78 targets[i].evaluate(); 79 } 80 } 81 82 function afterTest(callback) { 83 afterTestCallback = callback; 84 } 85 86 // Constructs a timing function which produces 'y' at x = 0.5 87 function createEasing(y) { 88 // FIXME: if 'y' is > 0 and < 1 use a linear timing function and allow 89 // 'x' to vary. Use a bezier only for values < 0 or > 1. 90 if (y == 0) { 91 return 'steps(1, end)'; 92 } 93 if (y == 1) { 94 return 'steps(1, start)'; 95 } 96 if (y == 0.5) { 97 return 'steps(2, end)'; 98 } 99 // Approximate using a bezier. 100 var b = (8 * y - 1) / 6; 101 return 'cubic-bezier(0, ' + b + ', 1, ' + b + ')'; 102 } 103 104 function createTestContainer(description, className) { 105 var testContainer = document.createElement('div'); 106 testContainer.setAttribute('description', description); 107 testContainer.classList.add('test'); 108 if (className) { 109 testContainer.classList.add(className); 110 } 111 return testContainer; 112 } 113 114 function convertPropertyToCamelCase(property) { 115 return property.replace(/^-/, '').replace(/-\w/g, function(m) {return m[1].toUpperCase();}); 116 } 117 118 function describeTest(params) { 119 return convertPropertyToCamelCase(params.property) + ': from [' + params.from + '] to [' + params.to + ']'; 120 } 121 122 var nextKeyframeId = 0; 123 function assertInterpolation(params, expectations) { 124 var testId = 'test-' + ++nextKeyframeId; 125 var nextCaseId = 0; 126 var testContainer = createTestContainer(describeTest(params), testId); 127 tests.appendChild(testContainer); 128 expectations.forEach(function(expectation) { 129 testContainer.appendChild(makeInterpolationTest( 130 expectation.at, testId, 'case-' + ++nextCaseId, params, expectation.is)); 131 }); 132 maybeScheduleUpdate(); 133 } 134 135 function roundNumbers(value) { 136 // Round numbers to two decimal places. 137 return value.replace(/-?\d*\.\d+/g, function(n) { 138 return (parseFloat(n).toFixed(2)). 139 replace(/\.\d+/, function(m) { 140 return m.replace(/0+$/, ''); 141 }). 142 replace(/\.$/, ''). 143 replace(/^-0$/, '0'); 144 }); 145 } 146 147 function normalizeValue(value) { 148 return roundNumbers(value). 149 // Place whitespace between tokens. 150 replace(/([\w\d.]+|[^\s])/g, '$1 '). 151 replace(/\s+/g, ' '); 152 } 153 154 function createTargetContainer(id) { 155 var targetContainer = document.createElement('div'); 156 var template = document.querySelector('#target-template'); 157 if (template) { 158 if (template.content) 159 targetContainer.appendChild(template.content.cloneNode(true)); 160 else if (template.querySelector('div')) 161 targetContainer.appendChild(template.querySelector('div').cloneNode(true)); 162 else 163 targetContainer.appendChild(template.cloneNode(true)); 164 // Remove whitespace text nodes at start / end. 165 while (targetContainer.firstChild.nodeType != Node.ELEMENT_NODE && !/\S/.test(targetContainer.firstChild.nodeValue)) { 166 targetContainer.removeChild(targetContainer.firstChild); 167 } 168 while (targetContainer.lastChild.nodeType != Node.ELEMENT_NODE && !/\S/.test(targetContainer.lastChild.nodeValue)) { 169 targetContainer.removeChild(targetContainer.lastChild); 170 } 171 // If the template contains just one element, use that rather than a wrapper div. 172 if (targetContainer.children.length == 1 && targetContainer.childNodes.length == 1) { 173 targetContainer = targetContainer.firstChild; 174 targetContainer.parentNode.removeChild(targetContainer); 175 } 176 } 177 var target = targetContainer.querySelector('.target') || targetContainer; 178 target.classList.add('target'); 179 target.classList.add(id); 180 return targetContainer; 181 } 182 183 function sanitizeUrls(value) { 184 var matches = value.match(/url\([^\)]*\)/g); 185 if (matches !== null) { 186 for (var i = 0; i < matches.length; ++i) { 187 var url = /url\(([^\)]*)\)/g.exec(matches[i])[1]; 188 var anchor = document.createElement('a'); 189 anchor.href = url; 190 anchor.pathname = '...' + anchor.pathname.substring(anchor.pathname.lastIndexOf('/')); 191 value = value.replace(matches[i], 'url(' + anchor.href + ')'); 192 } 193 } 194 return value; 195 } 196 197 function makeInterpolationTest(fraction, testId, caseId, params, expectation) { 198 var t = async_test(describeTest(params) + ' at ' + fraction); 199 var targetContainer = createTargetContainer(caseId); 200 var target = targetContainer.querySelector('.target') || targetContainer; 201 target.classList.add('active'); 202 var replicaContainer, replica; 203 replicaContainer = createTargetContainer(caseId); 204 replica = replicaContainer.querySelector('.target') || replicaContainer; 205 replica.classList.add('replica'); 206 replica.style.setProperty(params.property, expectation); 207 if (params.prefixedProperty) { 208 for (var i = 0; i < params.prefixedProperty.length; i++) { 209 replica.style.setProperty(params.prefixedProperty[i], expectation); 210 } 211 } 212 213 target.evaluate = function() { 214 var target = this; 215 t.step(function() { 216 window.CSS && assert_true(CSS.supports(params.property, expectation)); 217 var value = getComputedStyle(target).getPropertyValue(params.property); 218 var property = params.property; 219 if (params.prefixedProperty) { 220 var i = 0; 221 while (i < params.prefixedProperty.length && !value) { 222 property = params.prefixedProperty[i++]; 223 value = getComputedStyle(target).getPropertyValue(property) 224 } 225 } 226 if (!value) { 227 assert_false(params.property + ' not supported by this browser'); 228 } 229 var originalValue = value; 230 var parsedExpectation = getComputedStyle(replica).getPropertyValue(property); 231 assert_equals(normalizeValue(originalValue), normalizeValue(parsedExpectation)); 232 t.done(); 233 }); 234 }; 235 236 var easing = createEasing(fraction); 237 testCount++; 238 var keyframes = [{}, {}]; 239 keyframes[0][convertPropertyToCamelCase(params.property)] = params.from; 240 keyframes[1][convertPropertyToCamelCase(params.property)] = params.to; 241 fragmentAttachedListeners.push(function() { 242 target.animate(keyframes, { 243 fill: 'forwards', 244 duration: 1, 245 easing: easing, 246 delay: -0.5, 247 iterations: 0.5, 248 }); 249 animationEnded(); 250 }); 251 var testFragment = document.createDocumentFragment(); 252 testFragment.appendChild(targetContainer); 253 replica && testFragment.appendChild(replicaContainer); 254 testFragment.appendChild(document.createTextNode('\n')); 255 return testFragment; 256 } 257 258 var finished = false; 259 function finishTest() { 260 finished = true; 261 evaluateTests(); 262 if (afterTestCallback) { 263 afterTestCallback(); 264 } 265 if (window.testRunner) { 266 var results = document.querySelector('#results'); 267 document.documentElement.textContent = ''; 268 document.documentElement.appendChild(results); 269 testRunner.dumpAsText(); 270 testRunner.notifyDone(); 271 } 272 } 273 274 if (window.testRunner) { 275 testRunner.waitUntilDone(); 276 } 277 278 function isLastAnimationEvent() { 279 return !finished && animationEventCount === testCount; 280 } 281 282 function animationEnded() { 283 animationEventCount++; 284 if (!isLastAnimationEvent()) { 285 return; 286 } 287 finishTest(); 288 } 289 290 document.documentElement.addEventListener(endEvent, animationEnded); 291 292 if (!window.testRunner) { 293 setTimeout(function() { 294 if (finished) { 295 return; 296 } 297 finishTest(); 298 }, 10000); 299 } 300 301 window.assertInterpolation = assertInterpolation; 302 window.afterTest = afterTest; 303})(); 304