interpolation-testcommon.js (22606B)
1 'use strict'; 2 (function() { 3 var interpolationTests = []; 4 var compositionTests = []; 5 var cssAnimationsData = { 6 sharedStyle: null, 7 nextID: 0, 8 }; 9 var expectNoInterpolation = {}; 10 var expectNotAnimatable = {}; 11 var neutralKeyframe = {}; 12 function isNeutralKeyframe(keyframe) { 13 return keyframe === neutralKeyframe; 14 } 15 16 // For the CSS interpolation methods set the delay to be negative half the 17 // duration, so we are immediately at the halfway point of the animation. 18 // We then use an easing function that maps halfway to whatever progress 19 // we actually want. 20 21 var cssAnimationsInterpolation = { 22 name: 'CSS Animations', 23 isSupported: function() {return true;}, 24 supportsProperty: function() {return true;}, 25 supportsValue: function() {return true;}, 26 setup: function() {}, 27 nonInterpolationExpectations: function(from, to) { 28 return expectFlip(from, to, 0.5); 29 }, 30 notAnimatableExpectations: function(from, to, underlying) { 31 return expectFlip(underlying, underlying, -Infinity); 32 }, 33 interpolate: function(property, from, to, at, target) { 34 var id = cssAnimationsData.nextID++; 35 if (!cssAnimationsData.sharedStyle) { 36 cssAnimationsData.sharedStyle = createElement(document.body, 'style'); 37 } 38 cssAnimationsData.sharedStyle.textContent += '' + 39 '@keyframes animation' + id + ' {' + 40 (isNeutralKeyframe(from) ? '' : `from {${property}:${from};}`) + 41 (isNeutralKeyframe(to) ? '' : `to {${property}:${to};}`) + 42 '}'; 43 target.style.animationName = 'animation' + id; 44 target.style.animationDuration = '100s'; 45 target.style.animationDelay = '-50s'; 46 target.style.animationTimingFunction = createEasing(at); 47 }, 48 interpolateWithComposition: function(property, from, fromComposite, to, toComposite, at, target) { 49 const id = cssAnimationsData.nextID++; 50 if (!cssAnimationsData.sharedStyle) { 51 cssAnimationsData.sharedStyle = createElement(document.body, 'style'); 52 } 53 cssAnimationsData.sharedStyle.textContent += '' + 54 '@keyframes animation' + id + ' {' + 55 (isNeutralKeyframe(from) 56 ? '' : `from {${property}:${from};animation-composition:${fromComposite}}`) + 57 (isNeutralKeyframe(to) 58 ? '' : `to {${property}:${to};animation-composition:${toComposite}}`) + 59 '}'; 60 target.style.animationName = 'animation' + id; 61 target.style.animationDuration = '100s'; 62 target.style.animationDelay = '-50s'; 63 target.style.animationTimingFunction = createEasing(at); 64 }, 65 }; 66 67 var cssTransitionsInterpolation = { 68 name: 'CSS Transitions', 69 isSupported: function() {return true;}, 70 supportsProperty: function() {return true;}, 71 supportsValue: function() {return true;}, 72 setup: function(property, from, target) { 73 target.style.setProperty(property, isNeutralKeyframe(from) ? '' : from); 74 }, 75 nonInterpolationExpectations: function(from, to) { 76 return expectFlip(from, to, -Infinity); 77 }, 78 notAnimatableExpectations: function(from, to, underlying) { 79 return expectFlip(from, to, -Infinity); 80 }, 81 interpolate: function(property, from, to, at, target, behavior) { 82 // Force a style recalc on target to set the 'from' value. 83 getComputedStyle(target).getPropertyValue(property); 84 target.style.transitionDuration = '100s'; 85 target.style.transitionDelay = '-50s'; 86 target.style.transitionTimingFunction = createEasing(at); 87 target.style.transitionProperty = property; 88 if (behavior) { 89 target.style.transitionBehavior = behavior; 90 } 91 target.style.setProperty(property, isNeutralKeyframe(to) ? '' : to); 92 }, 93 }; 94 95 var cssTransitionAllInterpolation = { 96 name: 'CSS Transitions with transition: all', 97 isSupported: function() {return true;}, 98 // The 'all' value doesn't cover custom properties. 99 supportsProperty: function(property) {return property.indexOf('--') !== 0;}, 100 supportsValue: function() {return true;}, 101 setup: function(property, from, target) { 102 target.style.setProperty(property, isNeutralKeyframe(from) ? '' : from); 103 }, 104 nonInterpolationExpectations: function(from, to) { 105 return expectFlip(from, to, -Infinity); 106 }, 107 notAnimatableExpectations: function(from, to, underlying) { 108 return expectFlip(from, to, -Infinity); 109 }, 110 interpolate: function(property, from, to, at, target, behavior) { 111 // Force a style recalc on target to set the 'from' value. 112 getComputedStyle(target).getPropertyValue(property); 113 target.style.transitionDuration = '100s'; 114 target.style.transitionDelay = '-50s'; 115 target.style.transitionTimingFunction = createEasing(at); 116 target.style.transitionProperty = 'all'; 117 if (behavior) { 118 target.style.transitionBehavior = behavior; 119 } 120 target.style.setProperty(property, isNeutralKeyframe(to) ? '' : to); 121 }, 122 }; 123 124 var cssTransitionsInterpolationAllowDiscrete = { 125 name: 'CSS Transitions with transition-behavior:allow-discrete', 126 isSupported: function() {return true;}, 127 supportsProperty: function() {return true;}, 128 supportsValue: function() {return true;}, 129 setup: function(property, from, target) { 130 target.style.setProperty(property, isNeutralKeyframe(from) ? '' : from); 131 }, 132 nonInterpolationExpectations: function(from, to) { 133 return expectFlip(from, to, 0.5); 134 }, 135 notAnimatableExpectations: function(from, to, underlying) { 136 return expectFlip(from, to, -Infinity); 137 }, 138 interpolate: function(property, from, to, at, target, behavior) { 139 // Force a style recalc on target to set the 'from' value. 140 getComputedStyle(target).getPropertyValue(property); 141 target.style.transitionDuration = '100s'; 142 target.style.transitionDelay = '-50s'; 143 target.style.transitionTimingFunction = createEasing(at); 144 target.style.transitionProperty = property; 145 target.style.transitionBehavior = 'allow-discrete'; 146 target.style.setProperty(property, isNeutralKeyframe(to) ? '' : to); 147 }, 148 }; 149 150 var cssTransitionAllInterpolationAllowDiscrete = { 151 name: 'CSS Transitions with transition-property:all and transition-behavor:allow-discrete', 152 isSupported: function() {return true;}, 153 // The 'all' value doesn't cover custom properties. 154 supportsProperty: function(property) {return property.indexOf('--') !== 0;}, 155 supportsValue: function() {return true;}, 156 setup: function(property, from, target) { 157 target.style.setProperty(property, isNeutralKeyframe(from) ? '' : from); 158 }, 159 nonInterpolationExpectations: function(from, to) { 160 return expectFlip(from, to, 0.5); 161 }, 162 notAnimatableExpectations: function(from, to, underlying) { 163 return expectFlip(from, to, -Infinity); 164 }, 165 interpolate: function(property, from, to, at, target, behavior) { 166 // Force a style recalc on target to set the 'from' value. 167 getComputedStyle(target).getPropertyValue(property); 168 target.style.transitionDuration = '100s'; 169 target.style.transitionDelay = '-50s'; 170 target.style.transitionTimingFunction = createEasing(at); 171 target.style.transitionProperty = 'all'; 172 target.style.transitionBehavior = 'allow-discrete'; 173 target.style.setProperty(property, isNeutralKeyframe(to) ? '' : to); 174 }, 175 }; 176 177 var webAnimationsInterpolation = { 178 name: 'Web Animations', 179 isSupported: function() {return 'animate' in Element.prototype;}, 180 supportsProperty: function(property) {return true;}, 181 supportsValue: function(value) {return value !== '';}, 182 setup: function() {}, 183 nonInterpolationExpectations: function(from, to) { 184 return expectFlip(from, to, 0.5); 185 }, 186 notAnimatableExpectations: function(from, to, underlying) { 187 return expectFlip(underlying, underlying, -Infinity); 188 }, 189 interpolate: function(property, from, to, at, target) { 190 this.interpolateWithComposition(property, from, 'replace', to, 'replace', at, target); 191 }, 192 interpolateWithComposition: function(property, from, fromComposite, to, toComposite, at, target) { 193 // This case turns into a test error later on. 194 if (!this.isSupported()) 195 return; 196 197 // Convert standard properties to camelCase. 198 if (!property.startsWith('--')) { 199 for (var i = property.length - 2; i > 0; --i) { 200 if (property[i] === '-') { 201 property = property.substring(0, i) + property[i + 1].toUpperCase() + property.substring(i + 2); 202 } 203 } 204 if (property === 'offset') { 205 property = 'cssOffset'; 206 } else if (property === 'float') { 207 property = 'cssFloat'; 208 } 209 } 210 var keyframes = []; 211 if (!isNeutralKeyframe(from)) { 212 keyframes.push({ 213 offset: 0, 214 composite: fromComposite, 215 [property]: from, 216 }); 217 } 218 if (!isNeutralKeyframe(to)) { 219 keyframes.push({ 220 offset: 1, 221 composite: toComposite, 222 [property]: to, 223 }); 224 } 225 var animation = target.animate(keyframes, { 226 fill: 'forwards', 227 duration: 100 * 1000, 228 easing: createEasing(at), 229 }); 230 animation.pause(); 231 animation.currentTime = 50 * 1000; 232 }, 233 }; 234 235 function expectFlip(from, to, flipAt) { 236 return [-0.3, 0, 0.3, 0.5, 0.6, 1, 1.5].map(function(at) { 237 return { 238 at: at, 239 expect: at < flipAt ? from : to 240 }; 241 }); 242 } 243 244 // Constructs a timing function which produces 'y' at x = 0.5 245 function createEasing(y) { 246 if (y == 0) { 247 return 'steps(1, end)'; 248 } 249 if (y == 1) { 250 return 'steps(1, start)'; 251 } 252 if (y == 0.5) { 253 return 'linear'; 254 } 255 // Approximate using a bezier. 256 var b = (8 * y - 1) / 6; 257 return 'cubic-bezier(0, ' + b + ', 1, ' + b + ')'; 258 } 259 260 function createElement(parent, tag, text) { 261 var element = document.createElement(tag || 'div'); 262 element.textContent = text || ''; 263 parent.appendChild(element); 264 return element; 265 } 266 267 function createTargetContainer(parent, className) { 268 var targetContainer = createElement(parent); 269 targetContainer.classList.add('container'); 270 var template = document.querySelector('#target-template'); 271 if (template) { 272 targetContainer.appendChild(template.content.cloneNode(true)); 273 } 274 var target = targetContainer.querySelector('.target') || targetContainer; 275 target.classList.add('target', className); 276 target.parentElement.classList.add('parent'); 277 targetContainer.target = target; 278 return targetContainer; 279 } 280 281 function roundNumbers(value) { 282 return value. 283 // Round numbers to two decimal places. 284 replace(/-?\d*\.\d+(e-?\d+)?/g, function(n) { 285 return (parseFloat(n).toFixed(2)). 286 replace(/\.\d+/, function(m) { 287 return m.replace(/0+$/, ''); 288 }). 289 replace(/\.$/, ''). 290 replace(/^-0$/, '0'); 291 }); 292 } 293 294 var anchor = document.createElement('a'); 295 function sanitizeUrls(value) { 296 var matches = value.match(/url\("([^#][^\)]*)"\)/g); 297 if (matches !== null) { 298 for (var i = 0; i < matches.length; ++i) { 299 var url = /url\("([^#][^\)]*)"\)/g.exec(matches[i])[1]; 300 anchor.href = url; 301 anchor.pathname = '...' + anchor.pathname.substring(anchor.pathname.lastIndexOf('/')); 302 value = value.replace(matches[i], 'url(' + anchor.href + ')'); 303 } 304 } 305 return value; 306 } 307 308 function normalizeValue(value) { 309 return roundNumbers(sanitizeUrls(value)). 310 // Place whitespace between tokens. 311 replace(/([\w\d.]+|[^\s])/g, '$1 '). 312 replace(/\s+/g, ' '); 313 } 314 315 function stringify(text) { 316 if (!text.includes("'")) { 317 return `'${text}'`; 318 } 319 return `"${text.replace('"', '\\"')}"`; 320 } 321 322 function keyframeText(keyframe) { 323 return isNeutralKeyframe(keyframe) ? 'neutral' : `[${keyframe}]`; 324 } 325 326 function keyframeCode(keyframe) { 327 return isNeutralKeyframe(keyframe) ? 'neutralKeyframe' : `${stringify(keyframe)}`; 328 } 329 330 function createInterpolationTestTargets(interpolationMethod, interpolationMethodContainer, interpolationTest) { 331 var property = interpolationTest.options.property; 332 var from = interpolationTest.options.from; 333 var to = interpolationTest.options.to; 334 let underlying = interpolationTest.options.underlying; 335 var comparisonFunction = interpolationTest.options.comparisonFunction; 336 var behavior = interpolationTest.options.behavior; 337 338 if ((interpolationTest.options.method && interpolationTest.options.method != interpolationMethod.name) 339 || !interpolationMethod.supportsProperty(property) 340 || !interpolationMethod.supportsValue(from) 341 || !interpolationMethod.supportsValue(to)) { 342 return; 343 } 344 345 var testText = `${interpolationMethod.name}: property <${property}> from ${keyframeText(from)} to ${keyframeText(to)}`; 346 var testContainer = createElement(interpolationMethodContainer, 'div'); 347 createElement(testContainer); 348 var expectations = interpolationTest.expectations; 349 var applyUnderlying = false; 350 if (expectations === expectNoInterpolation) { 351 expectations = interpolationMethod.nonInterpolationExpectations(from, to); 352 } else if (expectations === expectNotAnimatable) { 353 expectations = interpolationMethod.notAnimatableExpectations(from, to, interpolationTest.options.underlying); 354 applyUnderlying = true; 355 } else if (interpolationTest.options[interpolationMethod.name]) { 356 expectations = interpolationTest.options[interpolationMethod.name]; 357 } 358 359 // Setup a standard equality function if an override is not provided. 360 if (!comparisonFunction) { 361 comparisonFunction = (actual, expected) => { 362 assert_equals(normalizeValue(actual), normalizeValue(expected)); 363 }; 364 } 365 366 return expectations.map(function(expectation) { 367 var actualTargetContainer = createTargetContainer(testContainer, 'actual'); 368 var expectedTargetContainer = createTargetContainer(testContainer, 'expected'); 369 var expectedProperties = expectation.option || expectation.expect; 370 if (typeof expectedProperties !== "object") { 371 expectedProperties = {[property]: expectedProperties}; 372 } 373 var target = actualTargetContainer.target; 374 if (applyUnderlying) { 375 assert_true(typeof underlying !== 'undefined', '\'underlying\' value must be provided'); 376 target.style.setProperty(property, underlying); 377 } 378 interpolationMethod.setup(property, from, target); 379 target.interpolate = function() { 380 interpolationMethod.interpolate(property, from, to, expectation.at, target, behavior); 381 }; 382 target.measure = function() { 383 for (var [expectedProp, expectedStr] of Object.entries(expectedProperties)) { 384 if (!isNeutralKeyframe(expectedStr)) { 385 expectedTargetContainer.target.style.setProperty(expectedProp, expectedStr); 386 } 387 var expectedValue = getComputedStyle(expectedTargetContainer.target).getPropertyValue(expectedProp); 388 let testName = `${testText} at (${expectation.at}) should be [${sanitizeUrls(expectedStr)}]`; 389 if (property !== expectedProp) { 390 testName += ` for <${expectedProp}>`; 391 } 392 test(function() { 393 assert_true(interpolationMethod.isSupported(), `${interpolationMethod.name} should be supported`); 394 395 if (from && from !== neutralKeyframe) { 396 assert_true(CSS.supports(property, from), '\'from\' value should be supported'); 397 } 398 if (to && to !== neutralKeyframe) { 399 assert_true(CSS.supports(property, to), '\'to\' value should be supported'); 400 } 401 if (typeof underlying !== 'undefined') { 402 assert_true(CSS.supports(property, underlying), '\'underlying\' value should be supported'); 403 } 404 405 comparisonFunction( 406 getComputedStyle(target).getPropertyValue(expectedProp), 407 expectedValue); 408 }, testName); 409 } 410 }; 411 return target; 412 }); 413 } 414 415 function createCompositionTestTargets(compositionMethod, compositionMethodContainer, compositionTest) { 416 var options = compositionTest.options; 417 var property = options.property; 418 var underlying = options.underlying; 419 var comparisonFunction = options.comparisonFunction; 420 var from = options.accumulateFrom || options.addFrom || options.replaceFrom; 421 var to = options.accumulateTo || options.addTo || options.replaceTo; 422 var fromComposite = 'accumulateFrom' in options ? 'accumulate' : 'addFrom' in options ? 'add' : 'replace'; 423 var toComposite = 'accumulateTo' in options ? 'accumulate' : 'addTo' in options ? 'add' : 'replace'; 424 const invalidFrom = 'addFrom' in options === 'replaceFrom' in options 425 && 'addFrom' in options === 'accumulateFrom' in options; 426 const invalidTo = 'addTo' in options === 'replaceTo' in options 427 && 'addTo' in options === 'accumulateTo' in options; 428 if (invalidFrom || invalidTo) { 429 test(function() { 430 assert_false(invalidFrom, 'Exactly one of accumulateFrom, addFrom, or replaceFrom must be specified'); 431 assert_false(invalidTo, 'Exactly one of accumulateTo, addTo, or replaceTo must be specified'); 432 }, `Composition tests must have valid setup`); 433 } 434 435 var testText = `Compositing ${compositionMethod.name}: property <${property}> underlying [${underlying}] from ${fromComposite} [${from}] to ${toComposite} [${to}]`; 436 var testContainer = createElement(compositionMethodContainer, 'div'); 437 createElement(testContainer); 438 439 // Setup a standard equality function if an override is not provided. 440 if (!comparisonFunction) { 441 comparisonFunction = (actual, expected) => { 442 assert_equals(normalizeValue(actual), normalizeValue(expected)); 443 }; 444 } 445 446 return compositionTest.expectations.map(function(expectation) { 447 var actualTargetContainer = createTargetContainer(testContainer, 'actual'); 448 var expectedTargetContainer = createTargetContainer(testContainer, 'expected'); 449 var expectedStr = expectation.option || expectation.expect; 450 if (!isNeutralKeyframe(expectedStr)) { 451 expectedTargetContainer.target.style.setProperty(property, expectedStr); 452 } 453 var target = actualTargetContainer.target; 454 target.style.setProperty(property, underlying); 455 target.interpolate = function() { 456 compositionMethod.interpolateWithComposition(property, from, fromComposite, to, toComposite, expectation.at, target); 457 }; 458 target.measure = function() { 459 var expectedValue = getComputedStyle(expectedTargetContainer.target).getPropertyValue(property); 460 test(function() { 461 462 if (from && from !== neutralKeyframe) { 463 assert_true(CSS.supports(property, from), '\'from\' value should be supported'); 464 } 465 if (to && to !== neutralKeyframe) { 466 assert_true(CSS.supports(property, to), '\'to\' value should be supported'); 467 } 468 if (typeof underlying !== 'undefined') { 469 assert_true(CSS.supports(property, underlying), '\'underlying\' value should be supported'); 470 } 471 472 comparisonFunction( 473 getComputedStyle(target).getPropertyValue(property), 474 expectedValue); 475 }, `${testText} at (${expectation.at}) should be [${sanitizeUrls(expectedStr)}]`); 476 }; 477 return target; 478 }); 479 } 480 481 482 483 function createTestTargets(interpolationMethods, interpolationTests, compositionMethods, compositionTests, container) { 484 var targets = []; 485 for (var interpolationMethod of interpolationMethods) { 486 var interpolationMethodContainer = createElement(container); 487 for (var interpolationTest of interpolationTests) { 488 if(!interpolationTest.options.target_names || 489 interpolationTest.options.target_names.includes(interpolationMethod.name)) { 490 [].push.apply(targets, createInterpolationTestTargets(interpolationMethod, interpolationMethodContainer, interpolationTest)); 491 } 492 } 493 } 494 for (var compositionMethod of compositionMethods) { 495 var compositionContainer = createElement(container); 496 for (var compositionTest of compositionTests) { 497 [].push.apply(targets, createCompositionTestTargets(compositionMethod, compositionContainer, compositionTest)); 498 } 499 } 500 return targets; 501 } 502 503 function test_no_interpolation(options) { 504 test_interpolation(options, expectNoInterpolation); 505 } 506 function test_not_animatable(options) { 507 test_interpolation(options, expectNotAnimatable); 508 } 509 function create_tests(addAllowDiscreteTests) { 510 var interpolationMethods = [ 511 cssTransitionsInterpolation, 512 cssTransitionAllInterpolation, 513 cssAnimationsInterpolation, 514 webAnimationsInterpolation, 515 ]; 516 var compositionMethods = [ 517 cssAnimationsInterpolation, 518 webAnimationsInterpolation, 519 ]; 520 if (addAllowDiscreteTests) { 521 interpolationMethods = [ 522 cssTransitionsInterpolationAllowDiscrete, 523 cssTransitionAllInterpolationAllowDiscrete, 524 ].concat(interpolationMethods); 525 } 526 var container = createElement(document.body); 527 var targets = createTestTargets(interpolationMethods, interpolationTests, 528 compositionMethods, compositionTests, 529 container); 530 // Separate interpolation and measurement into different phases to avoid O(n^2) of the number of targets. 531 for (var target of targets) { 532 target.interpolate(); 533 } 534 for (var target of targets) { 535 target.measure(); 536 } 537 container.remove(); 538 } 539 540 function test_interpolation(options, expectations) { 541 interpolationTests.push({options, expectations}); 542 create_tests(expectations === expectNoInterpolation || expectations === expectNotAnimatable); 543 interpolationTests = []; 544 } 545 function test_composition(options, expectations) { 546 compositionTests.push({options, expectations}); 547 create_tests(); 548 compositionTests = []; 549 } 550 window.test_interpolation = test_interpolation; 551 window.test_no_interpolation = test_no_interpolation; 552 window.test_not_animatable = test_not_animatable; 553 window.test_composition = test_composition; 554 window.neutralKeyframe = neutralKeyframe; 555 window.roundNumbers = roundNumbers; 556 window.normalizeValue = normalizeValue; 557 })();