/* -*- 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 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: 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 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 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); } }