tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

smilTestUtils.js (32839B)


      1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
      2 /* vim: set ts=2 sw=2 sts=2 et: */
      3 /* This Source Code Form is subject to the terms of the Mozilla Public
      4 * License, v. 2.0. If a copy of the MPL was not distributed with this
      5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      6 
      7 // Note: Class syntax roughly based on:
      8 // https://developer.mozilla.org/en/Core_JavaScript_1.5_Guide/Inheritance
      9 const SVG_NS = "http://www.w3.org/2000/svg";
     10 const XLINK_NS = "http://www.w3.org/1999/xlink";
     11 
     12 const MPATH_TARGET_ID = "smilTestUtilsTestingPath";
     13 
     14 function extend(child, supertype) {
     15  child.prototype.__proto__ = supertype.prototype;
     16 }
     17 
     18 // General Utility Methods
     19 var SMILUtil = {
     20  // Returns the first matched <svg> node in the document
     21  getSVGRoot() {
     22    return SMILUtil.getFirstElemWithTag("svg");
     23  },
     24 
     25  // Returns the first element in the document with the matching tag
     26  getFirstElemWithTag(aTargetTag) {
     27    var elemList = document.getElementsByTagName(aTargetTag);
     28    return !elemList.length ? null : elemList[0];
     29  },
     30 
     31  // Simple wrapper for getComputedStyle
     32  getComputedStyleSimple(elem, prop) {
     33    return window.getComputedStyle(elem).getPropertyValue(prop);
     34  },
     35 
     36  getAttributeValue(elem, attr) {
     37    if (attr.attrName == SMILUtil.getMotionFakeAttributeName()) {
     38      // Fake motion "attribute" -- "computed value" is the element's CTM
     39      return elem.getCTM();
     40    }
     41    if (attr.attrType == "CSS") {
     42      return SMILUtil.getComputedStyleWrapper(elem, attr.attrName);
     43    }
     44    if (attr.attrType == "XML") {
     45      // XXXdholbert This is appropriate for mapped attributes, but not
     46      // for other attributes.
     47      return SMILUtil.getComputedStyleWrapper(elem, attr.attrName);
     48    }
     49    throw new Error(`Unexpected attribute value ${attr.attrType}`);
     50  },
     51 
     52  // Smart wrapper for getComputedStyle, which will generate a "fake" computed
     53  // style for recognized shorthand properties (font, font-variant, overflow, marker)
     54  getComputedStyleWrapper(elem, propName) {
     55    // Special cases for shorthand properties (which aren't directly queriable
     56    // via getComputedStyle)
     57    var computedStyle;
     58    if (propName == "font") {
     59      var subProps = [
     60        "font-style",
     61        "font-variant-caps",
     62        "font-weight",
     63        "font-size",
     64        "line-height",
     65        "font-family",
     66      ];
     67      for (var i in subProps) {
     68        var subPropStyle = SMILUtil.getComputedStyleSimple(elem, subProps[i]);
     69        if (subPropStyle) {
     70          if (subProps[i] == "line-height") {
     71            // There needs to be a "/" before line-height
     72            subPropStyle = "/ " + subPropStyle;
     73          }
     74          if (!computedStyle) {
     75            computedStyle = subPropStyle;
     76          } else {
     77            computedStyle = computedStyle + " " + subPropStyle;
     78          }
     79        }
     80      }
     81    } else if (propName == "font-variant") {
     82      // xxx - this isn't completely correct but it's sufficient for what's
     83      //       being tested here
     84      computedStyle = SMILUtil.getComputedStyleSimple(
     85        elem,
     86        "font-variant-caps"
     87      );
     88    } else if (propName == "marker") {
     89      var subProps = ["marker-end", "marker-mid", "marker-start"];
     90      for (var i in subProps) {
     91        if (!computedStyle) {
     92          computedStyle = SMILUtil.getComputedStyleSimple(elem, subProps[i]);
     93        } else {
     94          is(
     95            computedStyle,
     96            SMILUtil.getComputedStyleSimple(elem, subProps[i]),
     97            "marker sub-properties should match each other " +
     98              "(they shouldn't be individually set)"
     99          );
    100        }
    101      }
    102    } else if (propName == "overflow") {
    103      var subProps = ["overflow-x", "overflow-y"];
    104      for (var i in subProps) {
    105        if (!computedStyle) {
    106          computedStyle = SMILUtil.getComputedStyleSimple(elem, subProps[i]);
    107        } else {
    108          is(
    109            computedStyle,
    110            SMILUtil.getComputedStyleSimple(elem, subProps[i]),
    111            "overflow sub-properties should match each other " +
    112              "(they shouldn't be individually set)"
    113          );
    114        }
    115      }
    116    } else {
    117      computedStyle = SMILUtil.getComputedStyleSimple(elem, propName);
    118    }
    119    return computedStyle;
    120  },
    121 
    122  getMotionFakeAttributeName() {
    123    return "_motion";
    124  },
    125 
    126  // Return stripped px value from specified value.
    127  stripPx: str => str.replace(/px\s*$/, ""),
    128 };
    129 
    130 var CTMUtil = {
    131  CTM_COMPONENTS_ALL: ["a", "b", "c", "d", "e", "f"],
    132  CTM_COMPONENTS_ROTATE: ["a", "b", "c", "d"],
    133 
    134  // Function to generate a CTM Matrix from a "summary"
    135  // (a 3-tuple containing [tX, tY, theta])
    136  generateCTM(aCtmSummary) {
    137    if (!aCtmSummary || aCtmSummary.length != 3) {
    138      ok(false, "Unexpected CTM summary tuple length: " + aCtmSummary.length);
    139    }
    140    var tX = aCtmSummary[0];
    141    var tY = aCtmSummary[1];
    142    var theta = aCtmSummary[2];
    143    var cosTheta = Math.cos(theta);
    144    var sinTheta = Math.sin(theta);
    145    var newCtm = {
    146      a: cosTheta,
    147      c: -sinTheta,
    148      e: tX,
    149      b: sinTheta,
    150      d: cosTheta,
    151      f: tY,
    152    };
    153    return newCtm;
    154  },
    155 
    156  /// Helper for isCtmEqual
    157  isWithinDelta(aTestVal, aExpectedVal, aErrMsg, aIsTodo) {
    158    var testFunc = aIsTodo ? todo : ok;
    159    const delta = 0.00001; // allowing margin of error = 10^-5
    160    ok(
    161      aTestVal >= aExpectedVal - delta && aTestVal <= aExpectedVal + delta,
    162      aErrMsg + " | got: " + aTestVal + ", expected: " + aExpectedVal
    163    );
    164  },
    165 
    166  assertCTMEqual(aLeftCtm, aRightCtm, aComponentsToCheck, aErrMsg, aIsTodo) {
    167    var foundCTMDifference = false;
    168    for (var j in aComponentsToCheck) {
    169      var curComponent = aComponentsToCheck[j];
    170      if (!aIsTodo) {
    171        CTMUtil.isWithinDelta(
    172          aLeftCtm[curComponent],
    173          aRightCtm[curComponent],
    174          aErrMsg + " | component: " + curComponent,
    175          false
    176        );
    177      } else if (aLeftCtm[curComponent] != aRightCtm[curComponent]) {
    178        foundCTMDifference = true;
    179      }
    180    }
    181 
    182    if (aIsTodo) {
    183      todo(!foundCTMDifference, aErrMsg + " | (currently marked todo)");
    184    }
    185  },
    186 
    187  assertCTMNotEqual(aLeftCtm, aRightCtm, aComponentsToCheck, aErrMsg, aIsTodo) {
    188    // CTM should not match initial one
    189    var foundCTMDifference = false;
    190    for (var j in aComponentsToCheck) {
    191      var curComponent = aComponentsToCheck[j];
    192      if (aLeftCtm[curComponent] != aRightCtm[curComponent]) {
    193        foundCTMDifference = true;
    194        break; // We found a difference, as expected. Success!
    195      }
    196    }
    197 
    198    if (aIsTodo) {
    199      todo(foundCTMDifference, aErrMsg + " | (currently marked todo)");
    200    } else {
    201      ok(foundCTMDifference, aErrMsg);
    202    }
    203  },
    204 };
    205 
    206 // Wrapper for timing information
    207 function SMILTimingData(aBegin, aDur) {
    208  this._begin = aBegin;
    209  this._dur = aDur;
    210 }
    211 SMILTimingData.prototype = {
    212  _begin: null,
    213  _dur: null,
    214  getBeginTime() {
    215    return this._begin;
    216  },
    217  getDur() {
    218    return this._dur;
    219  },
    220  getEndTime() {
    221    return this._begin + this._dur;
    222  },
    223  getFractionalTime(aPortion) {
    224    return this._begin + aPortion * this._dur;
    225  },
    226 };
    227 
    228 /**
    229 * Attribute: a container for information about an attribute we'll
    230 * attempt to animate with SMIL in our tests.
    231 *
    232 * See also the factory methods below: NonAnimatableAttribute(),
    233 * NonAdditiveAttribute(), and AdditiveAttribute().
    234 *
    235 * @param aAttrName The name of the attribute
    236 * @param aAttrType The type of the attribute ("CSS" vs "XML")
    237 * @param aTargetTag The name of an element that this attribute could be
    238 *                   applied to.
    239 * @param aIsAnimatable A bool indicating whether this attribute is defined as
    240 *                      animatable in the SVG spec.
    241 * @param aIsAdditive   A bool indicating whether this attribute is defined as
    242 *                      additive (i.e. supports "by" animation) in the SVG spec.
    243 */
    244 function Attribute(
    245  aAttrName,
    246  aAttrType,
    247  aTargetTag,
    248  aIsAnimatable,
    249  aIsAdditive
    250 ) {
    251  this.attrName = aAttrName;
    252  this.attrType = aAttrType;
    253  this.targetTag = aTargetTag;
    254  this.isAnimatable = aIsAnimatable;
    255  this.isAdditive = aIsAdditive;
    256 }
    257 Attribute.prototype = {
    258  // Member variables
    259  attrName: null,
    260  attrType: null,
    261  isAnimatable: null,
    262  testcaseList: null,
    263 };
    264 
    265 // Generators for Attribute objects.  These allow lists of attribute
    266 // definitions to be more human-readible than if we were using Attribute() with
    267 // boolean flags, e.g. "Attribute(..., true, true), Attribute(..., true, false)
    268 function NonAnimatableAttribute(aAttrName, aAttrType, aTargetTag) {
    269  return new Attribute(aAttrName, aAttrType, aTargetTag, false, false);
    270 }
    271 function NonAdditiveAttribute(aAttrName, aAttrType, aTargetTag) {
    272  return new Attribute(aAttrName, aAttrType, aTargetTag, true, false);
    273 }
    274 function AdditiveAttribute(aAttrName, aAttrType, aTargetTag) {
    275  return new Attribute(aAttrName, aAttrType, aTargetTag, true, true);
    276 }
    277 
    278 /**
    279 * TestcaseBundle: a container for a group of tests for a particular attribute
    280 *
    281 * @param aAttribute An Attribute object for the attribute
    282 * @param aTestcaseList An array of AnimTestcase objects
    283 */
    284 function TestcaseBundle(aAttribute, aTestcaseList, aSkipReason) {
    285  this.animatedAttribute = aAttribute;
    286  this.testcaseList = aTestcaseList;
    287  this.skipReason = aSkipReason;
    288 }
    289 TestcaseBundle.prototype = {
    290  // Member variables
    291  animatedAttribute: null,
    292  testcaseList: null,
    293  skipReason: null,
    294 
    295  // Methods
    296  go(aTimingData) {
    297    if (this.skipReason) {
    298      todo(
    299        false,
    300        "Skipping a bundle for '" +
    301          this.animatedAttribute.attrName +
    302          "' because: " +
    303          this.skipReason
    304      );
    305    } else {
    306      // Sanity Check: Bundle should have > 0 testcases
    307      if (!this.testcaseList || !this.testcaseList.length) {
    308        ok(
    309          false,
    310          "a bundle for '" +
    311            this.animatedAttribute.attrName +
    312            "' has no testcases"
    313        );
    314      }
    315 
    316      var targetElem = SMILUtil.getFirstElemWithTag(
    317        this.animatedAttribute.targetTag
    318      );
    319 
    320      if (!targetElem) {
    321        ok(
    322          false,
    323          "Error: can't find an element of type '" +
    324            this.animatedAttribute.targetTag +
    325            "', so I can't test property '" +
    326            this.animatedAttribute.attrName +
    327            "'"
    328        );
    329        return;
    330      }
    331 
    332      for (var testcaseIdx in this.testcaseList) {
    333        var testcase = this.testcaseList[testcaseIdx];
    334        if (testcase.skipReason) {
    335          todo(
    336            false,
    337            "Skipping a testcase for '" +
    338              this.animatedAttribute.attrName +
    339              "' because: " +
    340              testcase.skipReason
    341          );
    342        } else {
    343          testcase.runTest(
    344            targetElem,
    345            this.animatedAttribute,
    346            aTimingData,
    347            false
    348          );
    349          testcase.runTest(
    350            targetElem,
    351            this.animatedAttribute,
    352            aTimingData,
    353            true
    354          );
    355        }
    356      }
    357    }
    358  },
    359 };
    360 
    361 /**
    362 * AnimTestcase: an abstract class that represents an animation testcase.
    363 * (e.g. a set of "from"/"to" values to test)
    364 */
    365 function AnimTestcase() {} // abstract => no constructor
    366 AnimTestcase.prototype = {
    367  // Member variables
    368  _animElementTagName: "animate", // Can be overridden for e.g. animateColor
    369  computedValMap: null,
    370  skipReason: null,
    371 
    372  // Methods
    373  /**
    374   * runTest: Runs this AnimTestcase
    375   *
    376   * @param aTargetElem The node to be targeted in our test animation.
    377   * @param aTargetAttr An Attribute object representing the attribute
    378   *                    to be targeted in our test animation.
    379   * @param aTimeData A SMILTimingData object with timing information for
    380   *                  our test animation.
    381   * @param aIsFreeze If true, indicates that our test animation should use
    382   *                  fill="freeze"; otherwise, we'll default to fill="remove".
    383   */
    384  runTest(aTargetElem, aTargetAttr, aTimeData, aIsFreeze) {
    385    // SANITY CHECKS
    386    if (!SMILUtil.getSVGRoot().animationsPaused()) {
    387      ok(false, "Should start each test with animations paused");
    388    }
    389    if (SMILUtil.getSVGRoot().getCurrentTime() != 0) {
    390      ok(false, "Should start each test at time = 0");
    391    }
    392 
    393    // SET UP
    394    // Cache initial computed value
    395    var baseVal = SMILUtil.getAttributeValue(aTargetElem, aTargetAttr);
    396 
    397    // Create & append animation element
    398    var anim = this.setupAnimationElement(aTargetAttr, aTimeData, aIsFreeze);
    399    aTargetElem.appendChild(anim);
    400 
    401    // Build a list of [seek-time, expectedValue, errorMessage] triplets
    402    var seekList = this.buildSeekList(
    403      aTargetAttr,
    404      baseVal,
    405      aTimeData,
    406      aIsFreeze
    407    );
    408 
    409    // DO THE ACTUAL TESTING
    410    this.seekAndTest(seekList, aTargetElem, aTargetAttr);
    411 
    412    // CLEAN UP
    413    aTargetElem.removeChild(anim);
    414    SMILUtil.getSVGRoot().setCurrentTime(0);
    415  },
    416 
    417  // HELPER FUNCTIONS
    418  // setupAnimationElement: <animate> element
    419  // Subclasses should extend this parent method
    420  setupAnimationElement(aAnimAttr, aTimeData, aIsFreeze) {
    421    var animElement = document.createElementNS(
    422      SVG_NS,
    423      this._animElementTagName
    424    );
    425    animElement.setAttribute("attributeName", aAnimAttr.attrName);
    426    animElement.setAttribute("attributeType", aAnimAttr.attrType);
    427    animElement.setAttribute("begin", aTimeData.getBeginTime());
    428    animElement.setAttribute("dur", aTimeData.getDur());
    429    if (aIsFreeze) {
    430      animElement.setAttribute("fill", "freeze");
    431    }
    432    return animElement;
    433  },
    434 
    435  buildSeekList(aAnimAttr, aBaseVal, aTimeData, aIsFreeze) {
    436    if (!aAnimAttr.isAnimatable) {
    437      return this.buildSeekListStatic(
    438        aAnimAttr,
    439        aBaseVal,
    440        aTimeData,
    441        "defined as non-animatable in SVG spec"
    442      );
    443    }
    444    if (this.computedValMap.noEffect) {
    445      return this.buildSeekListStatic(
    446        aAnimAttr,
    447        aBaseVal,
    448        aTimeData,
    449        "testcase specified to have no effect"
    450      );
    451    }
    452    return this.buildSeekListAnimated(
    453      aAnimAttr,
    454      aBaseVal,
    455      aTimeData,
    456      aIsFreeze
    457    );
    458  },
    459 
    460  seekAndTest(aSeekList, aTargetElem, aTargetAttr) {
    461    var svg = document.getElementById("svg");
    462    for (var i in aSeekList) {
    463      var entry = aSeekList[i];
    464      SMILUtil.getSVGRoot().setCurrentTime(entry[0]);
    465 
    466      // Bug 1379908: The computed value of stroke-* properties should be
    467      // serialized with px units, but currently Gecko and Servo don't do that
    468      // when animating these values.
    469      if (
    470        ["stroke-width", "stroke-dasharray", "stroke-dashoffset"].includes(
    471          aTargetAttr.attrName
    472        )
    473      ) {
    474        var attr = SMILUtil.stripPx(
    475          SMILUtil.getAttributeValue(aTargetElem, aTargetAttr)
    476        );
    477        var expectedVal = SMILUtil.stripPx(entry[1]);
    478        is(attr, expectedVal, entry[2]);
    479        return;
    480      }
    481      is(
    482        SMILUtil.getAttributeValue(aTargetElem, aTargetAttr),
    483        entry[1],
    484        entry[2]
    485      );
    486    }
    487  },
    488 
    489  // methods that expect to be overridden in subclasses
    490  buildSeekListStatic(aAnimAttr, aBaseVal, aTimeData, aReasonStatic) {},
    491  buildSeekListAnimated(aAnimAttr, aBaseVal, aTimeData, aIsFreeze) {},
    492 };
    493 
    494 // Abstract parent class to share code between from-to & from-by testcases.
    495 function AnimTestcaseFrom() {} // abstract => no constructor
    496 AnimTestcaseFrom.prototype = {
    497  // Member variables
    498  from: null,
    499 
    500  // Methods
    501  setupAnimationElement(aAnimAttr, aTimeData, aIsFreeze) {
    502    // Call super, and then add my own customization
    503    var animElem = AnimTestcase.prototype.setupAnimationElement.apply(this, [
    504      aAnimAttr,
    505      aTimeData,
    506      aIsFreeze,
    507    ]);
    508    animElem.setAttribute("from", this.from);
    509    return animElem;
    510  },
    511 
    512  buildSeekListStatic(aAnimAttr, aBaseVal, aTimeData, aReasonStatic) {
    513    var seekList = new Array();
    514    var msgPrefix =
    515      aAnimAttr.attrName + ": shouldn't be affected by animation ";
    516    seekList.push([
    517      aTimeData.getBeginTime(),
    518      aBaseVal,
    519      msgPrefix + "(at animation begin) - " + aReasonStatic,
    520    ]);
    521    seekList.push([
    522      aTimeData.getFractionalTime(1 / 2),
    523      aBaseVal,
    524      msgPrefix + "(at animation mid) - " + aReasonStatic,
    525    ]);
    526    seekList.push([
    527      aTimeData.getEndTime(),
    528      aBaseVal,
    529      msgPrefix + "(at animation end) - " + aReasonStatic,
    530    ]);
    531    seekList.push([
    532      aTimeData.getEndTime() + aTimeData.getDur(),
    533      aBaseVal,
    534      msgPrefix + "(after animation end) - " + aReasonStatic,
    535    ]);
    536    return seekList;
    537  },
    538 
    539  buildSeekListAnimated(aAnimAttr, aBaseVal, aTimeData, aIsFreeze) {
    540    var seekList = new Array();
    541    var msgPrefix = aAnimAttr.attrName + ": ";
    542    if (aTimeData.getBeginTime() > 0.1) {
    543      seekList.push([
    544        aTimeData.getBeginTime() - 0.1,
    545        aBaseVal,
    546        msgPrefix +
    547          "checking that base value is set " +
    548          "before start of animation",
    549      ]);
    550    }
    551 
    552    seekList.push([
    553      aTimeData.getBeginTime(),
    554      this.computedValMap.fromComp || this.from,
    555      msgPrefix +
    556        "checking that 'from' value is set " +
    557        "at start of animation",
    558    ]);
    559    seekList.push([
    560      aTimeData.getFractionalTime(1 / 2),
    561      this.computedValMap.midComp || this.computedValMap.toComp || this.to,
    562      msgPrefix + "checking value halfway through animation",
    563    ]);
    564 
    565    var finalMsg;
    566    var expectedEndVal;
    567    if (aIsFreeze) {
    568      expectedEndVal = this.computedValMap.toComp || this.to;
    569      finalMsg = msgPrefix + "[freeze-mode] checking that final value is set ";
    570    } else {
    571      expectedEndVal = aBaseVal;
    572      finalMsg =
    573        msgPrefix + "[remove-mode] checking that animation is cleared ";
    574    }
    575    seekList.push([
    576      aTimeData.getEndTime(),
    577      expectedEndVal,
    578      finalMsg + "at end of animation",
    579    ]);
    580    seekList.push([
    581      aTimeData.getEndTime() + aTimeData.getDur(),
    582      expectedEndVal,
    583      finalMsg + "after end of animation",
    584    ]);
    585    return seekList;
    586  },
    587 };
    588 extend(AnimTestcaseFrom, AnimTestcase);
    589 
    590 /**
    591 * A testcase for a simple "from-to" animation
    592 *
    593 * @param aFrom  The 'from' value
    594 * @param aTo    The 'to' value
    595 * @param aComputedValMap  A hash-map that contains some computed values,
    596 *                         if they're needed, as follows:
    597 *    - fromComp: Computed value version of |aFrom| (if different from |aFrom|)
    598 *    - midComp:  Computed value that we expect to visit halfway through the
    599 *                animation (if different from |aTo|)
    600 *    - toComp:   Computed value version of |aTo| (if different from |aTo|)
    601 *    - noEffect: Special flag -- if set, indicates that this testcase is
    602 *                expected to have no effect on the computed value. (e.g. the
    603 *                given values are invalid.)
    604 * @param aSkipReason  If this test-case is known to currently fail, this
    605 *                     parameter should be a string explaining why.
    606 *                     Otherwise, this value should be null (or omitted).
    607 */
    608 function AnimTestcaseFromTo(aFrom, aTo, aComputedValMap, aSkipReason) {
    609  this.from = aFrom;
    610  this.to = aTo;
    611  this.computedValMap = aComputedValMap || {}; // Let aComputedValMap be omitted
    612  this.skipReason = aSkipReason;
    613 }
    614 AnimTestcaseFromTo.prototype = {
    615  // Member variables
    616  to: null,
    617 
    618  // Methods
    619  setupAnimationElement(aAnimAttr, aTimeData, aIsFreeze) {
    620    // Call super, and then add my own customization
    621    var animElem = AnimTestcaseFrom.prototype.setupAnimationElement.apply(
    622      this,
    623      [aAnimAttr, aTimeData, aIsFreeze]
    624    );
    625    animElem.setAttribute("to", this.to);
    626    return animElem;
    627  },
    628 };
    629 extend(AnimTestcaseFromTo, AnimTestcaseFrom);
    630 
    631 /**
    632 * A testcase for a simple "from-by" animation.
    633 *
    634 * @param aFrom  The 'from' value
    635 * @param aBy    The 'by' value
    636 * @param aComputedValMap  A hash-map that contains some computed values that
    637 *                         we expect to visit, as follows:
    638 *    - fromComp: Computed value version of |aFrom| (if different from |aFrom|)
    639 *    - midComp:  Computed value that we expect to visit halfway through the
    640 *                animation (|aFrom| + |aBy|/2)
    641 *    - toComp:   Computed value of the animation endpoint (|aFrom| + |aBy|)
    642 *    - noEffect: Special flag -- if set, indicates that this testcase is
    643 *                expected to have no effect on the computed value. (e.g. the
    644 *                given values are invalid.  Or the attribute may be animatable
    645 *                and additive, but the particular "from" & "by" values that
    646 *                are used don't support addition.)
    647 * @param aSkipReason  If this test-case is known to currently fail, this
    648 *                     parameter should be a string explaining why.
    649 *                     Otherwise, this value should be null (or omitted).
    650 */
    651 function AnimTestcaseFromBy(aFrom, aBy, aComputedValMap, aSkipReason) {
    652  this.from = aFrom;
    653  this.by = aBy;
    654  this.computedValMap = aComputedValMap;
    655  this.skipReason = aSkipReason;
    656  if (
    657    this.computedValMap &&
    658    !this.computedValMap.noEffect &&
    659    !this.computedValMap.toComp
    660  ) {
    661    ok(false, "AnimTestcaseFromBy needs expected computed final value");
    662  }
    663 }
    664 AnimTestcaseFromBy.prototype = {
    665  // Member variables
    666  by: null,
    667 
    668  // Methods
    669  setupAnimationElement(aAnimAttr, aTimeData, aIsFreeze) {
    670    // Call super, and then add my own customization
    671    var animElem = AnimTestcaseFrom.prototype.setupAnimationElement.apply(
    672      this,
    673      [aAnimAttr, aTimeData, aIsFreeze]
    674    );
    675    animElem.setAttribute("by", this.by);
    676    return animElem;
    677  },
    678  buildSeekList(aAnimAttr, aBaseVal, aTimeData, aIsFreeze) {
    679    if (!aAnimAttr.isAdditive) {
    680      return this.buildSeekListStatic(
    681        aAnimAttr,
    682        aBaseVal,
    683        aTimeData,
    684        "defined as non-additive in SVG spec"
    685      );
    686    }
    687    // Just use inherited method
    688    return AnimTestcaseFrom.prototype.buildSeekList.apply(this, [
    689      aAnimAttr,
    690      aBaseVal,
    691      aTimeData,
    692      aIsFreeze,
    693    ]);
    694  },
    695 };
    696 extend(AnimTestcaseFromBy, AnimTestcaseFrom);
    697 
    698 /**
    699 * A testcase for a "paced-mode" animation
    700 *
    701 * @param aValues   An array of values, to be used as the "Values" list
    702 * @param aComputedValMap  A hash-map that contains some computed values,
    703 *                         if they're needed, as follows:
    704 *      - comp0:   The computed value at the start of the animation
    705 *      - comp1_6: The computed value exactly 1/6 through animation
    706 *      - comp1_3: The computed value exactly 1/3 through animation
    707 *      - comp2_3: The computed value exactly 2/3 through animation
    708 *      - comp1:   The computed value of the animation endpoint
    709 *  The math works out easiest if...
    710 *    (a) aValuesString has 3 entries in its values list: vA, vB, vC
    711 *    (b) dist(vB, vC) = 2 * dist(vA, vB)
    712 *  With this setup, we can come up with expected intermediate values according
    713 *  to the following rules:
    714 *    - comp0 should be vA
    715 *    - comp1_6 should be us halfway between vA and vB
    716 *    - comp1_3 should be vB
    717 *    - comp2_3 should be halfway between vB and vC
    718 *    - comp1 should be vC
    719 * @param aSkipReason  If this test-case is known to currently fail, this
    720 *                     parameter should be a string explaining why.
    721 *                     Otherwise, this value should be null (or omitted).
    722 */
    723 function AnimTestcasePaced(aValuesString, aComputedValMap, aSkipReason) {
    724  this.valuesString = aValuesString;
    725  this.computedValMap = aComputedValMap;
    726  this.skipReason = aSkipReason;
    727  if (
    728    this.computedValMap &&
    729    (!this.computedValMap.comp0 ||
    730      !this.computedValMap.comp1_6 ||
    731      !this.computedValMap.comp1_3 ||
    732      !this.computedValMap.comp2_3 ||
    733      !this.computedValMap.comp1)
    734  ) {
    735    ok(false, "This AnimTestcasePaced has an incomplete computed value map");
    736  }
    737 }
    738 AnimTestcasePaced.prototype = {
    739  // Member variables
    740  valuesString: null,
    741 
    742  // Methods
    743  setupAnimationElement(aAnimAttr, aTimeData, aIsFreeze) {
    744    // Call super, and then add my own customization
    745    var animElem = AnimTestcase.prototype.setupAnimationElement.apply(this, [
    746      aAnimAttr,
    747      aTimeData,
    748      aIsFreeze,
    749    ]);
    750    animElem.setAttribute("values", this.valuesString);
    751    animElem.setAttribute("calcMode", "paced");
    752    return animElem;
    753  },
    754  buildSeekListAnimated(aAnimAttr, aBaseVal, aTimeData, aIsFreeze) {
    755    var seekList = new Array();
    756    var msgPrefix = aAnimAttr.attrName + ": checking value ";
    757    seekList.push([
    758      aTimeData.getBeginTime(),
    759      this.computedValMap.comp0,
    760      msgPrefix + "at start of animation",
    761    ]);
    762    seekList.push([
    763      aTimeData.getFractionalTime(1 / 6),
    764      this.computedValMap.comp1_6,
    765      msgPrefix + "1/6 of the way through animation.",
    766    ]);
    767    seekList.push([
    768      aTimeData.getFractionalTime(1 / 3),
    769      this.computedValMap.comp1_3,
    770      msgPrefix + "1/3 of the way through animation.",
    771    ]);
    772    seekList.push([
    773      aTimeData.getFractionalTime(2 / 3),
    774      this.computedValMap.comp2_3,
    775      msgPrefix + "2/3 of the way through animation.",
    776    ]);
    777 
    778    var finalMsg;
    779    var expectedEndVal;
    780    if (aIsFreeze) {
    781      expectedEndVal = this.computedValMap.comp1;
    782      finalMsg =
    783        aAnimAttr.attrName +
    784        ": [freeze-mode] checking that final value is set ";
    785    } else {
    786      expectedEndVal = aBaseVal;
    787      finalMsg =
    788        aAnimAttr.attrName +
    789        ": [remove-mode] checking that animation is cleared ";
    790    }
    791    seekList.push([
    792      aTimeData.getEndTime(),
    793      expectedEndVal,
    794      finalMsg + "at end of animation",
    795    ]);
    796    seekList.push([
    797      aTimeData.getEndTime() + aTimeData.getDur(),
    798      expectedEndVal,
    799      finalMsg + "after end of animation",
    800    ]);
    801    return seekList;
    802  },
    803  buildSeekListStatic(aAnimAttr, aBaseVal, aTimeData, aReasonStatic) {
    804    var seekList = new Array();
    805    var msgPrefix =
    806      aAnimAttr.attrName + ": shouldn't be affected by animation ";
    807    seekList.push([
    808      aTimeData.getBeginTime(),
    809      aBaseVal,
    810      msgPrefix + "(at animation begin) - " + aReasonStatic,
    811    ]);
    812    seekList.push([
    813      aTimeData.getFractionalTime(1 / 6),
    814      aBaseVal,
    815      msgPrefix + "(1/6 of the way through animation) - " + aReasonStatic,
    816    ]);
    817    seekList.push([
    818      aTimeData.getFractionalTime(1 / 3),
    819      aBaseVal,
    820      msgPrefix + "(1/3 of the way through animation) - " + aReasonStatic,
    821    ]);
    822    seekList.push([
    823      aTimeData.getFractionalTime(2 / 3),
    824      aBaseVal,
    825      msgPrefix + "(2/3 of the way through animation) - " + aReasonStatic,
    826    ]);
    827    seekList.push([
    828      aTimeData.getEndTime(),
    829      aBaseVal,
    830      msgPrefix + "(at animation end) - " + aReasonStatic,
    831    ]);
    832    seekList.push([
    833      aTimeData.getEndTime() + aTimeData.getDur(),
    834      aBaseVal,
    835      msgPrefix + "(after animation end) - " + aReasonStatic,
    836    ]);
    837    return seekList;
    838  },
    839 };
    840 extend(AnimTestcasePaced, AnimTestcase);
    841 
    842 /**
    843 * A testcase for an <animateMotion> animation.
    844 *
    845 * @param aAttrValueHash   A hash-map mapping attribute names to values.
    846 *                         Should include at least 'path', 'values', 'to'
    847 *                         or 'by' to describe the motion path.
    848 * @param aCtmMap  A hash-map that contains summaries of the expected resulting
    849 *                 CTM at various points during the animation. The CTM is
    850 *                 summarized as a tuple of three numbers: [tX, tY, theta]
    851                   (indicating a translate(tX,tY) followed by a rotate(theta))
    852 *      - ctm0:   The CTM summary at the start of the animation
    853 *      - ctm1_6: The CTM summary at exactly 1/6 through animation
    854 *      - ctm1_3: The CTM summary at exactly 1/3 through animation
    855 *      - ctm2_3: The CTM summary at exactly 2/3 through animation
    856 *      - ctm1:   The CTM summary at the animation endpoint
    857 *
    858 *  NOTE: For paced-mode animation (the default for animateMotion), the math
    859 *  works out easiest if:
    860 *    (a) our motion path has 3 points: vA, vB, vC
    861 *    (b) dist(vB, vC) = 2 * dist(vA, vB)
    862 *  (See discussion in header comment for AnimTestcasePaced.)
    863 *
    864 * @param aSkipReason  If this test-case is known to currently fail, this
    865 *                     parameter should be a string explaining why.
    866 *                     Otherwise, this value should be null (or omitted).
    867 */
    868 function AnimMotionTestcase(aAttrValueHash, aCtmMap, aSkipReason) {
    869  this.attrValueHash = aAttrValueHash;
    870  this.ctmMap = aCtmMap;
    871  this.skipReason = aSkipReason;
    872  if (
    873    this.ctmMap &&
    874    (!this.ctmMap.ctm0 ||
    875      !this.ctmMap.ctm1_6 ||
    876      !this.ctmMap.ctm1_3 ||
    877      !this.ctmMap.ctm2_3 ||
    878      !this.ctmMap.ctm1)
    879  ) {
    880    ok(false, "This AnimMotionTestcase has an incomplete CTM map");
    881  }
    882 }
    883 AnimMotionTestcase.prototype = {
    884  // Member variables
    885  _animElementTagName: "animateMotion",
    886 
    887  // Implementations of inherited methods that we need to override:
    888  // --------------------------------------------------------------
    889  setupAnimationElement(aAnimAttr, aTimeData, aIsFreeze) {
    890    var animElement = document.createElementNS(
    891      SVG_NS,
    892      this._animElementTagName
    893    );
    894    animElement.setAttribute("begin", aTimeData.getBeginTime());
    895    animElement.setAttribute("dur", aTimeData.getDur());
    896    if (aIsFreeze) {
    897      animElement.setAttribute("fill", "freeze");
    898    }
    899    for (var attrName in this.attrValueHash) {
    900      if (attrName == "mpath") {
    901        this.createPath(this.attrValueHash[attrName]);
    902        this.createMpath(animElement);
    903      } else {
    904        animElement.setAttribute(attrName, this.attrValueHash[attrName]);
    905      }
    906    }
    907    return animElement;
    908  },
    909 
    910  createPath(aPathDescription) {
    911    var path = document.createElementNS(SVG_NS, "path");
    912    path.setAttribute("d", aPathDescription);
    913    path.setAttribute("id", MPATH_TARGET_ID);
    914    return SMILUtil.getSVGRoot().appendChild(path);
    915  },
    916 
    917  createMpath(aAnimElement) {
    918    var mpath = document.createElementNS(SVG_NS, "mpath");
    919    mpath.setAttributeNS(XLINK_NS, "href", "#" + MPATH_TARGET_ID);
    920    return aAnimElement.appendChild(mpath);
    921  },
    922 
    923  // Override inherited seekAndTest method since...
    924  // (a) it expects a computedValMap and we have a computed-CTM map instead
    925  // and (b) it expects we might have no effect (for non-animatable attrs)
    926  buildSeekList(aAnimAttr, aBaseVal, aTimeData, aIsFreeze) {
    927    var seekList = new Array();
    928    var msgPrefix = "CTM mismatch ";
    929    seekList.push([
    930      aTimeData.getBeginTime(),
    931      CTMUtil.generateCTM(this.ctmMap.ctm0),
    932      msgPrefix + "at start of animation",
    933    ]);
    934    seekList.push([
    935      aTimeData.getFractionalTime(1 / 6),
    936      CTMUtil.generateCTM(this.ctmMap.ctm1_6),
    937      msgPrefix + "1/6 of the way through animation.",
    938    ]);
    939    seekList.push([
    940      aTimeData.getFractionalTime(1 / 3),
    941      CTMUtil.generateCTM(this.ctmMap.ctm1_3),
    942      msgPrefix + "1/3 of the way through animation.",
    943    ]);
    944    seekList.push([
    945      aTimeData.getFractionalTime(2 / 3),
    946      CTMUtil.generateCTM(this.ctmMap.ctm2_3),
    947      msgPrefix + "2/3 of the way through animation.",
    948    ]);
    949 
    950    var finalMsg;
    951    var expectedEndVal;
    952    if (aIsFreeze) {
    953      expectedEndVal = CTMUtil.generateCTM(this.ctmMap.ctm1);
    954      finalMsg =
    955        aAnimAttr.attrName +
    956        ": [freeze-mode] checking that final value is set ";
    957    } else {
    958      expectedEndVal = aBaseVal;
    959      finalMsg =
    960        aAnimAttr.attrName +
    961        ": [remove-mode] checking that animation is cleared ";
    962    }
    963    seekList.push([
    964      aTimeData.getEndTime(),
    965      expectedEndVal,
    966      finalMsg + "at end of animation",
    967    ]);
    968    seekList.push([
    969      aTimeData.getEndTime() + aTimeData.getDur(),
    970      expectedEndVal,
    971      finalMsg + "after end of animation",
    972    ]);
    973    return seekList;
    974  },
    975 
    976  // Override inherited seekAndTest method
    977  // (Have to use assertCTMEqual() instead of is() for comparison, to check each
    978  // component of the CTM and to allow for a small margin of error.)
    979  seekAndTest(aSeekList, aTargetElem, aTargetAttr) {
    980    var svg = document.getElementById("svg");
    981    for (var i in aSeekList) {
    982      var entry = aSeekList[i];
    983      SMILUtil.getSVGRoot().setCurrentTime(entry[0]);
    984      CTMUtil.assertCTMEqual(
    985        aTargetElem.getCTM(),
    986        entry[1],
    987        CTMUtil.CTM_COMPONENTS_ALL,
    988        entry[2],
    989        false
    990      );
    991    }
    992  },
    993 
    994  // Override "runTest" method so we can remove any <path> element that we
    995  // created at the end of each test.
    996  runTest(aTargetElem, aTargetAttr, aTimeData, aIsFreeze) {
    997    AnimTestcase.prototype.runTest.apply(this, [
    998      aTargetElem,
    999      aTargetAttr,
   1000      aTimeData,
   1001      aIsFreeze,
   1002    ]);
   1003    var pathElem = document.getElementById(MPATH_TARGET_ID);
   1004    if (pathElem) {
   1005      SMILUtil.getSVGRoot().removeChild(pathElem);
   1006    }
   1007  },
   1008 };
   1009 extend(AnimMotionTestcase, AnimTestcase);
   1010 
   1011 // MAIN METHOD
   1012 function testBundleList(aBundleList, aTimingData) {
   1013  for (var bundleIdx in aBundleList) {
   1014    aBundleList[bundleIdx].go(aTimingData);
   1015  }
   1016 }