blob: c98d8f66b1a50a6f779bddce24c5a5b3f7e20ea8 [file] [log] [blame]
/* Copyright 2015 The Chromium Authors. All rights reserved.
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*
* Exported function:
* - assertAttributeInterpolation({property, from, to, [fromComposite], [toComposite], [underlying]}, [{at: fraction, is: value}])
* Constructs a test case for each fraction that asserts the expected value
* equals the value produced by interpolation between from and to composited
* onto underlying by fromComposite and toComposite respectively using
* SMIL and Web Animations.
* Set from/to to the exported neutralKeyframe object to specify neutral keyframes.
* SMIL will only be tested with equal fromComposite and toComposite values.
*/
'use strict';
(() => {
var interpolationTests = [];
var neutralKeyframe = {};
// Set to true to output rebaselined test expectations.
var rebaselineTests = false;
function isNeutralKeyframe(keyframe) {
return keyframe === neutralKeyframe;
}
function createElement(tagName, container) {
var element = document.createElement(tagName);
if (container) {
container.appendChild(element);
}
return element;
}
// Constructs a timing function which produces 'y' at x = 0.5
function createEasing(y) {
// FIXME: if 'y' is > 0 and < 1 use a linear timing function and allow
// 'x' to vary. Use a bezier only for values < 0 or > 1.
if (y == 0) {
return 'steps(1, end)';
}
if (y == 1) {
return 'steps(1, start)';
}
if (y == 0.5) {
return 'steps(2, end)';
}
// Approximate using a bezier.
var b = (8 * y - 1) / 6;
return 'cubic-bezier(0, ' + b + ', 1, ' + b + ')';
}
function assertAttributeInterpolation(params, expectations) {
interpolationTests.push({params, expectations});
}
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');
});
}
function normalizeValue(value) {
return roundNumbers(value).
// Place whitespace between tokens.
replace(/([\w\d.]+|[^\s])/g, '$1 ').
replace(/\s+/g, ' ');
}
function createTarget(container) {
var targetContainer = createElement('div');
var template = document.querySelector('#target-template');
if (template) {
targetContainer.appendChild(template.content.cloneNode(true));
// Remove whitespace text nodes at start / end.
while (targetContainer.firstChild.nodeType != Node.ELEMENT_NODE && !/\S/.test(targetContainer.firstChild.nodeValue)) {
targetContainer.removeChild(targetContainer.firstChild);
}
while (targetContainer.lastChild.nodeType != Node.ELEMENT_NODE && !/\S/.test(targetContainer.lastChild.nodeValue)) {
targetContainer.removeChild(targetContainer.lastChild);
}
// If the template contains just one element, use that rather than a wrapper div.
if (targetContainer.children.length == 1 && targetContainer.childNodes.length == 1) {
targetContainer = targetContainer.firstChild;
}
container.appendChild(targetContainer);
}
var target = targetContainer.querySelector('.target') || targetContainer;
target.container = targetContainer;
return target;
}
var anchor = 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 serializeSVGLengthList(numberList) {
var elements = [];
for (var index = 0; index < numberList.numberOfItems; ++index)
elements.push(numberList.getItem(index).value);
return String(elements);
}
function serializeSVGNumberList(numberList) {
return Array.from(numberList).map(number => number.value).join(', ');
}
function serializeSVGPointList(pointList) {
var elements = [];
for (var index = 0; index < pointList.numberOfItems; ++index) {
var point = pointList.getItem(index);
elements.push(point.x);
elements.push(point.y);
}
return String(elements);
}
function serializeSVGPreserveAspectRatio(preserveAspectRatio) {
return String([preserveAspectRatio.align, preserveAspectRatio.meetOrSlice]);
}
function serializeSVGRect(rect) {
return [rect.x, rect.y, rect.width, rect.height].join(', ');
}
function serializeSVGTransformList(transformList) {
var elements = [];
for (var index = 0; index < transformList.numberOfItems; ++index) {
var transform = transformList.getItem(index);
elements.push(transform.type);
elements.push(transform.angle);
elements.push(transform.matrix.a);
elements.push(transform.matrix.b);
elements.push(transform.matrix.c);
elements.push(transform.matrix.d);
elements.push(transform.matrix.e);
elements.push(transform.matrix.f);
}
return String(elements);
}
var svgNamespace = 'http://www.w3.org/2000/svg';
var xlinkNamespace = 'http://www.w3.org/1999/xlink';
var animatedNumberOptionalNumberAttributes = [
'baseFrequency',
'kernelUnitLength',
'order',
'radius',
'stdDeviation',
];
function namespacedAttributeName(attributeName) {
if (attributeName === 'href')
return 'xlink:href';
return attributeName;
}
function getAttributeValue(element, attributeName) {
if (animatedNumberOptionalNumberAttributes.includes(attributeName))
return getAttributeValue(element, attributeName + 'X') + ', ' + getAttributeValue(element, attributeName + 'Y');
// The attribute 'class' is exposed in IDL as 'className'
if (attributeName === 'class')
attributeName = 'className';
// The attribute 'in' is exposed in IDL as 'in1'
if (attributeName === 'in')
attributeName = 'in1';
// The attribute 'orient' is exposed in IDL as 'orientType' and 'orientAngle'
if (attributeName === 'orient') {
if (element['orientType'] && element['orientType'].animVal === SVGMarkerElement.SVG_MARKER_ORIENT_AUTO)
return 'auto';
attributeName = 'orientAngle';
}
var result = null;
if (attributeName === 'd')
result = getComputedStyle(element).getPropertyValue('d');
else if (attributeName === 'points')
result = element['animatedPoints'];
else
result = element[attributeName].animVal;
if (result === null) {
if (attributeName === 'pathLength')
return '0';
if (attributeName === 'preserveAlpha')
return 'false';
console.error('Unknown attribute, cannot get ' + attributeName);
return null;
}
if (result instanceof SVGAngle || result instanceof SVGLength)
result = result.value;
else if (result instanceof SVGLengthList)
result = serializeSVGLengthList(result);
else if (result instanceof SVGNumberList)
result = serializeSVGNumberList(result);
else if (result instanceof SVGPointList)
result = serializeSVGPointList(result);
else if (result instanceof SVGPreserveAspectRatio)
result = serializeSVGPreserveAspectRatio(result);
else if (result instanceof SVGRect)
result = serializeSVGRect(result);
else if (result instanceof SVGTransformList)
result = serializeSVGTransformList(result);
if (typeof result !== 'string' && typeof result !== 'number' && typeof result !== 'boolean') {
console.error('Attribute value has unexpected type: ' + result);
}
return String(result);
}
function setAttributeValue(element, attributeName, expectation) {
if (!element[attributeName]
&& attributeName !== 'class'
&& attributeName !== 'd'
&& (attributeName !== 'in' || !element['in1'])
&& (attributeName !== 'orient' || !element['orientType'])
&& (animatedNumberOptionalNumberAttributes.indexOf(attributeName) === -1 || !element[attributeName + 'X'])) {
console.error('Unknown attribute, cannot set ' + attributeName);
return;
}
if (attributeName.toLowerCase().indexOf('transform') === -1) {
var setElement = document.createElementNS(svgNamespace, 'set');
setElement.setAttribute('attributeName', namespacedAttributeName(attributeName));
setElement.setAttribute('attributeType', 'XML');
setElement.setAttribute('to', expectation);
element.appendChild(setElement);
} else {
element.setAttribute(attributeName, expectation);
}
}
function createAnimateElement(attributeName, from, to, composite)
{
var animateElement;
if (attributeName.toLowerCase().includes('transform')) {
if (isNeutralKeyframe(from) || isNeutralKeyframe(to)) {
return null;
}
from = from.split(')');
to = to.split(')');
// Discard empty string at end.
from.pop();
to.pop();
// SMIL requires separate animateTransform elements for each transform in the list.
if (from.length !== 1 || to.length !== 1) {
return null;
}
from = from[0].split('(');
to = to[0].split('(');
if (from[0].trim() !== to[0].trim()) {
return null;
}
animateElement = document.createElementNS(svgNamespace, 'animateTransform');
animateElement.setAttribute('type', from[0].trim());
animateElement.setAttribute('from', from[1]);
animateElement.setAttribute('to', to[1]);
} else {
animateElement = document.createElementNS(svgNamespace, 'animate');
animateElement.setAttribute('from', from);
animateElement.setAttribute('to', to);
}
animateElement.setAttribute('attributeName', namespacedAttributeName(attributeName));
animateElement.setAttribute('attributeType', 'XML');
animateElement.setAttribute('begin', '0');
animateElement.setAttribute('dur', '1');
animateElement.setAttribute('fill', 'freeze');
animateElement.setAttribute('additive', composite === 'add' ? 'sum' : composite);
return animateElement;
}
function createTestTarget(method, description, container, params, expectation, rebaselineExpectation) {
var target = createTarget(container);
if (params.underlying) {
target.setAttribute(params.property, params.underlying);
}
var expected = createTarget(container);
setAttributeValue(expected, params.property, expectation.is);
target.interpolate = function() {
switch (method) {
case 'SMIL':
console.assert(params.fromComposite === params.toComposite);
var animateElement = createAnimateElement(params.property, params.from, params.to, params.fromComposite);
if (animateElement) {
target.appendChild(animateElement);
target.container.pauseAnimations();
target.container.setCurrentTime(expectation.at);
} else {
target.container.remove();
target.measure = function() {};
}
break;
case 'Web Animations':
// Replace 'transform' with 'svg-transform', etc. This avoids collisions with CSS properties or the Web Animations API (offset).
var prefixedProperty = 'svg-' + params.property;
var keyframes = [];
if (!isNeutralKeyframe(params.from)) {
keyframes.push({
offset: 0,
[prefixedProperty]: params.from,
composite: params.fromComposite,
});
}
if (!isNeutralKeyframe(params.to)) {
keyframes.push({
offset: 1,
[prefixedProperty]: params.to,
composite: params.toComposite,
});
}
target.animate(keyframes, {
fill: 'forwards',
duration: 1,
easing: createEasing(expectation.at),
delay: -0.5,
iterations: 0.5,
});
break;
default:
console.error('Unknown test method: ' + method);
}
};
target.measure = function() {
test(function() {
var actualResult = getAttributeValue(target, params.property);
if (rebaselineExpectation) {
var roundResult = roundNumbers(actualResult);
rebaselineExpectation.textContent += ` {at: ${expectation.at}, is: '${roundResult}'},\n`;
}
assert_equals(
normalizeValue(actualResult),
normalizeValue(getAttributeValue(expected, params.property)));
}, `${method}: ${description} at (${expectation.at}) is [${expectation.is}]`);
};
return target;
}
function createTestTargets(interpolationTests, container, rebaselineContainer) {
var targets = [];
for (var interpolationTest of interpolationTests) {
var params = interpolationTest.params;
assert_true('property' in params);
assert_true('from' in params);
assert_true('to' in params);
params.fromComposite = isNeutralKeyframe(params.from) ? 'add' : (params.fromComposite || 'replace');
params.toComposite = isNeutralKeyframe(params.to) ? 'add' : (params.toComposite || 'replace');
var underlyingText = params.underlying ? `with underlying [${params.underlying}] ` : '';
var fromText = isNeutralKeyframe(params.from) ? 'neutral' : `${params.fromComposite} [${params.from}]`;
var toText = isNeutralKeyframe(params.to) ? 'neutral' : `${params.toComposite} [${params.to}]`;
var description = `Interpolate attribute <${params.property}> ${underlyingText}from ${fromText} to ${toText}`;
if (rebaselineTests) {
var rebaseline = createElement('pre', rebaselineContainer);
var assertionCode =
`assertAttributeInterpolation({\n` +
` property: '${params.property}',\n` +
` underlying: '${params.underlying}',\n`;
if (isNeutralKeyframe(params.from)) {
assertionCode += ` from: neutralKeyframe,\n`;
} else {
assertionCode +=
` from: '${params.from}',\n` +
` fromComposite: '${params.fromComposite}',\n`;
}
if (isNeutralKeyframe(params.to)) {
assertionCode += ` to: neutralKeyframe,\n`;
} else {
assertionCode +=
` to: '${params.to}',\n` +
` toComposite: '${params.toComposite}',\n`;
}
assertionCode += `}, [\n`;
rebaseline.appendChild(document.createTextNode(assertionCode));
var rebaselineExpectation = document.createTextNode('');
rebaseline.appendChild(rebaselineExpectation);
rebaseline.appendChild(document.createTextNode(']);\n\n'));
}
for (var method of ['SMIL', 'Web Animations']) {
if (method === 'SMIL' && params.fromComposite !== params.toComposite) {
continue;
}
createElement('pre', container).textContent = `${method}: ${description}`;
var smilContainer = createElement('div', container);
for (var expectation of interpolationTest.expectations) {
if (method === 'SMIL' && (expectation.at < 0 || expectation.at > 1)) {
continue;
}
targets.push(createTestTarget(method, description, smilContainer, params, expectation, method === 'SMIL' ? null : rebaselineExpectation));
}
}
}
return targets;
}
function runTests() {
return new Promise((resolve) => {
var container = createElement('div', document.body);
var rebaselineContainer = createElement('pre', document.body);
var targets = createTestTargets(interpolationTests, container, rebaselineContainer);
requestAnimationFrame(() => {
for (var target of targets) {
target.interpolate();
}
requestAnimationFrame(() => {
for (var target of targets) {
target.measure();
}
if (window.testRunner) {
container.style.display = 'none';
}
resolve();
});
});
});
}
function loadScript(url) {
return new Promise(function(resolve) {
var script = createElement('script', document.head);
script.src = url;
script.onload = resolve;
});
}
loadScript('../../resources/testharness.js').then(() => {
return loadScript('../../resources/testharnessreport.js');
}).then(() => {
var asyncHandle = async_test('This test uses interpolation-test.js.')
requestAnimationFrame(() => {
runTests().then(() => asyncHandle.done());
});
});
window.assertAttributeInterpolation = assertAttributeInterpolation;
window.neutralKeyframe = neutralKeyframe;
})();