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 }