1 /** 2 * Copyright (C) 2012,2013 KO GmbH <copyright@kogmbh.com> 3 * 4 * @licstart 5 * This file is part of WebODF. 6 * 7 * WebODF is free software: you can redistribute it and/or modify it 8 * under the terms of the GNU Affero General Public License (GNU AGPL) 9 * as published by the Free Software Foundation, either version 3 of 10 * the License, or (at your option) any later version. 11 * 12 * WebODF is distributed in the hope that it will be useful, but 13 * WITHOUT ANY WARRANTY; without even the implied warranty of 14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 * GNU Affero General Public License for more details. 16 * 17 * You should have received a copy of the GNU Affero General Public License 18 * along with WebODF. If not, see <http://www.gnu.org/licenses/>. 19 * @licend 20 * 21 * @source: http://www.webodf.org/ 22 * @source: https://github.com/kogmbh/WebODF/ 23 */ 24 25 /*global runtime, Runtime, core, Node, Element*/ 26 /*jslint evil: true, continue: true, emptyblock: true, unparam: true*/ 27 28 /** 29 * @typedef{{f:function(),name:!string,expectFail:boolean}} 30 */ 31 core.TestData; 32 /** 33 * @typedef{{f:function(function()),name:!string,expectFail:boolean}} 34 */ 35 core.AsyncTestData; 36 /** 37 * @interface 38 */ 39 core.UnitTest = function UnitTest() {"use strict"; }; 40 /** 41 * @return {undefined} 42 */ 43 core.UnitTest.prototype.setUp = function () {"use strict"; }; 44 /** 45 * @return {undefined} 46 */ 47 core.UnitTest.prototype.tearDown = function () {"use strict"; }; 48 /** 49 * @return {!string} 50 */ 51 core.UnitTest.prototype.description = function () {"use strict"; }; 52 /** 53 * @return {!Array.<!core.TestData>} 54 */ 55 core.UnitTest.prototype.tests = function () {"use strict"; }; 56 /** 57 * @return {!Array.<!core.AsyncTestData>} 58 */ 59 core.UnitTest.prototype.asyncTests = function () {"use strict"; }; 60 61 /** 62 * @return {!HTMLDivElement} 63 */ 64 core.UnitTest.provideTestAreaDiv = function () { 65 "use strict"; 66 var maindoc = runtime.getWindow().document, 67 testarea = maindoc.getElementById('testarea'); 68 69 runtime.assert(!testarea, "Unclean test environment, found a div with id \"testarea\"."); 70 71 testarea = maindoc.createElement('div'); 72 testarea.setAttribute('id', 'testarea'); 73 maindoc.body.appendChild(testarea); 74 return /**@type{!HTMLDivElement}*/(testarea); 75 }; 76 77 /** 78 * @return {undefined} 79 */ 80 core.UnitTest.cleanupTestAreaDiv = function () { 81 "use strict"; 82 var maindoc = runtime.getWindow().document, 83 testarea = maindoc.getElementById('testarea'); 84 85 runtime.assert((!!testarea && (testarea.parentNode === maindoc.body)), "Test environment broken, found no div with id \"testarea\" below body."); 86 maindoc.body.removeChild(testarea); 87 }; 88 89 /** 90 * Creates and returns an XML document 91 * @param {!string} rootElementName name of the root element, "prefix:localName" 92 * @param {!string} xmlBodyString XML fragment to insert in the document between the root tags 93 * @param {!Object.<!string, !string>} namespaceMap Name-value pairs that map the 94 * prefix onto the appropriate uri namespace 95 * @return {?Document} 96 */ 97 core.UnitTest.createXmlDocument = function (rootElementName, xmlBodyString, namespaceMap) { 98 "use strict"; 99 var /**@type{!string}*/ 100 xmlDoc = "<?xml version='1.0' encoding='UTF-8'?>"; 101 102 xmlDoc += "<"+ rootElementName; 103 Object.keys(namespaceMap).forEach(function (key) { 104 xmlDoc += " xmlns:" + key + '="' + namespaceMap[key] + '"'; 105 }); 106 xmlDoc += ">"; 107 xmlDoc += xmlBodyString; 108 xmlDoc += "</"+rootElementName+">"; 109 110 return runtime.parseXML(xmlDoc); 111 }; 112 113 /** 114 * Creates and returns a simple ODT document 115 * @param {!string} xml Xml fragment to insert in the document between the 116 * <office:document>..</office:document> tags 117 * @param {!Object.<string, string>} namespaceMap Name-value pairs that map the 118 * prefix onto the appropriate uri namespace 119 * @return {?Document} 120 */ 121 core.UnitTest.createOdtDocument = function (xml, namespaceMap) { 122 "use strict"; 123 return core.UnitTest.createXmlDocument("office:document", xml, namespaceMap); 124 }; 125 126 127 /** 128 * @constructor 129 */ 130 core.UnitTestLogger = function UnitTestLogger() { 131 "use strict"; 132 var /**@type{!Array.<{category:string,message:string}>}*/ 133 messages = [], 134 /**@type{number}*/ 135 errors = 0, 136 start = 0, 137 suite = "", 138 test = ""; 139 /** 140 * @param {string} suiteName 141 * @param {string} testName 142 */ 143 this.startTest = function (suiteName, testName) { 144 messages = []; 145 errors = 0; 146 suite = suiteName; 147 test = testName; 148 start = Date.now(); 149 }; 150 /** 151 * @return {!{description:string,suite:!Array.<string>,success:boolean,log:!Array.<{category:string,message:string}>,time:number}} 152 */ 153 this.endTest = function () { 154 var end = Date.now(); 155 return { 156 description: test, 157 suite: [suite, test], 158 success: errors === 0, 159 log: messages, 160 time: end - start 161 }; 162 }; 163 /** 164 * @param {string} msg 165 */ 166 this.debug = function (msg) { 167 messages.push({category: "debug", message: msg}); 168 }; 169 /** 170 * @param {string} msg 171 */ 172 this.fail = function (msg) { 173 errors += 1; 174 messages.push({category: "fail", message: msg}); 175 }; 176 /** 177 * @param {string} msg 178 */ 179 this.pass = function (msg) { 180 messages.push({category: "pass", message: msg}); 181 }; 182 }; 183 184 /** 185 * @constructor 186 * @param {string} resourcePrefix 187 * @param {!core.UnitTestLogger} logger 188 */ 189 core.UnitTestRunner = function UnitTestRunner(resourcePrefix, logger) { 190 "use strict"; 191 var /**@type{number}*/ 192 failedTests = 0, 193 /**@type{number}*/ 194 failedTestsOnBeginExpectFail, 195 areObjectsEqual, 196 expectFail = false; 197 /** 198 * @return {string} 199 */ 200 this.resourcePrefix = function () { 201 return resourcePrefix; 202 }; 203 /** 204 * @return {undefined} 205 */ 206 this.beginExpectFail = function () { 207 failedTestsOnBeginExpectFail = failedTests; 208 expectFail = true; 209 }; 210 /** 211 * @return {undefined} 212 */ 213 this.endExpectFail = function () { 214 var hasNoFailedTests = (failedTestsOnBeginExpectFail === failedTests); 215 expectFail = false; 216 217 failedTests = failedTestsOnBeginExpectFail; 218 if (hasNoFailedTests) { 219 failedTests += 1; 220 logger.fail("Expected at least one failed test, but none registered."); 221 } 222 }; 223 /** 224 * @param {string} msg 225 * @return {undefined} 226 */ 227 function debug(msg) { 228 logger.debug(msg); 229 } 230 /** 231 * @param {string} msg 232 * @return {undefined} 233 */ 234 function testFailed(msg) { 235 failedTests += 1; 236 if (!expectFail) { 237 logger.fail(msg); 238 } else { 239 logger.debug(msg); 240 } 241 } 242 /** 243 * @param {string} msg 244 * @return {undefined} 245 */ 246 function testPassed(msg) { 247 logger.pass(msg); 248 } 249 /** 250 * @param {!Array.<*>} a actual 251 * @param {!Array.<*>} b expected 252 * @return {!boolean} 253 */ 254 function areArraysEqual(a, b) { 255 var i; 256 try { 257 if (a.length !== b.length) { 258 testFailed("array of length " + a.length + " should be " 259 + b.length + " long"); 260 return false; 261 } 262 for (i = 0; i < a.length; i += 1) { 263 if (a[i] !== b[i]) { 264 testFailed(a[i] + " should be " + b[i] + " at array index " 265 + i); 266 return false; 267 } 268 } 269 } catch (ex) { 270 return false; 271 } 272 return true; 273 } 274 /** 275 * @param {!Element} a actual 276 * @param {!Element} b expected 277 * @param {!boolean} skipReverseCheck 278 * @return {!boolean} 279 */ 280 function areAttributesEqual(a, b, skipReverseCheck) { 281 var aatts = a.attributes, 282 n = aatts.length, 283 i, 284 att, 285 v; 286 for (i = 0; i < n; i += 1) { 287 att = /**@type{!Attr}*/(aatts.item(i)); 288 if (att.prefix !== "xmlns" && att.namespaceURI !== "urn:webodf:names:steps") { 289 v = b.getAttributeNS(att.namespaceURI, att.localName); 290 if (!b.hasAttributeNS(att.namespaceURI, att.localName)) { 291 testFailed("Attribute " + att.localName + " with value " + att.value + " was not present"); 292 return false; 293 } 294 if (v !== att.value) { 295 testFailed("Attribute " + att.localName + " was " + v + " should be " + att.value); 296 return false; 297 } 298 } 299 } 300 return skipReverseCheck ? true : areAttributesEqual(b, a, true); 301 } 302 /** 303 * @param {!Node} a actual 304 * @param {!Node} b expected 305 * @return {!boolean} 306 */ 307 function areNodesEqual(a, b) { 308 var an, bn, 309 atype = a.nodeType, 310 btype = b.nodeType; 311 if (atype !== btype) { 312 testFailed("Nodetype '" + atype + "' should be '" + btype + "'"); 313 return false; 314 } 315 if (atype === Node.TEXT_NODE) { 316 if (/**@type{!Text}*/(a).data === /**@type{!Text}*/(b).data) { 317 return true; 318 } 319 testFailed("Textnode data '" + /**@type{!Text}*/(a).data 320 + "' should be '" + /**@type{!Text}*/(b).data + "'"); 321 return false; 322 } 323 runtime.assert(atype === Node.ELEMENT_NODE, 324 "Only textnodes and elements supported."); 325 if (a.namespaceURI !== b.namespaceURI) { 326 testFailed("namespace '" + a.namespaceURI + "' should be '" 327 + b.namespaceURI + "'"); 328 return false; 329 } 330 if (a.localName !== b.localName) { 331 testFailed("localName '" + a.localName + "' should be '" 332 + b.localName + "'"); 333 return false; 334 } 335 if (!areAttributesEqual(/**@type{!Element}*/(a), 336 /**@type{!Element}*/(b), false)) { 337 return false; 338 } 339 an = a.firstChild; 340 bn = b.firstChild; 341 while (an) { 342 if (!bn) { 343 testFailed("Nodetype '" + an.nodeType + "' is unexpected here."); 344 return false; 345 } 346 if (!areNodesEqual(an, bn)) { 347 return false; 348 } 349 an = an.nextSibling; 350 bn = bn.nextSibling; 351 } 352 if (bn) { 353 testFailed("Nodetype '" + bn.nodeType + "' is missing here."); 354 return false; 355 } 356 return true; 357 } 358 /** 359 * @param {!*} actual 360 * @param {!*} expected 361 * @param {!number=} absoluteTolerance absolute tolerance for number comparison 362 * @return {!boolean} 363 */ 364 function isResultCorrect(actual, expected, absoluteTolerance) { 365 var diff; 366 367 if (expected === 0) { 368 return actual === expected && (1 / actual) === (1 / expected); 369 } 370 if (actual === expected) { 371 return true; 372 } 373 if (actual === null || expected === null) { 374 return false; 375 } 376 if (typeof expected === "number" && isNaN(expected)) { 377 return typeof actual === "number" && isNaN(actual); 378 } 379 if (typeof expected === "number" && typeof actual === "number") { 380 // simple to check? 381 if (actual === expected) { 382 return true; 383 } 384 385 // default (randomly chosen, no theory behind) 386 if (absoluteTolerance === undefined) { 387 absoluteTolerance = 0.0001; 388 } 389 390 runtime.assert(typeof absoluteTolerance === "number", "Absolute tolerance not given as number."); 391 runtime.assert(absoluteTolerance >= 0, "Absolute tolerance should be given as positive number, was "+absoluteTolerance); 392 393 diff = Math.abs(actual - expected); 394 395 return (diff <= absoluteTolerance); 396 } 397 if (Object.prototype.toString.call(expected) === 398 Object.prototype.toString.call([])) { 399 return areArraysEqual(/**@type{!Array}*/(actual), 400 /**@type{!Array}*/(expected)); 401 } 402 if (typeof expected === "object" && typeof actual === "object") { 403 if (/**@type{!Object}*/(expected).constructor === Element 404 || /**@type{!Object}*/(expected).constructor === Node) { 405 return areNodesEqual(/**@type{!Node}*/(actual), 406 /**@type{!Node}*/(expected)); 407 } 408 return areObjectsEqual(/**@type{!Object}*/(actual), 409 /**@type{!Object}*/(expected)); 410 } 411 return false; 412 } 413 /** 414 * @param {*} v 415 * @return {!string} 416 */ 417 function stringify(v) { 418 if (v === 0 && 1 / v < 0) { 419 return "-0"; 420 } 421 if (typeof v === "object") { 422 try { 423 return JSON.stringify(v); 424 } catch (ignore) { 425 // JSON serialization will fail if there is a cyclic dependency of some sort. 426 // Just fall through to returning a normal string in this instance. 427 } 428 } 429 return String(v); 430 } 431 /** 432 * @param {!Object} t 433 * @param {!string} a 434 * @param {!string} b 435 * @param {!number=} absoluteTolerance absolute tolerance for number comparison 436 * @return {undefined} 437 */ 438 function shouldBe(t, a, b, absoluteTolerance) { 439 if (typeof a !== "string" || typeof b !== "string") { 440 debug("WARN: shouldBe() expects string arguments"); 441 } 442 var exception, av, bv; 443 try { 444 av = eval(a); 445 } catch (/**@type{*}*/e) { 446 exception = e; 447 } 448 bv = eval(b); 449 450 if (exception) { 451 testFailed(a + " should be " + bv + ". Threw exception " + 452 exception); 453 } else if (isResultCorrect(av, bv, absoluteTolerance)) { 454 testPassed(a + " is " + b); 455 } else if (String(typeof av) === String(typeof bv)) { 456 testFailed(a + " should be " + stringify(bv) + ". Was " + stringify(av) + "."); 457 } else { 458 testFailed(a + " should be " + bv + " (of type " + typeof bv + 459 "). Was " + av + " (of type " + typeof av + ")."); 460 } 461 } 462 463 /** 464 * @param {!Object} t context in which values to be tested are placed 465 * @param {!string} a the value to be checked 466 * @return {undefined} 467 */ 468 function shouldBeNonNull(t, a) { 469 var exception, av; 470 try { 471 av = eval(a); 472 } catch (/**@type{*}*/e) { 473 exception = e; 474 } 475 476 if (exception) { 477 testFailed(a + " should be non-null. Threw exception " + exception); 478 } else if (av !== null) { 479 testPassed(a + " is non-null."); 480 } else { 481 testFailed(a + " should be non-null. Was " + av); 482 } 483 } 484 /** 485 * @param {!Object} t context in which values to be tested are placed 486 * @param {!string} a the value to be checked 487 * @return {undefined} 488 */ 489 function shouldBeNull(t, a) { 490 shouldBe(t, a, "null"); 491 } 492 493 /** 494 * @param {!Object} a 495 * @param {!Object} b 496 * @return {!boolean} 497 */ 498 areObjectsEqual = function (a, b) { 499 var akeys = Object.keys(a), 500 bkeys = Object.keys(b); 501 akeys.sort(); 502 bkeys.sort(); 503 return areArraysEqual(akeys, bkeys) 504 && Object.keys(a).every(function (key) { 505 var /**@type{*}*/ 506 aval = a[key], 507 /**@type{*}*/ 508 bval = b[key]; 509 if (!isResultCorrect(aval, bval)) { 510 testFailed(aval + " should be " + bval + " for key " + key); 511 return false; 512 } 513 return true; 514 }); 515 }; 516 517 this.areNodesEqual = areNodesEqual; 518 this.shouldBeNull = shouldBeNull; 519 this.shouldBeNonNull = shouldBeNonNull; 520 this.shouldBe = shouldBe; 521 this.testFailed = testFailed; 522 523 /** 524 * @return {!number} 525 */ 526 this.countFailedTests = function () { 527 return failedTests; 528 }; 529 /** 530 * @param {!Array.<T>} functions 531 * @return {!Array.<!{f:T,name:string}>} 532 * @template T 533 */ 534 this.name = function (functions) { 535 var i, fname, 536 nf = [], 537 l = functions.length; 538 nf.length = l; 539 for (i = 0; i < l; i += 1) { 540 fname = Runtime.getFunctionName(functions[i]) || ""; 541 if (fname === "") { 542 throw "Found a function without a name."; 543 } 544 nf[i] = {f: functions[i], name: fname}; 545 } 546 return nf; 547 }; 548 }; 549 550 /** 551 * @constructor 552 */ 553 core.UnitTester = function UnitTester() { 554 "use strict"; 555 var self = this, 556 /**@type{!number}*/ 557 failedTests = 0, 558 logger = new core.UnitTestLogger(), 559 results = {}, 560 inBrowser = runtime.type() === "BrowserRuntime"; 561 /** 562 * @type {string} 563 */ 564 this.resourcePrefix = ""; 565 /** 566 * @param {!string} text 567 * @param {!string} code 568 * @return {!string} 569 **/ 570 function link(text, code) { 571 // NASTY HACK, DO NOT RE-USE. String concatenation with uncontrolled user input is a bad idea for building DOM 572 // fragments everyone. If you feel tempted to extract the HTML escape thing from here, please force yourself to 573 // visit http://shebang.brandonmintern.com/foolproof-html-escaping-in-javascript/ first, and learn a better 574 // approach to take. 575 576 return "<span style='color:blue;cursor:pointer' onclick='" + code + "'>" 577 + text.replace(/</g, "<") + "</span>"; 578 } 579 /** 580 * @type {function(!{description:string,suite:!Array.<string>,success:boolean,log:!Array.<{category:string,message:string}>,time:number})} 581 */ 582 this.reporter = function (r) { 583 var i, m; 584 if (inBrowser) { 585 runtime.log("<span>Running " 586 + link(r.description, "runTest(\"" + r.suite[0] + "\",\"" 587 + r.description + "\")") + "</span>"); 588 } else { 589 runtime.log("Running " + r.description); 590 } 591 if (!r.success) { 592 for (i = 0; i < r.log.length; i += 1) { 593 m = r.log[i]; 594 runtime.log(m.category, m.message); 595 } 596 } 597 }; 598 /** 599 * @param {!{description:string,suite:!Array.<string>,success:boolean,log:!Array.<{category:string,message:string}>,time:number}} r 600 */ 601 function report(r) { 602 if (self.reporter) { 603 self.reporter(r); 604 } 605 } 606 /** 607 * Run the tests from TestClass. 608 * If parameter testNames is supplied only the tests with the names 609 * supplied in that array will be executed. 610 * 611 * @param {!function(new:core.UnitTest,core.UnitTestRunner)} TestClass 612 * The constructor for the test class. 613 * @param {!function():undefined} callback 614 * @param {!Array.<!string>} testNames 615 * @return {undefined} 616 */ 617 this.runTests = function (TestClass, callback, testNames) { 618 var testName = Runtime.getFunctionName(TestClass) || "", 619 /**@type{!string}*/ 620 tname, 621 runner = new core.UnitTestRunner(self.resourcePrefix, logger), 622 test = new TestClass(runner), 623 testResults = {}, 624 i, 625 /**@type{function()|function(function())}*/ 626 t, 627 tests, 628 texpectFail, 629 lastFailCount; 630 631 // check that this test has not been run or started yet 632 if (results.hasOwnProperty(testName)) { 633 runtime.log("Test " + testName + " has already run."); 634 return; 635 } 636 637 if (inBrowser) { 638 runtime.log("<span>Running " 639 + link(testName, "runSuite(\"" + testName + "\");") 640 + ": " + test.description() + "</span>"); 641 } else { 642 runtime.log("Running " + testName + ": " + test.description()); 643 } 644 tests = test.tests(); 645 for (i = 0; i < tests.length; i += 1) { 646 t = tests[i].f; 647 tname = tests[i].name; 648 texpectFail = (tests[i].expectFail === true); 649 if (testNames.length && testNames.indexOf(tname) === -1) { 650 continue; 651 } 652 lastFailCount = runner.countFailedTests(); 653 test.setUp(); 654 logger.startTest(testName, tname); 655 if (texpectFail) { 656 runner.beginExpectFail(); 657 } 658 try { 659 t(); 660 } catch(/**@type{!Error}*/e) { 661 runner.testFailed("Unexpected exception encountered: " + e.toString() + "\n" + e.stack); 662 } 663 if (texpectFail) { 664 runner.endExpectFail(); 665 } 666 report(logger.endTest()); 667 test.tearDown(); 668 testResults[tname] = lastFailCount === runner.countFailedTests(); 669 } 670 /** 671 * @param {!Array.<!core.AsyncTestData>} todo 672 * @return {undefined} 673 */ 674 function runAsyncTests(todo) { 675 var fname, 676 expectFail; 677 if (todo.length === 0) { 678 results[testName] = testResults; 679 failedTests += runner.countFailedTests(); 680 callback(); 681 return; 682 } 683 function tearDownAndRunNext() { 684 if (expectFail) { 685 runner.endExpectFail(); 686 } 687 report(logger.endTest()); 688 test.tearDown(); 689 testResults[fname] = lastFailCount === runner.countFailedTests(); 690 runAsyncTests(todo.slice(1)); 691 } 692 t = todo[0].f; 693 fname = todo[0].name; 694 expectFail = (todo[0].expectFail === true); 695 lastFailCount = runner.countFailedTests(); 696 if (testNames.length && testNames.indexOf(fname) === -1) { 697 runAsyncTests(todo.slice(1)); 698 } else { 699 test.setUp(); 700 logger.startTest(testName, fname); 701 if (expectFail) { 702 runner.beginExpectFail(); 703 } 704 try { 705 t(tearDownAndRunNext); 706 } catch(/**@type{!Error}*/e) { 707 runner.testFailed("Unexpected exception encountered: " + e.toString() + "\n" + e.stack); 708 tearDownAndRunNext(); 709 } 710 } 711 } 712 runAsyncTests(test.asyncTests()); 713 }; 714 /** 715 * @return {!number} 716 **/ 717 this.failedTestsCount = function () { 718 return failedTests; 719 }; 720 /** 721 * @return {!Object} 722 **/ 723 this.results = function () { 724 return results; 725 }; 726 }; 727