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