summaryrefslogtreecommitdiffstats
path: root/dom/smil/test/smilTestUtils.js
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /dom/smil/test/smilTestUtils.js
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'dom/smil/test/smilTestUtils.js')
-rw-r--r--dom/smil/test/smilTestUtils.js858
1 files changed, 858 insertions, 0 deletions
diff --git a/dom/smil/test/smilTestUtils.js b/dom/smil/test/smilTestUtils.js
new file mode 100644
index 000000000..2304d499b
--- /dev/null
+++ b/dom/smil/test/smilTestUtils.js
@@ -0,0 +1,858 @@
+/* -*- 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 : function()
+ {
+ return SMILUtil.getFirstElemWithTag("svg");
+ },
+
+ // Returns the first element in the document with the matching tag
+ getFirstElemWithTag : function(aTargetTag)
+ {
+ var elemList = document.getElementsByTagName(aTargetTag);
+ return (elemList.length == 0 ? null : elemList[0]);
+ },
+
+ // Simple wrapper for getComputedStyle
+ getComputedStyleSimple: function(elem, prop)
+ {
+ return window.getComputedStyle(elem, null).getPropertyValue(prop);
+ },
+
+ getAttributeValue: function(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 : function(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 : function() {
+ return "_motion";
+ },
+};
+
+
+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 : function(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 : function(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 : function(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 : function(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 : function() { return this._begin; },
+ getDur : function() { return this._dur; },
+ getEndTime : function() { return this._begin + this._dur; },
+ getFractionalTime : function(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 : function(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 : function(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 : function(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 : function(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 : function(aSeekList, aTargetElem, aTargetAttr)
+ {
+ var svg = document.getElementById("svg");
+ for (var i in aSeekList) {
+ var entry = aSeekList[i];
+ SMILUtil.getSVGRoot().setCurrentTime(entry[0]);
+ is(SMILUtil.getAttributeValue(aTargetElem, aTargetAttr),
+ entry[1], entry[2]);
+ }
+ },
+
+ // methods that expect to be overridden in subclasses
+ buildSeekListStatic : function(aAnimAttr, aBaseVal,
+ aTimeData, aReasonStatic) {},
+ buildSeekListAnimated : function(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 : function(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 : function(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 : function(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 : function(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 : function(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 : function(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 : function(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 : function(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 : function(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 : function(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 : function(aPathDescription)
+ {
+ var path = document.createElementNS(SVG_NS, "path");
+ path.setAttribute("d", aPathDescription);
+ path.setAttribute("id", MPATH_TARGET_ID);
+ return SMILUtil.getSVGRoot().appendChild(path);
+ },
+
+ createMpath : function(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 : function(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 : function(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 : function(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);
+ }
+}