mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-12-03 10:33:33 +00:00
351d147e2f
Differential Revision: https://phabricator.services.mozilla.com/D44149 --HG-- extra : moz-landing-system : lando
1015 lines
32 KiB
JavaScript
1015 lines
32 KiB
JavaScript
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
|
/* vim: set ts=2 sw=2 sts=2 et: */
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
// Note: Class syntax roughly based on:
|
|
// https://developer.mozilla.org/en/Core_JavaScript_1.5_Guide/Inheritance
|
|
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
const XLINK_NS = "http://www.w3.org/1999/xlink";
|
|
|
|
const MPATH_TARGET_ID = "smilTestUtilsTestingPath";
|
|
|
|
function extend(child, supertype) {
|
|
child.prototype.__proto__ = supertype.prototype;
|
|
}
|
|
|
|
// General Utility Methods
|
|
var SMILUtil = {
|
|
// Returns the first matched <svg> node in the document
|
|
getSVGRoot() {
|
|
return SMILUtil.getFirstElemWithTag("svg");
|
|
},
|
|
|
|
// Returns the first element in the document with the matching tag
|
|
getFirstElemWithTag(aTargetTag) {
|
|
var elemList = document.getElementsByTagName(aTargetTag);
|
|
return elemList.length == 0 ? null : elemList[0];
|
|
},
|
|
|
|
// Simple wrapper for getComputedStyle
|
|
getComputedStyleSimple(elem, prop) {
|
|
return window.getComputedStyle(elem).getPropertyValue(prop);
|
|
},
|
|
|
|
getAttributeValue(elem, attr) {
|
|
if (attr.attrName == SMILUtil.getMotionFakeAttributeName()) {
|
|
// Fake motion "attribute" -- "computed value" is the element's CTM
|
|
return elem.getCTM();
|
|
}
|
|
if (attr.attrType == "CSS") {
|
|
return SMILUtil.getComputedStyleWrapper(elem, attr.attrName);
|
|
}
|
|
if (attr.attrType == "XML") {
|
|
// XXXdholbert This is appropriate for mapped attributes, but not
|
|
// for other attributes.
|
|
return SMILUtil.getComputedStyleWrapper(elem, attr.attrName);
|
|
}
|
|
},
|
|
|
|
// Smart wrapper for getComputedStyle, which will generate a "fake" computed
|
|
// style for recognized shorthand properties (font, font-variant, overflow, marker)
|
|
getComputedStyleWrapper(elem, propName) {
|
|
// Special cases for shorthand properties (which aren't directly queriable
|
|
// via getComputedStyle)
|
|
var computedStyle;
|
|
if (propName == "font") {
|
|
var subProps = [
|
|
"font-style",
|
|
"font-variant-caps",
|
|
"font-weight",
|
|
"font-size",
|
|
"line-height",
|
|
"font-family",
|
|
];
|
|
for (var i in subProps) {
|
|
var subPropStyle = SMILUtil.getComputedStyleSimple(elem, subProps[i]);
|
|
if (subPropStyle) {
|
|
if (subProps[i] == "line-height") {
|
|
// There needs to be a "/" before line-height
|
|
subPropStyle = "/ " + subPropStyle;
|
|
}
|
|
if (!computedStyle) {
|
|
computedStyle = subPropStyle;
|
|
} else {
|
|
computedStyle = computedStyle + " " + subPropStyle;
|
|
}
|
|
}
|
|
}
|
|
} else if (propName == "font-variant") {
|
|
// xxx - this isn't completely correct but it's sufficient for what's
|
|
// being tested here
|
|
computedStyle = SMILUtil.getComputedStyleSimple(
|
|
elem,
|
|
"font-variant-caps"
|
|
);
|
|
} else if (propName == "marker") {
|
|
var subProps = ["marker-end", "marker-mid", "marker-start"];
|
|
for (var i in subProps) {
|
|
if (!computedStyle) {
|
|
computedStyle = SMILUtil.getComputedStyleSimple(elem, subProps[i]);
|
|
} else {
|
|
is(
|
|
computedStyle,
|
|
SMILUtil.getComputedStyleSimple(elem, subProps[i]),
|
|
"marker sub-properties should match each other " +
|
|
"(they shouldn't be individually set)"
|
|
);
|
|
}
|
|
}
|
|
} else if (propName == "overflow") {
|
|
var subProps = ["overflow-x", "overflow-y"];
|
|
for (var i in subProps) {
|
|
if (!computedStyle) {
|
|
computedStyle = SMILUtil.getComputedStyleSimple(elem, subProps[i]);
|
|
} else {
|
|
is(
|
|
computedStyle,
|
|
SMILUtil.getComputedStyleSimple(elem, subProps[i]),
|
|
"overflow sub-properties should match each other " +
|
|
"(they shouldn't be individually set)"
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
computedStyle = SMILUtil.getComputedStyleSimple(elem, propName);
|
|
}
|
|
return computedStyle;
|
|
},
|
|
|
|
getMotionFakeAttributeName() {
|
|
return "_motion";
|
|
},
|
|
|
|
// Return stripped px value from specified value.
|
|
stripPx: str => str.replace(/px\s*$/, ""),
|
|
};
|
|
|
|
var CTMUtil = {
|
|
CTM_COMPONENTS_ALL: ["a", "b", "c", "d", "e", "f"],
|
|
CTM_COMPONENTS_ROTATE: ["a", "b", "c", "d"],
|
|
|
|
// Function to generate a CTM Matrix from a "summary"
|
|
// (a 3-tuple containing [tX, tY, theta])
|
|
generateCTM(aCtmSummary) {
|
|
if (!aCtmSummary || aCtmSummary.length != 3) {
|
|
ok(false, "Unexpected CTM summary tuple length: " + aCtmSummary.length);
|
|
}
|
|
var tX = aCtmSummary[0];
|
|
var tY = aCtmSummary[1];
|
|
var theta = aCtmSummary[2];
|
|
var cosTheta = Math.cos(theta);
|
|
var sinTheta = Math.sin(theta);
|
|
var newCtm = {
|
|
a: cosTheta,
|
|
c: -sinTheta,
|
|
e: tX,
|
|
b: sinTheta,
|
|
d: cosTheta,
|
|
f: tY,
|
|
};
|
|
return newCtm;
|
|
},
|
|
|
|
/// Helper for isCtmEqual
|
|
isWithinDelta(aTestVal, aExpectedVal, aErrMsg, aIsTodo) {
|
|
var testFunc = aIsTodo ? todo : ok;
|
|
const delta = 0.00001; // allowing margin of error = 10^-5
|
|
ok(
|
|
aTestVal >= aExpectedVal - delta && aTestVal <= aExpectedVal + delta,
|
|
aErrMsg + " | got: " + aTestVal + ", expected: " + aExpectedVal
|
|
);
|
|
},
|
|
|
|
assertCTMEqual(aLeftCtm, aRightCtm, aComponentsToCheck, aErrMsg, aIsTodo) {
|
|
var foundCTMDifference = false;
|
|
for (var j in aComponentsToCheck) {
|
|
var curComponent = aComponentsToCheck[j];
|
|
if (!aIsTodo) {
|
|
CTMUtil.isWithinDelta(
|
|
aLeftCtm[curComponent],
|
|
aRightCtm[curComponent],
|
|
aErrMsg + " | component: " + curComponent,
|
|
false
|
|
);
|
|
} else if (aLeftCtm[curComponent] != aRightCtm[curComponent]) {
|
|
foundCTMDifference = true;
|
|
}
|
|
}
|
|
|
|
if (aIsTodo) {
|
|
todo(!foundCTMDifference, aErrMsg + " | (currently marked todo)");
|
|
}
|
|
},
|
|
|
|
assertCTMNotEqual(aLeftCtm, aRightCtm, aComponentsToCheck, aErrMsg, aIsTodo) {
|
|
// CTM should not match initial one
|
|
var foundCTMDifference = false;
|
|
for (var j in aComponentsToCheck) {
|
|
var curComponent = aComponentsToCheck[j];
|
|
if (aLeftCtm[curComponent] != aRightCtm[curComponent]) {
|
|
foundCTMDifference = true;
|
|
break; // We found a difference, as expected. Success!
|
|
}
|
|
}
|
|
|
|
if (aIsTodo) {
|
|
todo(foundCTMDifference, aErrMsg + " | (currently marked todo)");
|
|
} else {
|
|
ok(foundCTMDifference, aErrMsg);
|
|
}
|
|
},
|
|
};
|
|
|
|
// Wrapper for timing information
|
|
function SMILTimingData(aBegin, aDur) {
|
|
this._begin = aBegin;
|
|
this._dur = aDur;
|
|
}
|
|
SMILTimingData.prototype = {
|
|
_begin: null,
|
|
_dur: null,
|
|
getBeginTime() {
|
|
return this._begin;
|
|
},
|
|
getDur() {
|
|
return this._dur;
|
|
},
|
|
getEndTime() {
|
|
return this._begin + this._dur;
|
|
},
|
|
getFractionalTime(aPortion) {
|
|
return this._begin + aPortion * this._dur;
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Attribute: a container for information about an attribute we'll
|
|
* attempt to animate with SMIL in our tests.
|
|
*
|
|
* See also the factory methods below: NonAnimatableAttribute(),
|
|
* NonAdditiveAttribute(), and AdditiveAttribute().
|
|
*
|
|
* @param aAttrName The name of the attribute
|
|
* @param aAttrType The type of the attribute ("CSS" vs "XML")
|
|
* @param aTargetTag The name of an element that this attribute could be
|
|
* applied to.
|
|
* @param aIsAnimatable A bool indicating whether this attribute is defined as
|
|
* animatable in the SVG spec.
|
|
* @param aIsAdditive A bool indicating whether this attribute is defined as
|
|
* additive (i.e. supports "by" animation) in the SVG spec.
|
|
*/
|
|
function Attribute(
|
|
aAttrName,
|
|
aAttrType,
|
|
aTargetTag,
|
|
aIsAnimatable,
|
|
aIsAdditive
|
|
) {
|
|
this.attrName = aAttrName;
|
|
this.attrType = aAttrType;
|
|
this.targetTag = aTargetTag;
|
|
this.isAnimatable = aIsAnimatable;
|
|
this.isAdditive = aIsAdditive;
|
|
}
|
|
Attribute.prototype = {
|
|
// Member variables
|
|
attrName: null,
|
|
attrType: null,
|
|
isAnimatable: null,
|
|
testcaseList: null,
|
|
};
|
|
|
|
// Generators for Attribute objects. These allow lists of attribute
|
|
// definitions to be more human-readible than if we were using Attribute() with
|
|
// boolean flags, e.g. "Attribute(..., true, true), Attribute(..., true, false)
|
|
function NonAnimatableAttribute(aAttrName, aAttrType, aTargetTag) {
|
|
return new Attribute(aAttrName, aAttrType, aTargetTag, false, false);
|
|
}
|
|
function NonAdditiveAttribute(aAttrName, aAttrType, aTargetTag) {
|
|
return new Attribute(aAttrName, aAttrType, aTargetTag, true, false);
|
|
}
|
|
function AdditiveAttribute(aAttrName, aAttrType, aTargetTag) {
|
|
return new Attribute(aAttrName, aAttrType, aTargetTag, true, true);
|
|
}
|
|
|
|
/**
|
|
* TestcaseBundle: a container for a group of tests for a particular attribute
|
|
*
|
|
* @param aAttribute An Attribute object for the attribute
|
|
* @param aTestcaseList An array of AnimTestcase objects
|
|
*/
|
|
function TestcaseBundle(aAttribute, aTestcaseList, aSkipReason) {
|
|
this.animatedAttribute = aAttribute;
|
|
this.testcaseList = aTestcaseList;
|
|
this.skipReason = aSkipReason;
|
|
}
|
|
TestcaseBundle.prototype = {
|
|
// Member variables
|
|
animatedAttribute: null,
|
|
testcaseList: null,
|
|
skipReason: null,
|
|
|
|
// Methods
|
|
go(aTimingData) {
|
|
if (this.skipReason) {
|
|
todo(
|
|
false,
|
|
"Skipping a bundle for '" +
|
|
this.animatedAttribute.attrName +
|
|
"' because: " +
|
|
this.skipReason
|
|
);
|
|
} else {
|
|
// Sanity Check: Bundle should have > 0 testcases
|
|
if (!this.testcaseList || !this.testcaseList.length) {
|
|
ok(
|
|
false,
|
|
"a bundle for '" +
|
|
this.animatedAttribute.attrName +
|
|
"' has no testcases"
|
|
);
|
|
}
|
|
|
|
var targetElem = SMILUtil.getFirstElemWithTag(
|
|
this.animatedAttribute.targetTag
|
|
);
|
|
|
|
if (!targetElem) {
|
|
ok(
|
|
false,
|
|
"Error: can't find an element of type '" +
|
|
this.animatedAttribute.targetTag +
|
|
"', so I can't test property '" +
|
|
this.animatedAttribute.attrName +
|
|
"'"
|
|
);
|
|
return;
|
|
}
|
|
|
|
for (var testcaseIdx in this.testcaseList) {
|
|
var testcase = this.testcaseList[testcaseIdx];
|
|
if (testcase.skipReason) {
|
|
todo(
|
|
false,
|
|
"Skipping a testcase for '" +
|
|
this.animatedAttribute.attrName +
|
|
"' because: " +
|
|
testcase.skipReason
|
|
);
|
|
} else {
|
|
testcase.runTest(
|
|
targetElem,
|
|
this.animatedAttribute,
|
|
aTimingData,
|
|
false
|
|
);
|
|
testcase.runTest(
|
|
targetElem,
|
|
this.animatedAttribute,
|
|
aTimingData,
|
|
true
|
|
);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
};
|
|
|
|
/**
|
|
* AnimTestcase: an abstract class that represents an animation testcase.
|
|
* (e.g. a set of "from"/"to" values to test)
|
|
*/
|
|
function AnimTestcase() {} // abstract => no constructor
|
|
AnimTestcase.prototype = {
|
|
// Member variables
|
|
_animElementTagName: "animate", // Can be overridden for e.g. animateColor
|
|
computedValMap: null,
|
|
skipReason: null,
|
|
|
|
// Methods
|
|
/**
|
|
* runTest: Runs this AnimTestcase
|
|
*
|
|
* @param aTargetElem The node to be targeted in our test animation.
|
|
* @param aTargetAttr An Attribute object representing the attribute
|
|
* to be targeted in our test animation.
|
|
* @param aTimeData A SMILTimingData object with timing information for
|
|
* our test animation.
|
|
* @param aIsFreeze If true, indicates that our test animation should use
|
|
* fill="freeze"; otherwise, we'll default to fill="remove".
|
|
*/
|
|
runTest(aTargetElem, aTargetAttr, aTimeData, aIsFreeze) {
|
|
// SANITY CHECKS
|
|
if (!SMILUtil.getSVGRoot().animationsPaused()) {
|
|
ok(false, "Should start each test with animations paused");
|
|
}
|
|
if (SMILUtil.getSVGRoot().getCurrentTime() != 0) {
|
|
ok(false, "Should start each test at time = 0");
|
|
}
|
|
|
|
// SET UP
|
|
// Cache initial computed value
|
|
var baseVal = SMILUtil.getAttributeValue(aTargetElem, aTargetAttr);
|
|
|
|
// Create & append animation element
|
|
var anim = this.setupAnimationElement(aTargetAttr, aTimeData, aIsFreeze);
|
|
aTargetElem.appendChild(anim);
|
|
|
|
// Build a list of [seek-time, expectedValue, errorMessage] triplets
|
|
var seekList = this.buildSeekList(
|
|
aTargetAttr,
|
|
baseVal,
|
|
aTimeData,
|
|
aIsFreeze
|
|
);
|
|
|
|
// DO THE ACTUAL TESTING
|
|
this.seekAndTest(seekList, aTargetElem, aTargetAttr);
|
|
|
|
// CLEAN UP
|
|
aTargetElem.removeChild(anim);
|
|
SMILUtil.getSVGRoot().setCurrentTime(0);
|
|
},
|
|
|
|
// HELPER FUNCTIONS
|
|
// setupAnimationElement: <animate> element
|
|
// Subclasses should extend this parent method
|
|
setupAnimationElement(aAnimAttr, aTimeData, aIsFreeze) {
|
|
var animElement = document.createElementNS(
|
|
SVG_NS,
|
|
this._animElementTagName
|
|
);
|
|
animElement.setAttribute("attributeName", aAnimAttr.attrName);
|
|
animElement.setAttribute("attributeType", aAnimAttr.attrType);
|
|
animElement.setAttribute("begin", aTimeData.getBeginTime());
|
|
animElement.setAttribute("dur", aTimeData.getDur());
|
|
if (aIsFreeze) {
|
|
animElement.setAttribute("fill", "freeze");
|
|
}
|
|
return animElement;
|
|
},
|
|
|
|
buildSeekList(aAnimAttr, aBaseVal, aTimeData, aIsFreeze) {
|
|
if (!aAnimAttr.isAnimatable) {
|
|
return this.buildSeekListStatic(
|
|
aAnimAttr,
|
|
aBaseVal,
|
|
aTimeData,
|
|
"defined as non-animatable in SVG spec"
|
|
);
|
|
}
|
|
if (this.computedValMap.noEffect) {
|
|
return this.buildSeekListStatic(
|
|
aAnimAttr,
|
|
aBaseVal,
|
|
aTimeData,
|
|
"testcase specified to have no effect"
|
|
);
|
|
}
|
|
return this.buildSeekListAnimated(
|
|
aAnimAttr,
|
|
aBaseVal,
|
|
aTimeData,
|
|
aIsFreeze
|
|
);
|
|
},
|
|
|
|
seekAndTest(aSeekList, aTargetElem, aTargetAttr) {
|
|
var svg = document.getElementById("svg");
|
|
for (var i in aSeekList) {
|
|
var entry = aSeekList[i];
|
|
SMILUtil.getSVGRoot().setCurrentTime(entry[0]);
|
|
|
|
// Bug 1379908: The computed value of stroke-* properties should be
|
|
// serialized with px units, but currently Gecko and Servo don't do that
|
|
// when animating these values.
|
|
if (
|
|
["stroke-width", "stroke-dasharray", "stroke-dashoffset"].includes(
|
|
aTargetAttr.attrName
|
|
)
|
|
) {
|
|
var attr = SMILUtil.stripPx(
|
|
SMILUtil.getAttributeValue(aTargetElem, aTargetAttr)
|
|
);
|
|
var expectedVal = SMILUtil.stripPx(entry[1]);
|
|
is(attr, expectedVal, entry[2]);
|
|
return;
|
|
}
|
|
is(
|
|
SMILUtil.getAttributeValue(aTargetElem, aTargetAttr),
|
|
entry[1],
|
|
entry[2]
|
|
);
|
|
}
|
|
},
|
|
|
|
// methods that expect to be overridden in subclasses
|
|
buildSeekListStatic(aAnimAttr, aBaseVal, aTimeData, aReasonStatic) {},
|
|
buildSeekListAnimated(aAnimAttr, aBaseVal, aTimeData, aIsFreeze) {},
|
|
};
|
|
|
|
// Abstract parent class to share code between from-to & from-by testcases.
|
|
function AnimTestcaseFrom() {} // abstract => no constructor
|
|
AnimTestcaseFrom.prototype = {
|
|
// Member variables
|
|
from: null,
|
|
|
|
// Methods
|
|
setupAnimationElement(aAnimAttr, aTimeData, aIsFreeze) {
|
|
// Call super, and then add my own customization
|
|
var animElem = AnimTestcase.prototype.setupAnimationElement.apply(this, [
|
|
aAnimAttr,
|
|
aTimeData,
|
|
aIsFreeze,
|
|
]);
|
|
animElem.setAttribute("from", this.from);
|
|
return animElem;
|
|
},
|
|
|
|
buildSeekListStatic(aAnimAttr, aBaseVal, aTimeData, aReasonStatic) {
|
|
var seekList = new Array();
|
|
var msgPrefix =
|
|
aAnimAttr.attrName + ": shouldn't be affected by animation ";
|
|
seekList.push([
|
|
aTimeData.getBeginTime(),
|
|
aBaseVal,
|
|
msgPrefix + "(at animation begin) - " + aReasonStatic,
|
|
]);
|
|
seekList.push([
|
|
aTimeData.getFractionalTime(1 / 2),
|
|
aBaseVal,
|
|
msgPrefix + "(at animation mid) - " + aReasonStatic,
|
|
]);
|
|
seekList.push([
|
|
aTimeData.getEndTime(),
|
|
aBaseVal,
|
|
msgPrefix + "(at animation end) - " + aReasonStatic,
|
|
]);
|
|
seekList.push([
|
|
aTimeData.getEndTime() + aTimeData.getDur(),
|
|
aBaseVal,
|
|
msgPrefix + "(after animation end) - " + aReasonStatic,
|
|
]);
|
|
return seekList;
|
|
},
|
|
|
|
buildSeekListAnimated(aAnimAttr, aBaseVal, aTimeData, aIsFreeze) {
|
|
var seekList = new Array();
|
|
var msgPrefix = aAnimAttr.attrName + ": ";
|
|
if (aTimeData.getBeginTime() > 0.1) {
|
|
seekList.push([
|
|
aTimeData.getBeginTime() - 0.1,
|
|
aBaseVal,
|
|
msgPrefix +
|
|
"checking that base value is set " +
|
|
"before start of animation",
|
|
]);
|
|
}
|
|
|
|
seekList.push([
|
|
aTimeData.getBeginTime(),
|
|
this.computedValMap.fromComp || this.from,
|
|
msgPrefix +
|
|
"checking that 'from' value is set " +
|
|
"at start of animation",
|
|
]);
|
|
seekList.push([
|
|
aTimeData.getFractionalTime(1 / 2),
|
|
this.computedValMap.midComp || this.computedValMap.toComp || this.to,
|
|
msgPrefix + "checking value halfway through animation",
|
|
]);
|
|
|
|
var finalMsg;
|
|
var expectedEndVal;
|
|
if (aIsFreeze) {
|
|
expectedEndVal = this.computedValMap.toComp || this.to;
|
|
finalMsg = msgPrefix + "[freeze-mode] checking that final value is set ";
|
|
} else {
|
|
expectedEndVal = aBaseVal;
|
|
finalMsg =
|
|
msgPrefix + "[remove-mode] checking that animation is cleared ";
|
|
}
|
|
seekList.push([
|
|
aTimeData.getEndTime(),
|
|
expectedEndVal,
|
|
finalMsg + "at end of animation",
|
|
]);
|
|
seekList.push([
|
|
aTimeData.getEndTime() + aTimeData.getDur(),
|
|
expectedEndVal,
|
|
finalMsg + "after end of animation",
|
|
]);
|
|
return seekList;
|
|
},
|
|
};
|
|
extend(AnimTestcaseFrom, AnimTestcase);
|
|
|
|
/*
|
|
* A testcase for a simple "from-to" animation
|
|
* @param aFrom The 'from' value
|
|
* @param aTo The 'to' value
|
|
* @param aComputedValMap A hash-map that contains some computed values,
|
|
* if they're needed, as follows:
|
|
* - fromComp: Computed value version of |aFrom| (if different from |aFrom|)
|
|
* - midComp: Computed value that we expect to visit halfway through the
|
|
* animation (if different from |aTo|)
|
|
* - toComp: Computed value version of |aTo| (if different from |aTo|)
|
|
* - noEffect: Special flag -- if set, indicates that this testcase is
|
|
* expected to have no effect on the computed value. (e.g. the
|
|
* given values are invalid.)
|
|
* @param aSkipReason If this test-case is known to currently fail, this
|
|
* parameter should be a string explaining why.
|
|
* Otherwise, this value should be null (or omitted).
|
|
*
|
|
*/
|
|
function AnimTestcaseFromTo(aFrom, aTo, aComputedValMap, aSkipReason) {
|
|
this.from = aFrom;
|
|
this.to = aTo;
|
|
this.computedValMap = aComputedValMap || {}; // Let aComputedValMap be omitted
|
|
this.skipReason = aSkipReason;
|
|
}
|
|
AnimTestcaseFromTo.prototype = {
|
|
// Member variables
|
|
to: null,
|
|
|
|
// Methods
|
|
setupAnimationElement(aAnimAttr, aTimeData, aIsFreeze) {
|
|
// Call super, and then add my own customization
|
|
var animElem = AnimTestcaseFrom.prototype.setupAnimationElement.apply(
|
|
this,
|
|
[aAnimAttr, aTimeData, aIsFreeze]
|
|
);
|
|
animElem.setAttribute("to", this.to);
|
|
return animElem;
|
|
},
|
|
};
|
|
extend(AnimTestcaseFromTo, AnimTestcaseFrom);
|
|
|
|
/*
|
|
* A testcase for a simple "from-by" animation.
|
|
*
|
|
* @param aFrom The 'from' value
|
|
* @param aBy The 'by' value
|
|
* @param aComputedValMap A hash-map that contains some computed values that
|
|
* we expect to visit, as follows:
|
|
* - fromComp: Computed value version of |aFrom| (if different from |aFrom|)
|
|
* - midComp: Computed value that we expect to visit halfway through the
|
|
* animation (|aFrom| + |aBy|/2)
|
|
* - toComp: Computed value of the animation endpoint (|aFrom| + |aBy|)
|
|
* - noEffect: Special flag -- if set, indicates that this testcase is
|
|
* expected to have no effect on the computed value. (e.g. the
|
|
* given values are invalid. Or the attribute may be animatable
|
|
* and additive, but the particular "from" & "by" values that
|
|
* are used don't support addition.)
|
|
* @param aSkipReason If this test-case is known to currently fail, this
|
|
* parameter should be a string explaining why.
|
|
* Otherwise, this value should be null (or omitted).
|
|
*/
|
|
function AnimTestcaseFromBy(aFrom, aBy, aComputedValMap, aSkipReason) {
|
|
this.from = aFrom;
|
|
this.by = aBy;
|
|
this.computedValMap = aComputedValMap;
|
|
this.skipReason = aSkipReason;
|
|
if (
|
|
this.computedValMap &&
|
|
!this.computedValMap.noEffect &&
|
|
!this.computedValMap.toComp
|
|
) {
|
|
ok(false, "AnimTestcaseFromBy needs expected computed final value");
|
|
}
|
|
}
|
|
AnimTestcaseFromBy.prototype = {
|
|
// Member variables
|
|
by: null,
|
|
|
|
// Methods
|
|
setupAnimationElement(aAnimAttr, aTimeData, aIsFreeze) {
|
|
// Call super, and then add my own customization
|
|
var animElem = AnimTestcaseFrom.prototype.setupAnimationElement.apply(
|
|
this,
|
|
[aAnimAttr, aTimeData, aIsFreeze]
|
|
);
|
|
animElem.setAttribute("by", this.by);
|
|
return animElem;
|
|
},
|
|
buildSeekList(aAnimAttr, aBaseVal, aTimeData, aIsFreeze) {
|
|
if (!aAnimAttr.isAdditive) {
|
|
return this.buildSeekListStatic(
|
|
aAnimAttr,
|
|
aBaseVal,
|
|
aTimeData,
|
|
"defined as non-additive in SVG spec"
|
|
);
|
|
}
|
|
// Just use inherited method
|
|
return AnimTestcaseFrom.prototype.buildSeekList.apply(this, [
|
|
aAnimAttr,
|
|
aBaseVal,
|
|
aTimeData,
|
|
aIsFreeze,
|
|
]);
|
|
},
|
|
};
|
|
extend(AnimTestcaseFromBy, AnimTestcaseFrom);
|
|
|
|
/*
|
|
* A testcase for a "paced-mode" animation
|
|
* @param aValues An array of values, to be used as the "Values" list
|
|
* @param aComputedValMap A hash-map that contains some computed values,
|
|
* if they're needed, as follows:
|
|
* - comp0: The computed value at the start of the animation
|
|
* - comp1_6: The computed value exactly 1/6 through animation
|
|
* - comp1_3: The computed value exactly 1/3 through animation
|
|
* - comp2_3: The computed value exactly 2/3 through animation
|
|
* - comp1: The computed value of the animation endpoint
|
|
* The math works out easiest if...
|
|
* (a) aValuesString has 3 entries in its values list: vA, vB, vC
|
|
* (b) dist(vB, vC) = 2 * dist(vA, vB)
|
|
* With this setup, we can come up with expected intermediate values according
|
|
* to the following rules:
|
|
* - comp0 should be vA
|
|
* - comp1_6 should be us halfway between vA and vB
|
|
* - comp1_3 should be vB
|
|
* - comp2_3 should be halfway between vB and vC
|
|
* - comp1 should be vC
|
|
* @param aSkipReason If this test-case is known to currently fail, this
|
|
* parameter should be a string explaining why.
|
|
* Otherwise, this value should be null (or omitted).
|
|
*/
|
|
function AnimTestcasePaced(aValuesString, aComputedValMap, aSkipReason) {
|
|
this.valuesString = aValuesString;
|
|
this.computedValMap = aComputedValMap;
|
|
this.skipReason = aSkipReason;
|
|
if (
|
|
this.computedValMap &&
|
|
(!this.computedValMap.comp0 ||
|
|
!this.computedValMap.comp1_6 ||
|
|
!this.computedValMap.comp1_3 ||
|
|
!this.computedValMap.comp2_3 ||
|
|
!this.computedValMap.comp1)
|
|
) {
|
|
ok(false, "This AnimTestcasePaced has an incomplete computed value map");
|
|
}
|
|
}
|
|
AnimTestcasePaced.prototype = {
|
|
// Member variables
|
|
valuesString: null,
|
|
|
|
// Methods
|
|
setupAnimationElement(aAnimAttr, aTimeData, aIsFreeze) {
|
|
// Call super, and then add my own customization
|
|
var animElem = AnimTestcase.prototype.setupAnimationElement.apply(this, [
|
|
aAnimAttr,
|
|
aTimeData,
|
|
aIsFreeze,
|
|
]);
|
|
animElem.setAttribute("values", this.valuesString);
|
|
animElem.setAttribute("calcMode", "paced");
|
|
return animElem;
|
|
},
|
|
buildSeekListAnimated(aAnimAttr, aBaseVal, aTimeData, aIsFreeze) {
|
|
var seekList = new Array();
|
|
var msgPrefix = aAnimAttr.attrName + ": checking value ";
|
|
seekList.push([
|
|
aTimeData.getBeginTime(),
|
|
this.computedValMap.comp0,
|
|
msgPrefix + "at start of animation",
|
|
]);
|
|
seekList.push([
|
|
aTimeData.getFractionalTime(1 / 6),
|
|
this.computedValMap.comp1_6,
|
|
msgPrefix + "1/6 of the way through animation.",
|
|
]);
|
|
seekList.push([
|
|
aTimeData.getFractionalTime(1 / 3),
|
|
this.computedValMap.comp1_3,
|
|
msgPrefix + "1/3 of the way through animation.",
|
|
]);
|
|
seekList.push([
|
|
aTimeData.getFractionalTime(2 / 3),
|
|
this.computedValMap.comp2_3,
|
|
msgPrefix + "2/3 of the way through animation.",
|
|
]);
|
|
|
|
var finalMsg;
|
|
var expectedEndVal;
|
|
if (aIsFreeze) {
|
|
expectedEndVal = this.computedValMap.comp1;
|
|
finalMsg =
|
|
aAnimAttr.attrName +
|
|
": [freeze-mode] checking that final value is set ";
|
|
} else {
|
|
expectedEndVal = aBaseVal;
|
|
finalMsg =
|
|
aAnimAttr.attrName +
|
|
": [remove-mode] checking that animation is cleared ";
|
|
}
|
|
seekList.push([
|
|
aTimeData.getEndTime(),
|
|
expectedEndVal,
|
|
finalMsg + "at end of animation",
|
|
]);
|
|
seekList.push([
|
|
aTimeData.getEndTime() + aTimeData.getDur(),
|
|
expectedEndVal,
|
|
finalMsg + "after end of animation",
|
|
]);
|
|
return seekList;
|
|
},
|
|
buildSeekListStatic(aAnimAttr, aBaseVal, aTimeData, aReasonStatic) {
|
|
var seekList = new Array();
|
|
var msgPrefix =
|
|
aAnimAttr.attrName + ": shouldn't be affected by animation ";
|
|
seekList.push([
|
|
aTimeData.getBeginTime(),
|
|
aBaseVal,
|
|
msgPrefix + "(at animation begin) - " + aReasonStatic,
|
|
]);
|
|
seekList.push([
|
|
aTimeData.getFractionalTime(1 / 6),
|
|
aBaseVal,
|
|
msgPrefix + "(1/6 of the way through animation) - " + aReasonStatic,
|
|
]);
|
|
seekList.push([
|
|
aTimeData.getFractionalTime(1 / 3),
|
|
aBaseVal,
|
|
msgPrefix + "(1/3 of the way through animation) - " + aReasonStatic,
|
|
]);
|
|
seekList.push([
|
|
aTimeData.getFractionalTime(2 / 3),
|
|
aBaseVal,
|
|
msgPrefix + "(2/3 of the way through animation) - " + aReasonStatic,
|
|
]);
|
|
seekList.push([
|
|
aTimeData.getEndTime(),
|
|
aBaseVal,
|
|
msgPrefix + "(at animation end) - " + aReasonStatic,
|
|
]);
|
|
seekList.push([
|
|
aTimeData.getEndTime() + aTimeData.getDur(),
|
|
aBaseVal,
|
|
msgPrefix + "(after animation end) - " + aReasonStatic,
|
|
]);
|
|
return seekList;
|
|
},
|
|
};
|
|
extend(AnimTestcasePaced, AnimTestcase);
|
|
|
|
/*
|
|
* A testcase for an <animateMotion> animation.
|
|
*
|
|
* @param aAttrValueHash A hash-map mapping attribute names to values.
|
|
* Should include at least 'path', 'values', 'to'
|
|
* or 'by' to describe the motion path.
|
|
* @param aCtmMap A hash-map that contains summaries of the expected resulting
|
|
* CTM at various points during the animation. The CTM is
|
|
* summarized as a tuple of three numbers: [tX, tY, theta]
|
|
(indicating a translate(tX,tY) followed by a rotate(theta))
|
|
* - ctm0: The CTM summary at the start of the animation
|
|
* - ctm1_6: The CTM summary at exactly 1/6 through animation
|
|
* - ctm1_3: The CTM summary at exactly 1/3 through animation
|
|
* - ctm2_3: The CTM summary at exactly 2/3 through animation
|
|
* - ctm1: The CTM summary at the animation endpoint
|
|
*
|
|
* NOTE: For paced-mode animation (the default for animateMotion), the math
|
|
* works out easiest if:
|
|
* (a) our motion path has 3 points: vA, vB, vC
|
|
* (b) dist(vB, vC) = 2 * dist(vA, vB)
|
|
* (See discussion in header comment for AnimTestcasePaced.)
|
|
*
|
|
* @param aSkipReason If this test-case is known to currently fail, this
|
|
* parameter should be a string explaining why.
|
|
* Otherwise, this value should be null (or omitted).
|
|
*/
|
|
function AnimMotionTestcase(aAttrValueHash, aCtmMap, aSkipReason) {
|
|
this.attrValueHash = aAttrValueHash;
|
|
this.ctmMap = aCtmMap;
|
|
this.skipReason = aSkipReason;
|
|
if (
|
|
this.ctmMap &&
|
|
(!this.ctmMap.ctm0 ||
|
|
!this.ctmMap.ctm1_6 ||
|
|
!this.ctmMap.ctm1_3 ||
|
|
!this.ctmMap.ctm2_3 ||
|
|
!this.ctmMap.ctm1)
|
|
) {
|
|
ok(false, "This AnimMotionTestcase has an incomplete CTM map");
|
|
}
|
|
}
|
|
AnimMotionTestcase.prototype = {
|
|
// Member variables
|
|
_animElementTagName: "animateMotion",
|
|
|
|
// Implementations of inherited methods that we need to override:
|
|
// --------------------------------------------------------------
|
|
setupAnimationElement(aAnimAttr, aTimeData, aIsFreeze) {
|
|
var animElement = document.createElementNS(
|
|
SVG_NS,
|
|
this._animElementTagName
|
|
);
|
|
animElement.setAttribute("begin", aTimeData.getBeginTime());
|
|
animElement.setAttribute("dur", aTimeData.getDur());
|
|
if (aIsFreeze) {
|
|
animElement.setAttribute("fill", "freeze");
|
|
}
|
|
for (var attrName in this.attrValueHash) {
|
|
if (attrName == "mpath") {
|
|
this.createPath(this.attrValueHash[attrName]);
|
|
this.createMpath(animElement);
|
|
} else {
|
|
animElement.setAttribute(attrName, this.attrValueHash[attrName]);
|
|
}
|
|
}
|
|
return animElement;
|
|
},
|
|
|
|
createPath(aPathDescription) {
|
|
var path = document.createElementNS(SVG_NS, "path");
|
|
path.setAttribute("d", aPathDescription);
|
|
path.setAttribute("id", MPATH_TARGET_ID);
|
|
return SMILUtil.getSVGRoot().appendChild(path);
|
|
},
|
|
|
|
createMpath(aAnimElement) {
|
|
var mpath = document.createElementNS(SVG_NS, "mpath");
|
|
mpath.setAttributeNS(XLINK_NS, "href", "#" + MPATH_TARGET_ID);
|
|
return aAnimElement.appendChild(mpath);
|
|
},
|
|
|
|
// Override inherited seekAndTest method since...
|
|
// (a) it expects a computedValMap and we have a computed-CTM map instead
|
|
// and (b) it expects we might have no effect (for non-animatable attrs)
|
|
buildSeekList(aAnimAttr, aBaseVal, aTimeData, aIsFreeze) {
|
|
var seekList = new Array();
|
|
var msgPrefix = "CTM mismatch ";
|
|
seekList.push([
|
|
aTimeData.getBeginTime(),
|
|
CTMUtil.generateCTM(this.ctmMap.ctm0),
|
|
msgPrefix + "at start of animation",
|
|
]);
|
|
seekList.push([
|
|
aTimeData.getFractionalTime(1 / 6),
|
|
CTMUtil.generateCTM(this.ctmMap.ctm1_6),
|
|
msgPrefix + "1/6 of the way through animation.",
|
|
]);
|
|
seekList.push([
|
|
aTimeData.getFractionalTime(1 / 3),
|
|
CTMUtil.generateCTM(this.ctmMap.ctm1_3),
|
|
msgPrefix + "1/3 of the way through animation.",
|
|
]);
|
|
seekList.push([
|
|
aTimeData.getFractionalTime(2 / 3),
|
|
CTMUtil.generateCTM(this.ctmMap.ctm2_3),
|
|
msgPrefix + "2/3 of the way through animation.",
|
|
]);
|
|
|
|
var finalMsg;
|
|
var expectedEndVal;
|
|
if (aIsFreeze) {
|
|
expectedEndVal = CTMUtil.generateCTM(this.ctmMap.ctm1);
|
|
finalMsg =
|
|
aAnimAttr.attrName +
|
|
": [freeze-mode] checking that final value is set ";
|
|
} else {
|
|
expectedEndVal = aBaseVal;
|
|
finalMsg =
|
|
aAnimAttr.attrName +
|
|
": [remove-mode] checking that animation is cleared ";
|
|
}
|
|
seekList.push([
|
|
aTimeData.getEndTime(),
|
|
expectedEndVal,
|
|
finalMsg + "at end of animation",
|
|
]);
|
|
seekList.push([
|
|
aTimeData.getEndTime() + aTimeData.getDur(),
|
|
expectedEndVal,
|
|
finalMsg + "after end of animation",
|
|
]);
|
|
return seekList;
|
|
},
|
|
|
|
// Override inherited seekAndTest method
|
|
// (Have to use assertCTMEqual() instead of is() for comparison, to check each
|
|
// component of the CTM and to allow for a small margin of error.)
|
|
seekAndTest(aSeekList, aTargetElem, aTargetAttr) {
|
|
var svg = document.getElementById("svg");
|
|
for (var i in aSeekList) {
|
|
var entry = aSeekList[i];
|
|
SMILUtil.getSVGRoot().setCurrentTime(entry[0]);
|
|
CTMUtil.assertCTMEqual(
|
|
aTargetElem.getCTM(),
|
|
entry[1],
|
|
CTMUtil.CTM_COMPONENTS_ALL,
|
|
entry[2],
|
|
false
|
|
);
|
|
}
|
|
},
|
|
|
|
// Override "runTest" method so we can remove any <path> element that we
|
|
// created at the end of each test.
|
|
runTest(aTargetElem, aTargetAttr, aTimeData, aIsFreeze) {
|
|
AnimTestcase.prototype.runTest.apply(this, [
|
|
aTargetElem,
|
|
aTargetAttr,
|
|
aTimeData,
|
|
aIsFreeze,
|
|
]);
|
|
var pathElem = document.getElementById(MPATH_TARGET_ID);
|
|
if (pathElem) {
|
|
SMILUtil.getSVGRoot().removeChild(pathElem);
|
|
}
|
|
},
|
|
};
|
|
extend(AnimMotionTestcase, AnimTestcase);
|
|
|
|
// MAIN METHOD
|
|
function testBundleList(aBundleList, aTimingData) {
|
|
for (var bundleIdx in aBundleList) {
|
|
aBundleList[bundleIdx].go(aTimingData);
|
|
}
|
|
}
|