summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/css/support/interpolation-testcommon.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
commit43a97878ce14b72f0981164f87f2e35e14151312 (patch)
tree620249daf56c0258faa40cbdcf9cfba06de2a846 /testing/web-platform/tests/css/support/interpolation-testcommon.js
parentInitial commit. (diff)
downloadfirefox-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.js464
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;
+})();