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 return String(v); 422 } 423 /** 424 * @param {!Object} t 425 * @param {!string} a 426 * @param {!string} b 427 * @param {!number=} absoluteTolerance absolute tolerance for number comparison 428 * @return {undefined} 429 */ 430 function shouldBe(t, a, b, absoluteTolerance) { 431 if (typeof a !== "string" || typeof b !== "string") { 432 debug("WARN: shouldBe() expects string arguments"); 433 } 434 var exception, av, bv; 435 try { 436 av = eval(a); 437 } catch (/**@type{*}*/e) { 438 exception = e; 439 } 440 bv = eval(b); 441 442 if (exception) { 443 testFailed(a + " should be " + bv + ". Threw exception " + 444 exception); 445 } else if (isResultCorrect(av, bv, absoluteTolerance)) { 446 testPassed(a + " is " + b); 447 } else if (String(typeof av) === String(typeof bv)) { 448 testFailed(a + " should be " + bv + ". Was " + stringify(av) + "."); 449 } else { 450 testFailed(a + " should be " + bv + " (of type " + typeof bv + 451 "). Was " + av + " (of type " + typeof av + ")."); 452 } 453 } 454 455 /** 456 * @param {!Object} t context in which values to be tested are placed 457 * @param {!string} a the value to be checked 458 * @return {undefined} 459 */ 460 function shouldBeNonNull(t, a) { 461 var exception, av; 462 try { 463 av = eval(a); 464 } catch (/**@type{*}*/e) { 465 exception = e; 466 } 467 468 if (exception) { 469 testFailed(a + " should be non-null. Threw exception " + exception); 470 } else if (av !== null) { 471 testPassed(a + " is non-null."); 472 } else { 473 testFailed(a + " should be non-null. Was " + av); 474 } 475 } 476 /** 477 * @param {!Object} t context in which values to be tested are placed 478 * @param {!string} a the value to be checked 479 * @return {undefined} 480 */ 481 function shouldBeNull(t, a) { 482 shouldBe(t, a, "null"); 483 } 484 485 /** 486 * @param {!Object} a 487 * @param {!Object} b 488 * @return {!boolean} 489 */ 490 areObjectsEqual = function (a, b) { 491 var akeys = Object.keys(a), 492 bkeys = Object.keys(b); 493 akeys.sort(); 494 bkeys.sort(); 495 return areArraysEqual(akeys, bkeys) 496 && Object.keys(a).every(function (key) { 497 var /**@type{*}*/ 498 aval = a[key], 499 /**@type{*}*/ 500 bval = b[key]; 501 if (!isResultCorrect(aval, bval)) { 502 testFailed(aval + " should be " + bval + " for key " + key); 503 return false; 504 } 505 return true; 506 }); 507 }; 508 509 this.areNodesEqual = areNodesEqual; 510 this.shouldBeNull = shouldBeNull; 511 this.shouldBeNonNull = shouldBeNonNull; 512 this.shouldBe = shouldBe; 513 this.testFailed = testFailed; 514 515 /** 516 * @return {!number} 517 */ 518 this.countFailedTests = function () { 519 return failedTests; 520 }; 521 /** 522 * @param {!Array.<T>} functions 523 * @return {!Array.<!{f:T,name:string}>} 524 * @template T 525 */ 526 this.name = function (functions) { 527 var i, fname, 528 nf = [], 529 l = functions.length; 530 nf.length = l; 531 for (i = 0; i < l; i += 1) { 532 fname = Runtime.getFunctionName(functions[i]) || ""; 533 if (fname === "") { 534 throw "Found a function without a name."; 535 } 536 nf[i] = {f: functions[i], name: fname}; 537 } 538 return nf; 539 }; 540 }; 541 542 /** 543 * @constructor 544 */ 545 core.UnitTester = function UnitTester() { 546 "use strict"; 547 var self = this, 548 /**@type{!number}*/ 549 failedTests = 0, 550 logger = new core.UnitTestLogger(), 551 results = {}, 552 inBrowser = runtime.type() === "BrowserRuntime"; 553 /** 554 * @type {string} 555 */ 556 this.resourcePrefix = ""; 557 /** 558 * @param {!string} text 559 * @param {!string} code 560 * @return {!string} 561 **/ 562 function link(text, code) { 563 return "<span style='color:blue;cursor:pointer' onclick='" + code + "'>" 564 + text + "</span>"; 565 } 566 /** 567 * @type {function(!{description:string,suite:!Array.<string>,success:boolean,log:!Array.<{category:string,message:string}>,time:number})} 568 */ 569 this.reporter = function (r) { 570 var i, m; 571 if (inBrowser) { 572 runtime.log("<span>Running " 573 + link(r.description, "runTest(\"" + r.suite[0] + "\",\"" 574 + r.description + "\")") + "</span>"); 575 } else { 576 runtime.log("Running " + r.description); 577 } 578 if (!r.success) { 579 for (i = 0; i < r.log.length; i += 1) { 580 m = r.log[i]; 581 runtime.log(m.category, m.message); 582 } 583 } 584 }; 585 /** 586 * @param {!{description:string,suite:!Array.<string>,success:boolean,log:!Array.<{category:string,message:string}>,time:number}} r 587 */ 588 function report(r) { 589 if (self.reporter) { 590 self.reporter(r); 591 } 592 } 593 /** 594 * Run the tests from TestClass. 595 * If parameter testNames is supplied only the tests with the names 596 * supplied in that array will be executed. 597 * 598 * @param {!function(new:core.UnitTest,core.UnitTestRunner)} TestClass 599 * The constructor for the test class. 600 * @param {!function():undefined} callback 601 * @param {!Array.<!string>} testNames 602 * @return {undefined} 603 */ 604 this.runTests = function (TestClass, callback, testNames) { 605 var testName = Runtime.getFunctionName(TestClass) || "", 606 /**@type{!string}*/ 607 tname, 608 runner = new core.UnitTestRunner(self.resourcePrefix, logger), 609 test = new TestClass(runner), 610 testResults = {}, 611 i, 612 /**@type{function()|function(function())}*/ 613 t, 614 tests, 615 texpectFail, 616 lastFailCount; 617 618 // check that this test has not been run or started yet 619 if (results.hasOwnProperty(testName)) { 620 runtime.log("Test " + testName + " has already run."); 621 return; 622 } 623 624 if (inBrowser) { 625 runtime.log("<span>Running " 626 + link(testName, "runSuite(\"" + testName + "\");") 627 + ": " + test.description() + "</span>"); 628 } else { 629 runtime.log("Running " + testName + ": " + test.description); 630 } 631 tests = test.tests(); 632 for (i = 0; i < tests.length; i += 1) { 633 t = tests[i].f; 634 tname = tests[i].name; 635 texpectFail = (tests[i].expectFail === true); 636 if (testNames.length && testNames.indexOf(tname) === -1) { 637 continue; 638 } 639 lastFailCount = runner.countFailedTests(); 640 test.setUp(); 641 logger.startTest(testName, tname); 642 if (texpectFail) { 643 runner.beginExpectFail(); 644 } 645 try { 646 t(); 647 } catch(/**@type{!Error}*/e) { 648 runner.testFailed("Unexpected exception encountered: " + e.toString() + "\n" + e.stack); 649 } 650 if (texpectFail) { 651 runner.endExpectFail(); 652 } 653 report(logger.endTest()); 654 test.tearDown(); 655 testResults[tname] = lastFailCount === runner.countFailedTests(); 656 } 657 /** 658 * @param {!Array.<!core.AsyncTestData>} todo 659 * @return {undefined} 660 */ 661 function runAsyncTests(todo) { 662 var fname, 663 expectFail; 664 if (todo.length === 0) { 665 results[testName] = testResults; 666 failedTests += runner.countFailedTests(); 667 callback(); 668 return; 669 } 670 function tearDownAndRunNext() { 671 if (expectFail) { 672 runner.endExpectFail(); 673 } 674 report(logger.endTest()); 675 test.tearDown(); 676 testResults[fname] = lastFailCount === runner.countFailedTests(); 677 runAsyncTests(todo.slice(1)); 678 } 679 t = todo[0].f; 680 fname = todo[0].name; 681 expectFail = (todo[0].expectFail === true); 682 lastFailCount = runner.countFailedTests(); 683 if (testNames.length && testNames.indexOf(fname) === -1) { 684 runAsyncTests(todo.slice(1)); 685 } else { 686 test.setUp(); 687 logger.startTest(testName, fname); 688 if (expectFail) { 689 runner.beginExpectFail(); 690 } 691 try { 692 t(tearDownAndRunNext); 693 } catch(/**@type{!Error}*/e) { 694 runner.testFailed("Unexpected exception encountered: " + e.toString() + "\n" + e.stack); 695 tearDownAndRunNext(); 696 } 697 } 698 } 699 runAsyncTests(test.asyncTests()); 700 }; 701 /** 702 * @return {!number} 703 **/ 704 this.failedTestsCount = function () { 705 return failedTests; 706 }; 707 /** 708 * @return {!Object} 709 **/ 710 this.results = function () { 711 return results; 712 }; 713 }; 714