1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/dom/smil/test/smilTestUtils.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,877 @@ 1.4 +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 1.5 +/* vim: set ts=2 sw=2 sts=2 et: */ 1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.9 + 1.10 +// Note: Class syntax roughly based on: 1.11 +// https://developer.mozilla.org/en/Core_JavaScript_1.5_Guide/Inheritance 1.12 +const SVG_NS = "http://www.w3.org/2000/svg"; 1.13 +const XLINK_NS = "http://www.w3.org/1999/xlink"; 1.14 + 1.15 +const MPATH_TARGET_ID = "smilTestUtilsTestingPath"; 1.16 + 1.17 +function extend(child, supertype) 1.18 +{ 1.19 + child.prototype.__proto__ = supertype.prototype; 1.20 +} 1.21 + 1.22 +// General Utility Methods 1.23 +var SMILUtil = 1.24 +{ 1.25 + // Returns the first matched <svg> node in the document 1.26 + getSVGRoot : function() 1.27 + { 1.28 + return SMILUtil.getFirstElemWithTag("svg"); 1.29 + }, 1.30 + 1.31 + // Returns the first element in the document with the matching tag 1.32 + getFirstElemWithTag : function(aTargetTag) 1.33 + { 1.34 + var elemList = document.getElementsByTagName(aTargetTag); 1.35 + return (elemList.length == 0 ? null : elemList[0]); 1.36 + }, 1.37 + 1.38 + // Simple wrapper for getComputedStyle 1.39 + getComputedStyleSimple: function(elem, prop) 1.40 + { 1.41 + return window.getComputedStyle(elem, null).getPropertyValue(prop); 1.42 + }, 1.43 + 1.44 + getAttributeValue: function(elem, attr) 1.45 + { 1.46 + if (attr.attrName == SMILUtil.getMotionFakeAttributeName()) { 1.47 + // Fake motion "attribute" -- "computed value" is the element's CTM 1.48 + return elem.getCTM(); 1.49 + } 1.50 + if (attr.attrType == "CSS") { 1.51 + return SMILUtil.getComputedStyleWrapper(elem, attr.attrName); 1.52 + } 1.53 + if (attr.attrType == "XML") { 1.54 + // XXXdholbert This is appropriate for mapped attributes, but not 1.55 + // for other attributes. 1.56 + return SMILUtil.getComputedStyleWrapper(elem, attr.attrName); 1.57 + } 1.58 + }, 1.59 + 1.60 + // Smart wrapper for getComputedStyle, which will generate a "fake" computed 1.61 + // style for recognized shorthand properties (font, overflow, marker) 1.62 + getComputedStyleWrapper : function(elem, propName) 1.63 + { 1.64 + // Special cases for shorthand properties (which aren't directly queriable 1.65 + // via getComputedStyle) 1.66 + var computedStyle; 1.67 + if (propName == "font") { 1.68 + var subProps = ["font-style", "font-variant", "font-weight", 1.69 + "font-size", "line-height", "font-family"]; 1.70 + for (var i in subProps) { 1.71 + var subPropStyle = SMILUtil.getComputedStyleSimple(elem, subProps[i]); 1.72 + if (subPropStyle) { 1.73 + if (subProps[i] == "line-height") { 1.74 + // There needs to be a "/" before line-height 1.75 + subPropStyle = "/ " + subPropStyle; 1.76 + } 1.77 + if (!computedStyle) { 1.78 + computedStyle = subPropStyle; 1.79 + } else { 1.80 + computedStyle = computedStyle + " " + subPropStyle; 1.81 + } 1.82 + } 1.83 + } 1.84 + } else if (propName == "marker") { 1.85 + var subProps = ["marker-end", "marker-mid", "marker-start"]; 1.86 + for (var i in subProps) { 1.87 + if (!computedStyle) { 1.88 + computedStyle = SMILUtil.getComputedStyleSimple(elem, subProps[i]); 1.89 + } else { 1.90 + is(computedStyle, SMILUtil.getComputedStyleSimple(elem, subProps[i]), 1.91 + "marker sub-properties should match each other " + 1.92 + "(they shouldn't be individually set)"); 1.93 + } 1.94 + } 1.95 + } else if (propName == "overflow") { 1.96 + var subProps = ["overflow-x", "overflow-y"]; 1.97 + for (var i in subProps) { 1.98 + if (!computedStyle) { 1.99 + computedStyle = SMILUtil.getComputedStyleSimple(elem, subProps[i]); 1.100 + } else { 1.101 + is(computedStyle, SMILUtil.getComputedStyleSimple(elem, subProps[i]), 1.102 + "overflow sub-properties should match each other " + 1.103 + "(they shouldn't be individually set)"); 1.104 + } 1.105 + } 1.106 + } else { 1.107 + computedStyle = SMILUtil.getComputedStyleSimple(elem, propName); 1.108 + } 1.109 + return computedStyle; 1.110 + }, 1.111 + 1.112 + // This method hides (i.e. sets "display: none" on) all of the given node's 1.113 + // descendents. It also hides the node itself, if requested. 1.114 + hideSubtree : function(node, hideNodeItself, useXMLAttribute) 1.115 + { 1.116 + // Hide node, if requested 1.117 + if (hideNodeItself) { 1.118 + if (useXMLAttribute) { 1.119 + if (node.setAttribute) { 1.120 + node.setAttribute("display", "none"); 1.121 + } 1.122 + } else if (node.style) { 1.123 + node.style.display = "none"; 1.124 + } 1.125 + } 1.126 + 1.127 + // Hide node's descendents 1.128 + var child = node.firstChild; 1.129 + while (child) { 1.130 + SMILUtil.hideSubtree(child, true, useXMLAttribute); 1.131 + child = child.nextSibling; 1.132 + } 1.133 + }, 1.134 + 1.135 + getMotionFakeAttributeName : function() { 1.136 + return "_motion"; 1.137 + }, 1.138 +}; 1.139 + 1.140 + 1.141 +var CTMUtil = 1.142 +{ 1.143 + CTM_COMPONENTS_ALL : ["a", "b", "c", "d", "e", "f"], 1.144 + CTM_COMPONENTS_ROTATE : ["a", "b", "c", "d" ], 1.145 + 1.146 + // Function to generate a CTM Matrix from a "summary" 1.147 + // (a 3-tuple containing [tX, tY, theta]) 1.148 + generateCTM : function(aCtmSummary) 1.149 + { 1.150 + if (!aCtmSummary || aCtmSummary.length != 3) { 1.151 + ok(false, "Unexpected CTM summary tuple length: " + aCtmSummary.length); 1.152 + } 1.153 + var tX = aCtmSummary[0]; 1.154 + var tY = aCtmSummary[1]; 1.155 + var theta = aCtmSummary[2]; 1.156 + var cosTheta = Math.cos(theta); 1.157 + var sinTheta = Math.sin(theta); 1.158 + var newCtm = { a : cosTheta, c: -sinTheta, e: tX, 1.159 + b : sinTheta, d: cosTheta, f: tY }; 1.160 + return newCtm; 1.161 + }, 1.162 + 1.163 + /// Helper for isCtmEqual 1.164 + isWithinDelta : function(aTestVal, aExpectedVal, aErrMsg, aIsTodo) { 1.165 + var testFunc = aIsTodo ? todo : ok; 1.166 + const delta = 0.00001; // allowing margin of error = 10^-5 1.167 + ok(aTestVal >= aExpectedVal - delta && 1.168 + aTestVal <= aExpectedVal + delta, 1.169 + aErrMsg + " | got: " + aTestVal + ", expected: " + aExpectedVal); 1.170 + }, 1.171 + 1.172 + assertCTMEqual : function(aLeftCtm, aRightCtm, aComponentsToCheck, 1.173 + aErrMsg, aIsTodo) { 1.174 + var foundCTMDifference = false; 1.175 + for (var j in aComponentsToCheck) { 1.176 + var curComponent = aComponentsToCheck[j]; 1.177 + if (!aIsTodo) { 1.178 + CTMUtil.isWithinDelta(aLeftCtm[curComponent], aRightCtm[curComponent], 1.179 + aErrMsg + " | component: " + curComponent, false); 1.180 + } else if (aLeftCtm[curComponent] != aRightCtm[curComponent]) { 1.181 + foundCTMDifference = true; 1.182 + } 1.183 + } 1.184 + 1.185 + if (aIsTodo) { 1.186 + todo(!foundCTMDifference, aErrMsg + " | (currently marked todo)"); 1.187 + } 1.188 + }, 1.189 + 1.190 + assertCTMNotEqual : function(aLeftCtm, aRightCtm, aComponentsToCheck, 1.191 + aErrMsg, aIsTodo) { 1.192 + // CTM should not match initial one 1.193 + var foundCTMDifference = false; 1.194 + for (var j in aComponentsToCheck) { 1.195 + var curComponent = aComponentsToCheck[j]; 1.196 + if (aLeftCtm[curComponent] != aRightCtm[curComponent]) { 1.197 + foundCTMDifference = true; 1.198 + break; // We found a difference, as expected. Success! 1.199 + } 1.200 + } 1.201 + 1.202 + if (aIsTodo) { 1.203 + todo(foundCTMDifference, aErrMsg + " | (currently marked todo)"); 1.204 + } else { 1.205 + ok(foundCTMDifference, aErrMsg); 1.206 + } 1.207 + }, 1.208 +}; 1.209 + 1.210 + 1.211 +// Wrapper for timing information 1.212 +function SMILTimingData(aBegin, aDur) 1.213 +{ 1.214 + this._begin = aBegin; 1.215 + this._dur = aDur; 1.216 +} 1.217 +SMILTimingData.prototype = 1.218 +{ 1.219 + _begin: null, 1.220 + _dur: null, 1.221 + getBeginTime : function() { return this._begin; }, 1.222 + getDur : function() { return this._dur; }, 1.223 + getEndTime : function() { return this._begin + this._dur; }, 1.224 + getFractionalTime : function(aPortion) 1.225 + { 1.226 + return this._begin + aPortion * this._dur; 1.227 + }, 1.228 +} 1.229 + 1.230 +/** 1.231 + * Attribute: a container for information about an attribute we'll 1.232 + * attempt to animate with SMIL in our tests. 1.233 + * 1.234 + * See also the factory methods below: NonAnimatableAttribute(), 1.235 + * NonAdditiveAttribute(), and AdditiveAttribute(). 1.236 + * 1.237 + * @param aAttrName The name of the attribute 1.238 + * @param aAttrType The type of the attribute ("CSS" vs "XML") 1.239 + * @param aTargetTag The name of an element that this attribute could be 1.240 + * applied to. 1.241 + * @param aIsAnimatable A bool indicating whether this attribute is defined as 1.242 + * animatable in the SVG spec. 1.243 + * @param aIsAdditive A bool indicating whether this attribute is defined as 1.244 + * additive (i.e. supports "by" animation) in the SVG spec. 1.245 + */ 1.246 +function Attribute(aAttrName, aAttrType, aTargetTag, 1.247 + aIsAnimatable, aIsAdditive) 1.248 +{ 1.249 + this.attrName = aAttrName; 1.250 + this.attrType = aAttrType; 1.251 + this.targetTag = aTargetTag; 1.252 + this.isAnimatable = aIsAnimatable; 1.253 + this.isAdditive = aIsAdditive; 1.254 +} 1.255 +Attribute.prototype = 1.256 +{ 1.257 + // Member variables 1.258 + attrName : null, 1.259 + attrType : null, 1.260 + isAnimatable : null, 1.261 + testcaseList : null, 1.262 +}; 1.263 + 1.264 +// Generators for Attribute objects. These allow lists of attribute 1.265 +// definitions to be more human-readible than if we were using Attribute() with 1.266 +// boolean flags, e.g. "Attribute(..., true, true), Attribute(..., true, false) 1.267 +function NonAnimatableAttribute(aAttrName, aAttrType, aTargetTag) 1.268 +{ 1.269 + return new Attribute(aAttrName, aAttrType, aTargetTag, false, false); 1.270 +} 1.271 +function NonAdditiveAttribute(aAttrName, aAttrType, aTargetTag) 1.272 +{ 1.273 + return new Attribute(aAttrName, aAttrType, aTargetTag, true, false); 1.274 +} 1.275 +function AdditiveAttribute(aAttrName, aAttrType, aTargetTag) 1.276 +{ 1.277 + return new Attribute(aAttrName, aAttrType, aTargetTag, true, true); 1.278 +} 1.279 + 1.280 +/** 1.281 + * TestcaseBundle: a container for a group of tests for a particular attribute 1.282 + * 1.283 + * @param aAttribute An Attribute object for the attribute 1.284 + * @param aTestcaseList An array of AnimTestcase objects 1.285 + */ 1.286 +function TestcaseBundle(aAttribute, aTestcaseList, aSkipReason) 1.287 +{ 1.288 + this.animatedAttribute = aAttribute; 1.289 + this.testcaseList = aTestcaseList; 1.290 + this.skipReason = aSkipReason; 1.291 +} 1.292 +TestcaseBundle.prototype = 1.293 +{ 1.294 + // Member variables 1.295 + animatedAttribute : null, 1.296 + testcaseList : null, 1.297 + skipReason : null, 1.298 + 1.299 + // Methods 1.300 + go : function(aTimingData) { 1.301 + if (this.skipReason) { 1.302 + todo(false, "Skipping a bundle for '" + this.animatedAttribute.attrName + 1.303 + "' because: " + this.skipReason); 1.304 + } else { 1.305 + // Sanity Check: Bundle should have > 0 testcases 1.306 + if (!this.testcaseList || !this.testcaseList.length) { 1.307 + ok(false, "a bundle for '" + this.animatedAttribute.attrName + 1.308 + "' has no testcases"); 1.309 + } 1.310 + 1.311 + var targetElem = 1.312 + SMILUtil.getFirstElemWithTag(this.animatedAttribute.targetTag); 1.313 + 1.314 + if (!targetElem) { 1.315 + ok(false, "Error: can't find an element of type '" + 1.316 + this.animatedAttribute.targetTag + 1.317 + "', so I can't test property '" + 1.318 + this.animatedAttribute.attrName + "'"); 1.319 + return; 1.320 + } 1.321 + 1.322 + for (var testcaseIdx in this.testcaseList) { 1.323 + var testcase = this.testcaseList[testcaseIdx]; 1.324 + if (testcase.skipReason) { 1.325 + todo(false, "Skipping a testcase for '" + 1.326 + this.animatedAttribute.attrName + 1.327 + "' because: " + testcase.skipReason); 1.328 + } else { 1.329 + testcase.runTest(targetElem, this.animatedAttribute, 1.330 + aTimingData, false); 1.331 + testcase.runTest(targetElem, this.animatedAttribute, 1.332 + aTimingData, true); 1.333 + } 1.334 + } 1.335 + } 1.336 + }, 1.337 +}; 1.338 + 1.339 +/** 1.340 + * AnimTestcase: an abstract class that represents an animation testcase. 1.341 + * (e.g. a set of "from"/"to" values to test) 1.342 + */ 1.343 +function AnimTestcase() {} // abstract => no constructor 1.344 +AnimTestcase.prototype = 1.345 +{ 1.346 + // Member variables 1.347 + _animElementTagName : "animate", // Can be overridden for e.g. animateColor 1.348 + computedValMap : null, 1.349 + skipReason : null, 1.350 + 1.351 + // Methods 1.352 + /** 1.353 + * runTest: Runs this AnimTestcase 1.354 + * 1.355 + * @param aTargetElem The node to be targeted in our test animation. 1.356 + * @param aTargetAttr An Attribute object representing the attribute 1.357 + * to be targeted in our test animation. 1.358 + * @param aTimeData A SMILTimingData object with timing information for 1.359 + * our test animation. 1.360 + * @param aIsFreeze If true, indicates that our test animation should use 1.361 + * fill="freeze"; otherwise, we'll default to fill="remove". 1.362 + */ 1.363 + runTest : function(aTargetElem, aTargetAttr, aTimeData, aIsFreeze) 1.364 + { 1.365 + // SANITY CHECKS 1.366 + if (!SMILUtil.getSVGRoot().animationsPaused()) { 1.367 + ok(false, "Should start each test with animations paused"); 1.368 + } 1.369 + if (SMILUtil.getSVGRoot().getCurrentTime() != 0) { 1.370 + ok(false, "Should start each test at time = 0"); 1.371 + } 1.372 + 1.373 + // SET UP 1.374 + // Cache initial computed value 1.375 + var baseVal = SMILUtil.getAttributeValue(aTargetElem, aTargetAttr); 1.376 + 1.377 + // Create & append animation element 1.378 + var anim = this.setupAnimationElement(aTargetAttr, aTimeData, aIsFreeze); 1.379 + aTargetElem.appendChild(anim); 1.380 + 1.381 + // Build a list of [seek-time, expectedValue, errorMessage] triplets 1.382 + var seekList = this.buildSeekList(aTargetAttr, baseVal, aTimeData, aIsFreeze); 1.383 + 1.384 + // DO THE ACTUAL TESTING 1.385 + this.seekAndTest(seekList, aTargetElem, aTargetAttr); 1.386 + 1.387 + // CLEAN UP 1.388 + aTargetElem.removeChild(anim); 1.389 + SMILUtil.getSVGRoot().setCurrentTime(0); 1.390 + }, 1.391 + 1.392 + // HELPER FUNCTIONS 1.393 + // setupAnimationElement: <animate> element 1.394 + // Subclasses should extend this parent method 1.395 + setupAnimationElement : function(aAnimAttr, aTimeData, aIsFreeze) 1.396 + { 1.397 + var animElement = document.createElementNS(SVG_NS, 1.398 + this._animElementTagName); 1.399 + animElement.setAttribute("attributeName", aAnimAttr.attrName); 1.400 + animElement.setAttribute("attributeType", aAnimAttr.attrType); 1.401 + animElement.setAttribute("begin", aTimeData.getBeginTime()); 1.402 + animElement.setAttribute("dur", aTimeData.getDur()); 1.403 + if (aIsFreeze) { 1.404 + animElement.setAttribute("fill", "freeze"); 1.405 + } 1.406 + return animElement; 1.407 + }, 1.408 + 1.409 + buildSeekList : function(aAnimAttr, aBaseVal, aTimeData, aIsFreeze) 1.410 + { 1.411 + if (!aAnimAttr.isAnimatable) { 1.412 + return this.buildSeekListStatic(aAnimAttr, aBaseVal, aTimeData, 1.413 + "defined as non-animatable in SVG spec"); 1.414 + } 1.415 + if (this.computedValMap.noEffect) { 1.416 + return this.buildSeekListStatic(aAnimAttr, aBaseVal, aTimeData, 1.417 + "testcase specified to have no effect"); 1.418 + } 1.419 + return this.buildSeekListAnimated(aAnimAttr, aBaseVal, 1.420 + aTimeData, aIsFreeze) 1.421 + }, 1.422 + 1.423 + seekAndTest : function(aSeekList, aTargetElem, aTargetAttr) 1.424 + { 1.425 + var svg = document.getElementById("svg"); 1.426 + for (var i in aSeekList) { 1.427 + var entry = aSeekList[i]; 1.428 + SMILUtil.getSVGRoot().setCurrentTime(entry[0]); 1.429 + is(SMILUtil.getAttributeValue(aTargetElem, aTargetAttr), 1.430 + entry[1], entry[2]); 1.431 + } 1.432 + }, 1.433 + 1.434 + // methods that expect to be overridden in subclasses 1.435 + buildSeekListStatic : function(aAnimAttr, aBaseVal, 1.436 + aTimeData, aReasonStatic) {}, 1.437 + buildSeekListAnimated : function(aAnimAttr, aBaseVal, 1.438 + aTimeData, aIsFreeze) {}, 1.439 +}; 1.440 + 1.441 + 1.442 +// Abstract parent class to share code between from-to & from-by testcases. 1.443 +function AnimTestcaseFrom() {} // abstract => no constructor 1.444 +AnimTestcaseFrom.prototype = 1.445 +{ 1.446 + // Member variables 1.447 + from : null, 1.448 + 1.449 + // Methods 1.450 + setupAnimationElement : function(aAnimAttr, aTimeData, aIsFreeze) 1.451 + { 1.452 + // Call super, and then add my own customization 1.453 + var animElem = AnimTestcase.prototype.setupAnimationElement.apply(this, 1.454 + [aAnimAttr, aTimeData, aIsFreeze]); 1.455 + animElem.setAttribute("from", this.from) 1.456 + return animElem; 1.457 + }, 1.458 + 1.459 + buildSeekListStatic : function(aAnimAttr, aBaseVal, aTimeData, aReasonStatic) 1.460 + { 1.461 + var seekList = new Array(); 1.462 + var msgPrefix = aAnimAttr.attrName + 1.463 + ": shouldn't be affected by animation "; 1.464 + seekList.push([aTimeData.getBeginTime(), aBaseVal, 1.465 + msgPrefix + "(at animation begin) - " + aReasonStatic]); 1.466 + seekList.push([aTimeData.getFractionalTime(1/2), aBaseVal, 1.467 + msgPrefix + "(at animation mid) - " + aReasonStatic]); 1.468 + seekList.push([aTimeData.getEndTime(), aBaseVal, 1.469 + msgPrefix + "(at animation end) - " + aReasonStatic]); 1.470 + seekList.push([aTimeData.getEndTime() + aTimeData.getDur(), aBaseVal, 1.471 + msgPrefix + "(after animation end) - " + aReasonStatic]); 1.472 + return seekList; 1.473 + }, 1.474 + 1.475 + buildSeekListAnimated : function(aAnimAttr, aBaseVal, aTimeData, aIsFreeze) 1.476 + { 1.477 + var seekList = new Array(); 1.478 + var msgPrefix = aAnimAttr.attrName + ": "; 1.479 + if (aTimeData.getBeginTime() > 0.1) { 1.480 + seekList.push([aTimeData.getBeginTime() - 0.1, 1.481 + aBaseVal, 1.482 + msgPrefix + "checking that base value is set " + 1.483 + "before start of animation"]); 1.484 + } 1.485 + 1.486 + seekList.push([aTimeData.getBeginTime(), 1.487 + this.computedValMap.fromComp || this.from, 1.488 + msgPrefix + "checking that 'from' value is set " + 1.489 + "at start of animation"]); 1.490 + seekList.push([aTimeData.getFractionalTime(1/2), 1.491 + this.computedValMap.midComp || 1.492 + this.computedValMap.toComp || this.to, 1.493 + msgPrefix + "checking value halfway through animation"]); 1.494 + 1.495 + var finalMsg; 1.496 + var expectedEndVal; 1.497 + if (aIsFreeze) { 1.498 + expectedEndVal = this.computedValMap.toComp || this.to; 1.499 + finalMsg = msgPrefix + "[freeze-mode] checking that final value is set "; 1.500 + } else { 1.501 + expectedEndVal = aBaseVal; 1.502 + finalMsg = msgPrefix + 1.503 + "[remove-mode] checking that animation is cleared "; 1.504 + } 1.505 + seekList.push([aTimeData.getEndTime(), 1.506 + expectedEndVal, finalMsg + "at end of animation"]); 1.507 + seekList.push([aTimeData.getEndTime() + aTimeData.getDur(), 1.508 + expectedEndVal, finalMsg + "after end of animation"]); 1.509 + return seekList; 1.510 + }, 1.511 +} 1.512 +extend(AnimTestcaseFrom, AnimTestcase); 1.513 + 1.514 +/* 1.515 + * A testcase for a simple "from-to" animation 1.516 + * @param aFrom The 'from' value 1.517 + * @param aTo The 'to' value 1.518 + * @param aComputedValMap A hash-map that contains some computed values, 1.519 + * if they're needed, as follows: 1.520 + * - fromComp: Computed value version of |aFrom| (if different from |aFrom|) 1.521 + * - midComp: Computed value that we expect to visit halfway through the 1.522 + * animation (if different from |aTo|) 1.523 + * - toComp: Computed value version of |aTo| (if different from |aTo|) 1.524 + * - noEffect: Special flag -- if set, indicates that this testcase is 1.525 + * expected to have no effect on the computed value. (e.g. the 1.526 + * given values are invalid.) 1.527 + * @param aSkipReason If this test-case is known to currently fail, this 1.528 + * parameter should be a string explaining why. 1.529 + * Otherwise, this value should be null (or omitted). 1.530 + * 1.531 + */ 1.532 +function AnimTestcaseFromTo(aFrom, aTo, aComputedValMap, aSkipReason) 1.533 +{ 1.534 + this.from = aFrom; 1.535 + this.to = aTo; 1.536 + this.computedValMap = aComputedValMap || {}; // Let aComputedValMap be omitted 1.537 + this.skipReason = aSkipReason; 1.538 +} 1.539 +AnimTestcaseFromTo.prototype = 1.540 +{ 1.541 + // Member variables 1.542 + to : null, 1.543 + 1.544 + // Methods 1.545 + setupAnimationElement : function(aAnimAttr, aTimeData, aIsFreeze) 1.546 + { 1.547 + // Call super, and then add my own customization 1.548 + var animElem = AnimTestcaseFrom.prototype.setupAnimationElement.apply(this, 1.549 + [aAnimAttr, aTimeData, aIsFreeze]); 1.550 + animElem.setAttribute("to", this.to) 1.551 + return animElem; 1.552 + }, 1.553 +} 1.554 +extend(AnimTestcaseFromTo, AnimTestcaseFrom); 1.555 + 1.556 +/* 1.557 + * A testcase for a simple "from-by" animation. 1.558 + * 1.559 + * @param aFrom The 'from' value 1.560 + * @param aBy The 'by' value 1.561 + * @param aComputedValMap A hash-map that contains some computed values that 1.562 + * we expect to visit, as follows: 1.563 + * - fromComp: Computed value version of |aFrom| (if different from |aFrom|) 1.564 + * - midComp: Computed value that we expect to visit halfway through the 1.565 + * animation (|aFrom| + |aBy|/2) 1.566 + * - toComp: Computed value of the animation endpoint (|aFrom| + |aBy|) 1.567 + * - noEffect: Special flag -- if set, indicates that this testcase is 1.568 + * expected to have no effect on the computed value. (e.g. the 1.569 + * given values are invalid. Or the attribute may be animatable 1.570 + * and additive, but the particular "from" & "by" values that 1.571 + * are used don't support addition.) 1.572 + * @param aSkipReason If this test-case is known to currently fail, this 1.573 + * parameter should be a string explaining why. 1.574 + * Otherwise, this value should be null (or omitted). 1.575 + */ 1.576 +function AnimTestcaseFromBy(aFrom, aBy, aComputedValMap, aSkipReason) 1.577 +{ 1.578 + this.from = aFrom; 1.579 + this.by = aBy; 1.580 + this.computedValMap = aComputedValMap; 1.581 + this.skipReason = aSkipReason; 1.582 + if (this.computedValMap && 1.583 + !this.computedValMap.noEffect && !this.computedValMap.toComp) { 1.584 + ok(false, "AnimTestcaseFromBy needs expected computed final value"); 1.585 + } 1.586 +} 1.587 +AnimTestcaseFromBy.prototype = 1.588 +{ 1.589 + // Member variables 1.590 + by : null, 1.591 + 1.592 + // Methods 1.593 + setupAnimationElement : function(aAnimAttr, aTimeData, aIsFreeze) 1.594 + { 1.595 + // Call super, and then add my own customization 1.596 + var animElem = AnimTestcaseFrom.prototype.setupAnimationElement.apply(this, 1.597 + [aAnimAttr, aTimeData, aIsFreeze]); 1.598 + animElem.setAttribute("by", this.by) 1.599 + return animElem; 1.600 + }, 1.601 + buildSeekList : function(aAnimAttr, aBaseVal, aTimeData, aIsFreeze) 1.602 + { 1.603 + if (!aAnimAttr.isAdditive) { 1.604 + return this.buildSeekListStatic(aAnimAttr, aBaseVal, aTimeData, 1.605 + "defined as non-additive in SVG spec"); 1.606 + } 1.607 + // Just use inherited method 1.608 + return AnimTestcaseFrom.prototype.buildSeekList.apply(this, 1.609 + [aAnimAttr, aBaseVal, aTimeData, aIsFreeze]); 1.610 + }, 1.611 +} 1.612 +extend(AnimTestcaseFromBy, AnimTestcaseFrom); 1.613 + 1.614 +/* 1.615 + * A testcase for a "paced-mode" animation 1.616 + * @param aValues An array of values, to be used as the "Values" list 1.617 + * @param aComputedValMap A hash-map that contains some computed values, 1.618 + * if they're needed, as follows: 1.619 + * - comp0: The computed value at the start of the animation 1.620 + * - comp1_6: The computed value exactly 1/6 through animation 1.621 + * - comp1_3: The computed value exactly 1/3 through animation 1.622 + * - comp2_3: The computed value exactly 2/3 through animation 1.623 + * - comp1: The computed value of the animation endpoint 1.624 + * The math works out easiest if... 1.625 + * (a) aValuesString has 3 entries in its values list: vA, vB, vC 1.626 + * (b) dist(vB, vC) = 2 * dist(vA, vB) 1.627 + * With this setup, we can come up with expected intermediate values according 1.628 + * to the following rules: 1.629 + * - comp0 should be vA 1.630 + * - comp1_6 should be us halfway between vA and vB 1.631 + * - comp1_3 should be vB 1.632 + * - comp2_3 should be halfway between vB and vC 1.633 + * - comp1 should be vC 1.634 + * @param aSkipReason If this test-case is known to currently fail, this 1.635 + * parameter should be a string explaining why. 1.636 + * Otherwise, this value should be null (or omitted). 1.637 + */ 1.638 +function AnimTestcasePaced(aValuesString, aComputedValMap, aSkipReason) 1.639 +{ 1.640 + this.valuesString = aValuesString; 1.641 + this.computedValMap = aComputedValMap; 1.642 + this.skipReason = aSkipReason; 1.643 + if (this.computedValMap && 1.644 + (!this.computedValMap.comp0 || 1.645 + !this.computedValMap.comp1_6 || 1.646 + !this.computedValMap.comp1_3 || 1.647 + !this.computedValMap.comp2_3 || 1.648 + !this.computedValMap.comp1)) { 1.649 + ok(false, "This AnimTestcasePaced has an incomplete computed value map"); 1.650 + } 1.651 +} 1.652 +AnimTestcasePaced.prototype = 1.653 +{ 1.654 + // Member variables 1.655 + valuesString : null, 1.656 + 1.657 + // Methods 1.658 + setupAnimationElement : function(aAnimAttr, aTimeData, aIsFreeze) 1.659 + { 1.660 + // Call super, and then add my own customization 1.661 + var animElem = AnimTestcase.prototype.setupAnimationElement.apply(this, 1.662 + [aAnimAttr, aTimeData, aIsFreeze]); 1.663 + animElem.setAttribute("values", this.valuesString) 1.664 + animElem.setAttribute("calcMode", "paced"); 1.665 + return animElem; 1.666 + }, 1.667 + buildSeekListAnimated : function(aAnimAttr, aBaseVal, aTimeData, aIsFreeze) 1.668 + { 1.669 + var seekList = new Array(); 1.670 + var msgPrefix = aAnimAttr.attrName + ": checking value "; 1.671 + seekList.push([aTimeData.getBeginTime(), 1.672 + this.computedValMap.comp0, 1.673 + msgPrefix + "at start of animation"]); 1.674 + seekList.push([aTimeData.getFractionalTime(1/6), 1.675 + this.computedValMap.comp1_6, 1.676 + msgPrefix + "1/6 of the way through animation."]); 1.677 + seekList.push([aTimeData.getFractionalTime(1/3), 1.678 + this.computedValMap.comp1_3, 1.679 + msgPrefix + "1/3 of the way through animation."]); 1.680 + seekList.push([aTimeData.getFractionalTime(2/3), 1.681 + this.computedValMap.comp2_3, 1.682 + msgPrefix + "2/3 of the way through animation."]); 1.683 + 1.684 + var finalMsg; 1.685 + var expectedEndVal; 1.686 + if (aIsFreeze) { 1.687 + expectedEndVal = this.computedValMap.comp1; 1.688 + finalMsg = aAnimAttr.attrName + 1.689 + ": [freeze-mode] checking that final value is set "; 1.690 + } else { 1.691 + expectedEndVal = aBaseVal; 1.692 + finalMsg = aAnimAttr.attrName + 1.693 + ": [remove-mode] checking that animation is cleared "; 1.694 + } 1.695 + seekList.push([aTimeData.getEndTime(), 1.696 + expectedEndVal, finalMsg + "at end of animation"]); 1.697 + seekList.push([aTimeData.getEndTime() + aTimeData.getDur(), 1.698 + expectedEndVal, finalMsg + "after end of animation"]); 1.699 + return seekList; 1.700 + }, 1.701 + buildSeekListStatic : function(aAnimAttr, aBaseVal, aTimeData, aReasonStatic) 1.702 + { 1.703 + var seekList = new Array(); 1.704 + var msgPrefix = 1.705 + aAnimAttr.attrName + ": shouldn't be affected by animation "; 1.706 + seekList.push([aTimeData.getBeginTime(), aBaseVal, 1.707 + msgPrefix + "(at animation begin) - " + aReasonStatic]); 1.708 + seekList.push([aTimeData.getFractionalTime(1/6), aBaseVal, 1.709 + msgPrefix + "(1/6 of the way through animation) - " + 1.710 + aReasonStatic]); 1.711 + seekList.push([aTimeData.getFractionalTime(1/3), aBaseVal, 1.712 + msgPrefix + "(1/3 of the way through animation) - " + 1.713 + aReasonStatic]); 1.714 + seekList.push([aTimeData.getFractionalTime(2/3), aBaseVal, 1.715 + msgPrefix + "(2/3 of the way through animation) - " + 1.716 + aReasonStatic]); 1.717 + seekList.push([aTimeData.getEndTime(), aBaseVal, 1.718 + msgPrefix + "(at animation end) - " + aReasonStatic]); 1.719 + seekList.push([aTimeData.getEndTime() + aTimeData.getDur(), aBaseVal, 1.720 + msgPrefix + "(after animation end) - " + aReasonStatic]); 1.721 + return seekList; 1.722 + }, 1.723 +}; 1.724 +extend(AnimTestcasePaced, AnimTestcase); 1.725 + 1.726 +/* 1.727 + * A testcase for an <animateMotion> animation. 1.728 + * 1.729 + * @param aAttrValueHash A hash-map mapping attribute names to values. 1.730 + * Should include at least 'path', 'values', 'to' 1.731 + * or 'by' to describe the motion path. 1.732 + * @param aCtmMap A hash-map that contains summaries of the expected resulting 1.733 + * CTM at various points during the animation. The CTM is 1.734 + * summarized as a tuple of three numbers: [tX, tY, theta] 1.735 + (indicating a translate(tX,tY) followed by a rotate(theta)) 1.736 + * - ctm0: The CTM summary at the start of the animation 1.737 + * - ctm1_6: The CTM summary at exactly 1/6 through animation 1.738 + * - ctm1_3: The CTM summary at exactly 1/3 through animation 1.739 + * - ctm2_3: The CTM summary at exactly 2/3 through animation 1.740 + * - ctm1: The CTM summary at the animation endpoint 1.741 + * 1.742 + * NOTE: For paced-mode animation (the default for animateMotion), the math 1.743 + * works out easiest if: 1.744 + * (a) our motion path has 3 points: vA, vB, vC 1.745 + * (b) dist(vB, vC) = 2 * dist(vA, vB) 1.746 + * (See discussion in header comment for AnimTestcasePaced.) 1.747 + * 1.748 + * @param aSkipReason If this test-case is known to currently fail, this 1.749 + * parameter should be a string explaining why. 1.750 + * Otherwise, this value should be null (or omitted). 1.751 + */ 1.752 +function AnimMotionTestcase(aAttrValueHash, aCtmMap, aSkipReason) 1.753 +{ 1.754 + this.attrValueHash = aAttrValueHash; 1.755 + this.ctmMap = aCtmMap; 1.756 + this.skipReason = aSkipReason; 1.757 + if (this.ctmMap && 1.758 + (!this.ctmMap.ctm0 || 1.759 + !this.ctmMap.ctm1_6 || 1.760 + !this.ctmMap.ctm1_3 || 1.761 + !this.ctmMap.ctm2_3 || 1.762 + !this.ctmMap.ctm1)) { 1.763 + ok(false, "This AnimMotionTestcase has an incomplete CTM map"); 1.764 + } 1.765 +} 1.766 +AnimMotionTestcase.prototype = 1.767 +{ 1.768 + // Member variables 1.769 + _animElementTagName : "animateMotion", 1.770 + 1.771 + // Implementations of inherited methods that we need to override: 1.772 + // -------------------------------------------------------------- 1.773 + setupAnimationElement : function(aAnimAttr, aTimeData, aIsFreeze) 1.774 + { 1.775 + var animElement = document.createElementNS(SVG_NS, 1.776 + this._animElementTagName); 1.777 + animElement.setAttribute("begin", aTimeData.getBeginTime()); 1.778 + animElement.setAttribute("dur", aTimeData.getDur()); 1.779 + if (aIsFreeze) { 1.780 + animElement.setAttribute("fill", "freeze"); 1.781 + } 1.782 + for (var attrName in this.attrValueHash) { 1.783 + if (attrName == "mpath") { 1.784 + this.createPath(this.attrValueHash[attrName]); 1.785 + this.createMpath(animElement); 1.786 + } else { 1.787 + animElement.setAttribute(attrName, this.attrValueHash[attrName]); 1.788 + } 1.789 + } 1.790 + return animElement; 1.791 + }, 1.792 + 1.793 + createPath : function(aPathDescription) 1.794 + { 1.795 + var path = document.createElementNS(SVG_NS, "path"); 1.796 + path.setAttribute("d", aPathDescription); 1.797 + path.setAttribute("id", MPATH_TARGET_ID); 1.798 + return SMILUtil.getSVGRoot().appendChild(path); 1.799 + }, 1.800 + 1.801 + createMpath : function(aAnimElement) 1.802 + { 1.803 + var mpath = document.createElementNS(SVG_NS, "mpath"); 1.804 + mpath.setAttributeNS(XLINK_NS, "href", "#" + MPATH_TARGET_ID); 1.805 + return aAnimElement.appendChild(mpath); 1.806 + }, 1.807 + 1.808 + // Override inherited seekAndTest method since... 1.809 + // (a) it expects a computedValMap and we have a computed-CTM map instead 1.810 + // and (b) it expects we might have no effect (for non-animatable attrs) 1.811 + buildSeekList : function(aAnimAttr, aBaseVal, aTimeData, aIsFreeze) 1.812 + { 1.813 + var seekList = new Array(); 1.814 + var msgPrefix = "CTM mismatch "; 1.815 + seekList.push([aTimeData.getBeginTime(), 1.816 + CTMUtil.generateCTM(this.ctmMap.ctm0), 1.817 + msgPrefix + "at start of animation"]); 1.818 + seekList.push([aTimeData.getFractionalTime(1/6), 1.819 + CTMUtil.generateCTM(this.ctmMap.ctm1_6), 1.820 + msgPrefix + "1/6 of the way through animation."]); 1.821 + seekList.push([aTimeData.getFractionalTime(1/3), 1.822 + CTMUtil.generateCTM(this.ctmMap.ctm1_3), 1.823 + msgPrefix + "1/3 of the way through animation."]); 1.824 + seekList.push([aTimeData.getFractionalTime(2/3), 1.825 + CTMUtil.generateCTM(this.ctmMap.ctm2_3), 1.826 + msgPrefix + "2/3 of the way through animation."]); 1.827 + 1.828 + var finalMsg; 1.829 + var expectedEndVal; 1.830 + if (aIsFreeze) { 1.831 + expectedEndVal = CTMUtil.generateCTM(this.ctmMap.ctm1); 1.832 + finalMsg = aAnimAttr.attrName + 1.833 + ": [freeze-mode] checking that final value is set "; 1.834 + } else { 1.835 + expectedEndVal = aBaseVal; 1.836 + finalMsg = aAnimAttr.attrName + 1.837 + ": [remove-mode] checking that animation is cleared "; 1.838 + } 1.839 + seekList.push([aTimeData.getEndTime(), 1.840 + expectedEndVal, finalMsg + "at end of animation"]); 1.841 + seekList.push([aTimeData.getEndTime() + aTimeData.getDur(), 1.842 + expectedEndVal, finalMsg + "after end of animation"]); 1.843 + return seekList; 1.844 + }, 1.845 + 1.846 + // Override inherited seekAndTest method 1.847 + // (Have to use assertCTMEqual() instead of is() for comparison, to check each 1.848 + // component of the CTM and to allow for a small margin of error.) 1.849 + seekAndTest : function(aSeekList, aTargetElem, aTargetAttr) 1.850 + { 1.851 + var svg = document.getElementById("svg"); 1.852 + for (var i in aSeekList) { 1.853 + var entry = aSeekList[i]; 1.854 + SMILUtil.getSVGRoot().setCurrentTime(entry[0]); 1.855 + CTMUtil.assertCTMEqual(aTargetElem.getCTM(), entry[1], 1.856 + CTMUtil.CTM_COMPONENTS_ALL, entry[2], false); 1.857 + } 1.858 + }, 1.859 + 1.860 + // Override "runTest" method so we can remove any <path> element that we 1.861 + // created at the end of each test. 1.862 + runTest : function(aTargetElem, aTargetAttr, aTimeData, aIsFreeze) 1.863 + { 1.864 + AnimTestcase.prototype.runTest.apply(this, 1.865 + [aTargetElem, aTargetAttr, aTimeData, aIsFreeze]); 1.866 + var pathElem = document.getElementById(MPATH_TARGET_ID); 1.867 + if (pathElem) { 1.868 + SMILUtil.getSVGRoot().removeChild(pathElem); 1.869 + } 1.870 + } 1.871 +}; 1.872 +extend(AnimMotionTestcase, AnimTestcase); 1.873 + 1.874 +// MAIN METHOD 1.875 +function testBundleList(aBundleList, aTimingData) 1.876 +{ 1.877 + for (var bundleIdx in aBundleList) { 1.878 + aBundleList[bundleIdx].go(aTimingData); 1.879 + } 1.880 +}