diff options
Diffstat (limited to 'dom/smil/test/smilTestUtils.js')
-rw-r--r-- | dom/smil/test/smilTestUtils.js | 1015 |
1 files changed, 1015 insertions, 0 deletions
diff --git a/dom/smil/test/smilTestUtils.js b/dom/smil/test/smilTestUtils.js new file mode 100644 index 0000000000..be270a39bf --- /dev/null +++ b/dom/smil/test/smilTestUtils.js @@ -0,0 +1,1015 @@ +/* -*- 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); + } + throw new Error(`Unexpected attribute value ${attr.attrType}`); + }, + + // 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); + } +} |