dom/smil/test/smilTestUtils.js

changeset 0
6474c204b198
     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 +}

mercurial