diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /testing/web-platform/tests/css/support/interpolation-testcommon.js | |
parent | Initial commit. (diff) | |
download | firefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/css/support/interpolation-testcommon.js')
-rw-r--r-- | testing/web-platform/tests/css/support/interpolation-testcommon.js | 464 |
1 files changed, 464 insertions, 0 deletions
diff --git a/testing/web-platform/tests/css/support/interpolation-testcommon.js b/testing/web-platform/tests/css/support/interpolation-testcommon.js new file mode 100644 index 0000000000..002841cfea --- /dev/null +++ b/testing/web-platform/tests/css/support/interpolation-testcommon.js @@ -0,0 +1,464 @@ +'use strict'; +(function() { + var interpolationTests = []; + var compositionTests = []; + var cssAnimationsData = { + sharedStyle: null, + nextID: 0, + }; + var expectNoInterpolation = {}; + var expectNotAnimatable = {}; + var neutralKeyframe = {}; + function isNeutralKeyframe(keyframe) { + return keyframe === neutralKeyframe; + } + + // For the CSS interpolation methods set the delay to be negative half the + // duration, so we are immediately at the halfway point of the animation. + // We then use an easing function that maps halfway to whatever progress + // we actually want. + + var cssAnimationsInterpolation = { + name: 'CSS Animations', + isSupported: function() {return true;}, + supportsProperty: function() {return true;}, + supportsValue: function() {return true;}, + setup: function() {}, + nonInterpolationExpectations: function(from, to) { + return expectFlip(from, to, 0.5); + }, + notAnimatableExpectations: function(from, to, underlying) { + return expectFlip(underlying, underlying, -Infinity); + }, + interpolate: function(property, from, to, at, target) { + var id = cssAnimationsData.nextID++; + if (!cssAnimationsData.sharedStyle) { + cssAnimationsData.sharedStyle = createElement(document.body, 'style'); + } + cssAnimationsData.sharedStyle.textContent += '' + + '@keyframes animation' + id + ' {' + + (isNeutralKeyframe(from) ? '' : `from {${property}:${from};}`) + + (isNeutralKeyframe(to) ? '' : `to {${property}:${to};}`) + + '}'; + target.style.animationName = 'animation' + id; + target.style.animationDuration = '100s'; + target.style.animationDelay = '-50s'; + target.style.animationTimingFunction = createEasing(at); + }, + }; + + var cssTransitionsInterpolation = { + name: 'CSS Transitions', + isSupported: function() {return true;}, + supportsProperty: function() {return true;}, + supportsValue: function() {return true;}, + setup: function(property, from, target) { + target.style.setProperty(property, isNeutralKeyframe(from) ? '' : from); + }, + nonInterpolationExpectations: function(from, to) { + return expectFlip(from, to, -Infinity); + }, + notAnimatableExpectations: function(from, to, underlying) { + return expectFlip(from, to, -Infinity); + }, + interpolate: function(property, from, to, at, target) { + // Force a style recalc on target to set the 'from' value. + getComputedStyle(target).getPropertyValue(property); + target.style.transitionDuration = '100s'; + target.style.transitionDelay = '-50s'; + target.style.transitionTimingFunction = createEasing(at); + target.style.transitionProperty = property; + target.style.setProperty(property, isNeutralKeyframe(to) ? '' : to); + }, + }; + + var cssTransitionAllInterpolation = { + name: 'CSS Transitions with transition: all', + isSupported: function() {return true;}, + // The 'all' value doesn't cover custom properties. + supportsProperty: function(property) {return property.indexOf('--') !== 0;}, + supportsValue: function() {return true;}, + setup: function(property, from, target) { + target.style.setProperty(property, isNeutralKeyframe(from) ? '' : from); + }, + nonInterpolationExpectations: function(from, to) { + return expectFlip(from, to, -Infinity); + }, + notAnimatableExpectations: function(from, to, underlying) { + return expectFlip(from, to, -Infinity); + }, + interpolate: function(property, from, to, at, target) { + // Force a style recalc on target to set the 'from' value. + getComputedStyle(target).getPropertyValue(property); + target.style.transitionDuration = '100s'; + target.style.transitionDelay = '-50s'; + target.style.transitionTimingFunction = createEasing(at); + target.style.transitionProperty = 'all'; + target.style.setProperty(property, isNeutralKeyframe(to) ? '' : to); + }, + }; + + var webAnimationsInterpolation = { + name: 'Web Animations', + isSupported: function() {return 'animate' in Element.prototype;}, + supportsProperty: function(property) {return true;}, + supportsValue: function(value) {return value !== '';}, + setup: function() {}, + nonInterpolationExpectations: function(from, to) { + return expectFlip(from, to, 0.5); + }, + notAnimatableExpectations: function(from, to, underlying) { + return expectFlip(underlying, underlying, -Infinity); + }, + interpolate: function(property, from, to, at, target) { + this.interpolateComposite(property, from, 'replace', to, 'replace', at, target); + }, + interpolateComposite: function(property, from, fromComposite, to, toComposite, at, target) { + // This case turns into a test error later on. + if (!this.isSupported()) + return; + + // Convert standard properties to camelCase. + if (!property.startsWith('--')) { + for (var i = property.length - 2; i > 0; --i) { + if (property[i] === '-') { + property = property.substring(0, i) + property[i + 1].toUpperCase() + property.substring(i + 2); + } + } + if (property === 'offset') { + property = 'cssOffset'; + } else if (property === 'float') { + property = 'cssFloat'; + } + } + var keyframes = []; + if (!isNeutralKeyframe(from)) { + keyframes.push({ + offset: 0, + composite: fromComposite, + [property]: from, + }); + } + if (!isNeutralKeyframe(to)) { + keyframes.push({ + offset: 1, + composite: toComposite, + [property]: to, + }); + } + var animation = target.animate(keyframes, { + fill: 'forwards', + duration: 100 * 1000, + easing: createEasing(at), + }); + animation.pause(); + animation.currentTime = 50 * 1000; + }, + }; + + function expectFlip(from, to, flipAt) { + return [-0.3, 0, 0.3, 0.5, 0.6, 1, 1.5].map(function(at) { + return { + at: at, + expect: at < flipAt ? from : to + }; + }); + } + + // Constructs a timing function which produces 'y' at x = 0.5 + function createEasing(y) { + if (y == 0) { + return 'steps(1, end)'; + } + if (y == 1) { + return 'steps(1, start)'; + } + if (y == 0.5) { + return 'linear'; + } + // Approximate using a bezier. + var b = (8 * y - 1) / 6; + return 'cubic-bezier(0, ' + b + ', 1, ' + b + ')'; + } + + function createElement(parent, tag, text) { + var element = document.createElement(tag || 'div'); + element.textContent = text || ''; + parent.appendChild(element); + return element; + } + + function createTargetContainer(parent, className) { + var targetContainer = createElement(parent); + targetContainer.classList.add('container'); + var template = document.querySelector('#target-template'); + if (template) { + targetContainer.appendChild(template.content.cloneNode(true)); + } + var target = targetContainer.querySelector('.target') || targetContainer; + target.classList.add('target', className); + target.parentElement.classList.add('parent'); + targetContainer.target = target; + return targetContainer; + } + + function roundNumbers(value) { + return value. + // Round numbers to two decimal places. + replace(/-?\d*\.\d+(e-?\d+)?/g, function(n) { + return (parseFloat(n).toFixed(2)). + replace(/\.\d+/, function(m) { + return m.replace(/0+$/, ''); + }). + replace(/\.$/, ''). + replace(/^-0$/, '0'); + }); + } + + var anchor = document.createElement('a'); + function sanitizeUrls(value) { + var matches = value.match(/url\("([^#][^\)]*)"\)/g); + if (matches !== null) { + for (var i = 0; i < matches.length; ++i) { + var url = /url\("([^#][^\)]*)"\)/g.exec(matches[i])[1]; + anchor.href = url; + anchor.pathname = '...' + anchor.pathname.substring(anchor.pathname.lastIndexOf('/')); + value = value.replace(matches[i], 'url(' + anchor.href + ')'); + } + } + return value; + } + + function normalizeValue(value) { + return roundNumbers(sanitizeUrls(value)). + // Place whitespace between tokens. + replace(/([\w\d.]+|[^\s])/g, '$1 '). + replace(/\s+/g, ' '); + } + + function stringify(text) { + if (!text.includes("'")) { + return `'${text}'`; + } + return `"${text.replace('"', '\\"')}"`; + } + + function keyframeText(keyframe) { + return isNeutralKeyframe(keyframe) ? 'neutral' : `[${keyframe}]`; + } + + function keyframeCode(keyframe) { + return isNeutralKeyframe(keyframe) ? 'neutralKeyframe' : `${stringify(keyframe)}`; + } + + function createInterpolationTestTargets(interpolationMethod, interpolationMethodContainer, interpolationTest) { + var property = interpolationTest.options.property; + var from = interpolationTest.options.from; + var to = interpolationTest.options.to; + var comparisonFunction = interpolationTest.options.comparisonFunction; + + if ((interpolationTest.options.method && interpolationTest.options.method != interpolationMethod.name) + || !interpolationMethod.supportsProperty(property) + || !interpolationMethod.supportsValue(from) + || !interpolationMethod.supportsValue(to)) { + return; + } + + var testText = `${interpolationMethod.name}: property <${property}> from ${keyframeText(from)} to ${keyframeText(to)}`; + var testContainer = createElement(interpolationMethodContainer, 'div'); + createElement(testContainer); + var expectations = interpolationTest.expectations; + var applyUnderlying = false; + if (expectations === expectNoInterpolation) { + expectations = interpolationMethod.nonInterpolationExpectations(from, to); + } else if (expectations === expectNotAnimatable) { + expectations = interpolationMethod.notAnimatableExpectations(from, to, interpolationTest.options.underlying); + applyUnderlying = true; + } + + // Setup a standard equality function if an override is not provided. + if (!comparisonFunction) { + comparisonFunction = (actual, expected) => { + assert_equals(normalizeValue(actual), normalizeValue(expected)); + }; + } + + return expectations.map(function(expectation) { + var actualTargetContainer = createTargetContainer(testContainer, 'actual'); + var expectedTargetContainer = createTargetContainer(testContainer, 'expected'); + var expectedProperties = expectation.option || expectation.expect; + if (typeof expectedProperties !== "object") { + expectedProperties = {[property]: expectedProperties}; + } + var target = actualTargetContainer.target; + if (applyUnderlying) { + let underlying = interpolationTest.options.underlying; + assert_true(typeof underlying !== 'undefined', '\'underlying\' value must be provided'); + assert_true(CSS.supports(property, underlying), '\'underlying\' value must be supported'); + target.style.setProperty(property, underlying); + } + interpolationMethod.setup(property, from, target); + target.interpolate = function() { + interpolationMethod.interpolate(property, from, to, expectation.at, target); + }; + target.measure = function() { + for (var [expectedProp, expectedStr] of Object.entries(expectedProperties)) { + if (!isNeutralKeyframe(expectedStr)) { + expectedTargetContainer.target.style.setProperty(expectedProp, expectedStr); + } + var expectedValue = getComputedStyle(expectedTargetContainer.target).getPropertyValue(expectedProp); + let testName = `${testText} at (${expectation.at}) should be [${sanitizeUrls(expectedStr)}]`; + if (property !== expectedProp) { + testName += ` for <${expectedProp}>`; + } + test(function() { + assert_true(interpolationMethod.isSupported(), `${interpolationMethod.name} should be supported`); + + if (from && from !== neutralKeyframe) { + assert_true(CSS.supports(property, from), '\'from\' value should be supported'); + } + if (to && to !== neutralKeyframe) { + assert_true(CSS.supports(property, to), '\'to\' value should be supported'); + } + if (typeof underlying !== 'undefined') { + assert_true(CSS.supports(property, underlying), '\'underlying\' value should be supported'); + } + + comparisonFunction( + getComputedStyle(target).getPropertyValue(expectedProp), + expectedValue); + }, testName); + } + }; + return target; + }); + } + + function createCompositionTestTargets(compositionContainer, compositionTest) { + var options = compositionTest.options; + var property = options.property; + var underlying = options.underlying; + var comparisonFunction = options.comparisonFunction; + var from = options.accumulateFrom || options.addFrom || options.replaceFrom; + var to = options.accumulateTo || options.addTo || options.replaceTo; + var fromComposite = 'accumulateFrom' in options ? 'accumulate' : 'addFrom' in options ? 'add' : 'replace'; + var toComposite = 'accumulateTo' in options ? 'accumulate' : 'addTo' in options ? 'add' : 'replace'; + const invalidFrom = 'addFrom' in options === 'replaceFrom' in options + && 'addFrom' in options === 'accumulateFrom' in options; + const invalidTo = 'addTo' in options === 'replaceTo' in options + && 'addTo' in options === 'accumulateTo' in options; + if (invalidFrom || invalidTo) { + test(function() { + assert_false(invalidFrom, 'Exactly one of accumulateFrom, addFrom, or replaceFrom must be specified'); + assert_false(invalidTo, 'Exactly one of accumulateTo, addTo, or replaceTo must be specified'); + }, `Composition tests must have valid setup`); + } + + var testText = `Compositing: property <${property}> underlying [${underlying}] from ${fromComposite} [${from}] to ${toComposite} [${to}]`; + var testContainer = createElement(compositionContainer, 'div'); + createElement(testContainer); + + // Setup a standard equality function if an override is not provided. + if (!comparisonFunction) { + comparisonFunction = (actual, expected) => { + assert_equals(normalizeValue(actual), normalizeValue(expected)); + }; + } + + return compositionTest.expectations.map(function(expectation) { + var actualTargetContainer = createTargetContainer(testContainer, 'actual'); + var expectedTargetContainer = createTargetContainer(testContainer, 'expected'); + var expectedStr = expectation.option || expectation.expect; + if (!isNeutralKeyframe(expectedStr)) { + expectedTargetContainer.target.style.setProperty(property, expectedStr); + } + var target = actualTargetContainer.target; + target.style.setProperty(property, underlying); + target.interpolate = function() { + webAnimationsInterpolation.interpolateComposite(property, from, fromComposite, to, toComposite, expectation.at, target); + }; + target.measure = function() { + var expectedValue = getComputedStyle(expectedTargetContainer.target).getPropertyValue(property); + test(function() { + + if (from && from !== neutralKeyframe) { + assert_true(CSS.supports(property, from), '\'from\' value should be supported'); + } + if (to && to !== neutralKeyframe) { + assert_true(CSS.supports(property, to), '\'to\' value should be supported'); + } + if (typeof underlying !== 'undefined') { + assert_true(CSS.supports(property, underlying), '\'underlying\' value should be supported'); + } + + comparisonFunction( + getComputedStyle(target).getPropertyValue(property), + expectedValue); + }, `${testText} at (${expectation.at}) should be [${sanitizeUrls(expectedStr)}]`); + }; + return target; + }); + } + + + + function createTestTargets(interpolationMethods, interpolationTests, compositionTests, container) { + var targets = []; + for (var interpolationMethod of interpolationMethods) { + var interpolationMethodContainer = createElement(container); + for (var interpolationTest of interpolationTests) { + if(!interpolationTest.options.target_names || + interpolationTest.options.target_names.includes(interpolationMethod.name)) { + [].push.apply(targets, createInterpolationTestTargets(interpolationMethod, interpolationMethodContainer, interpolationTest)); + } + } + } + var compositionContainer = createElement(container); + for (var compositionTest of compositionTests) { + [].push.apply(targets, createCompositionTestTargets(compositionContainer, compositionTest)); + } + return targets; + } + + function test_no_interpolation(options) { + test_interpolation(options, expectNoInterpolation); + } + function test_not_animatable(options) { + test_interpolation(options, expectNotAnimatable); + } + function create_tests() { + var interpolationMethods = [ + cssTransitionsInterpolation, + cssTransitionAllInterpolation, + cssAnimationsInterpolation, + webAnimationsInterpolation, + ]; + var container = createElement(document.body); + var targets = createTestTargets(interpolationMethods, interpolationTests, compositionTests, container); + // Separate interpolation and measurement into different phases to avoid O(n^2) of the number of targets. + for (var target of targets) { + target.interpolate(); + } + for (var target of targets) { + target.measure(); + } + container.remove(); + } + + function test_interpolation(options, expectations) { + interpolationTests.push({options, expectations}); + create_tests(); + interpolationTests = []; + } + function test_composition(options, expectations) { + compositionTests.push({options, expectations}); + create_tests(); + compositionTests = []; + } + window.test_interpolation = test_interpolation; + window.test_no_interpolation = test_no_interpolation; + window.test_not_animatable = test_not_animatable; + window.test_composition = test_composition; + window.neutralKeyframe = neutralKeyframe; + window.roundNumbers = roundNumbers; +})(); |