summaryrefslogtreecommitdiffstats
path: root/dom/smil/test/smilTestUtils.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--dom/smil/test/smilTestUtils.js1014
1 files changed, 1014 insertions, 0 deletions
diff --git a/dom/smil/test/smilTestUtils.js b/dom/smil/test/smilTestUtils.js
new file mode 100644
index 0000000000..8b6bddd4e1
--- /dev/null
+++ b/dom/smil/test/smilTestUtils.js
@@ -0,0 +1,1014 @@
+/* -*- 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 ? 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);
+ }
+}