OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 'use strict'; |
| 6 |
| 7 /** |
| 8 * @fileoverview A test harness loosely based on Python unittest, but that |
| 9 * installs global assert methods during the test for backward compatibility |
| 10 * with Closure tests. |
| 11 */ |
| 12 base.requireStylesheet('base.unittest'); |
| 13 base.exportTo('base.unittest', function() { |
| 14 |
| 15 var NOCATCH_MODE = false; |
| 16 |
| 17 // Uncomment the line below to make unit test failures throw exceptions. |
| 18 NOCATCH_MODE = true; |
| 19 |
| 20 function createTestCaseDiv(testName, opt_href, opt_alwaysShowErrorLink) { |
| 21 var el = document.createElement('test-case'); |
| 22 |
| 23 var titleBlockEl = document.createElement('title'); |
| 24 titleBlockEl.style.display = 'inline'; |
| 25 el.appendChild(titleBlockEl); |
| 26 |
| 27 var titleEl = document.createElement('span'); |
| 28 titleEl.style.marginRight = '20px'; |
| 29 titleBlockEl.appendChild(titleEl); |
| 30 |
| 31 var errorLink = document.createElement('a'); |
| 32 errorLink.textContent = 'Run individually...'; |
| 33 if (opt_href) |
| 34 errorLink.href = opt_href; |
| 35 else |
| 36 errorLink.href = '#' + testName; |
| 37 errorLink.style.display = 'none'; |
| 38 titleBlockEl.appendChild(errorLink); |
| 39 |
| 40 el.__defineSetter__('status', function(status) { |
| 41 titleEl.textContent = testName + ': ' + status; |
| 42 updateClassListGivenStatus(titleEl, status); |
| 43 if (status == 'FAILED' || opt_alwaysShowErrorLink) |
| 44 errorLink.style.display = ''; |
| 45 else |
| 46 errorLink.style.display = 'none'; |
| 47 }); |
| 48 |
| 49 el.addError = function(test, e) { |
| 50 var errorEl = createErrorDiv(test, e); |
| 51 el.appendChild(errorEl); |
| 52 return errorEl; |
| 53 }; |
| 54 |
| 55 el.addHTMLOutput = function(opt_title, opt_element) { |
| 56 var outputEl = createOutputDiv(opt_title, opt_element); |
| 57 el.appendChild(outputEl); |
| 58 return outputEl.contents; |
| 59 }; |
| 60 |
| 61 el.status = 'READY'; |
| 62 return el; |
| 63 } |
| 64 |
| 65 function createErrorDiv(test, e) { |
| 66 var el = document.createElement('test-case-error'); |
| 67 el.className = 'unittest-error'; |
| 68 |
| 69 var stackEl = document.createElement('test-case-stack'); |
| 70 if (typeof e == 'string') { |
| 71 stackEl.textContent = e; |
| 72 } else if (e.stack) { |
| 73 var i = document.location.pathname.lastIndexOf('/'); |
| 74 var path = document.location.origin + |
| 75 document.location.pathname.substring(0, i); |
| 76 var pathEscaped = path.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); |
| 77 var cleanStack = e.stack.replace(new RegExp(pathEscaped, 'g'), '.'); |
| 78 stackEl.textContent = cleanStack; |
| 79 } else { |
| 80 stackEl.textContent = e; |
| 81 } |
| 82 el.appendChild(stackEl); |
| 83 return el; |
| 84 } |
| 85 |
| 86 function createOutputDiv(opt_title, opt_element) { |
| 87 var el = document.createElement('test-case-output'); |
| 88 if (opt_title) { |
| 89 var titleEl = document.createElement('div'); |
| 90 titleEl.textContent = opt_title; |
| 91 el.appendChild(titleEl); |
| 92 } |
| 93 var contentEl = opt_element || document.createElement('div'); |
| 94 el.appendChild(contentEl); |
| 95 |
| 96 el.__defineGetter__('contents', function() { |
| 97 return contentEl; |
| 98 }); |
| 99 return el; |
| 100 } |
| 101 |
| 102 function statusToClassName(status) { |
| 103 if (status == 'PASSED') |
| 104 return 'unittest-green'; |
| 105 else if (status == 'RUNNING' || status == 'READY') |
| 106 return 'unittest-yellow'; |
| 107 else |
| 108 return 'unittest-red'; |
| 109 } |
| 110 |
| 111 function updateClassListGivenStatus(el, status) { |
| 112 var newClass = statusToClassName(status); |
| 113 if (newClass != 'unittest-green') |
| 114 el.classList.remove('unittest-green'); |
| 115 if (newClass != 'unittest-yellow') |
| 116 el.classList.remove('unittest-yellow'); |
| 117 if (newClass != 'unittest-red') |
| 118 el.classList.remove('unittest-red'); |
| 119 |
| 120 el.classList.add(newClass); |
| 121 } |
| 122 |
| 123 function HTMLTestRunner(opt_title, opt_curHash) { |
| 124 // This constructs a HTMLDivElement and then adds our own runner methods to |
| 125 // it. This is usually done via ui.js' define system, but we dont want our |
| 126 // test runner to be dependent on the UI lib. :) |
| 127 var outputEl = document.createElement('unittest-test-runner'); |
| 128 outputEl.__proto__ = HTMLTestRunner.prototype; |
| 129 this.decorate.call(outputEl, opt_title, opt_curHash); |
| 130 return outputEl; |
| 131 } |
| 132 |
| 133 HTMLTestRunner.prototype = { |
| 134 __proto__: HTMLDivElement.prototype, |
| 135 |
| 136 decorate: function(opt_title, opt_curHash) { |
| 137 this.running = false; |
| 138 |
| 139 this.currentTest_ = undefined; |
| 140 this.results = undefined; |
| 141 if (opt_curHash) { |
| 142 var trimmedHash = opt_curHash.substring(1); |
| 143 this.filterFunc_ = function(testName) { |
| 144 return testName.indexOf(trimmedHash) == 0; |
| 145 }; |
| 146 } else |
| 147 this.filterFunc_ = function(testName) { return true; }; |
| 148 |
| 149 this.statusEl_ = document.createElement('title'); |
| 150 this.appendChild(this.statusEl_); |
| 151 |
| 152 this.resultsEl_ = document.createElement('div'); |
| 153 this.appendChild(this.resultsEl_); |
| 154 |
| 155 this.title_ = opt_title || document.title; |
| 156 |
| 157 this.updateStatus(); |
| 158 }, |
| 159 |
| 160 computeResultStats: function() { |
| 161 var numTestsRun = 0; |
| 162 var numTestsPassed = 0; |
| 163 var numTestsWithErrors = 0; |
| 164 if (this.results) { |
| 165 for (var i = 0; i < this.results.length; i++) { |
| 166 numTestsRun++; |
| 167 if (this.results[i].errors.length) |
| 168 numTestsWithErrors++; |
| 169 else |
| 170 numTestsPassed++; |
| 171 } |
| 172 } |
| 173 return { |
| 174 numTestsRun: numTestsRun, |
| 175 numTestsPassed: numTestsPassed, |
| 176 numTestsWithErrors: numTestsWithErrors |
| 177 }; |
| 178 }, |
| 179 |
| 180 updateStatus: function() { |
| 181 var stats = this.computeResultStats(); |
| 182 var status; |
| 183 if (!this.results) { |
| 184 status = 'READY'; |
| 185 } else if (this.running) { |
| 186 status = 'RUNNING'; |
| 187 } else { |
| 188 if (stats.numTestsRun && stats.numTestsWithErrors == 0) |
| 189 status = 'PASSED'; |
| 190 else |
| 191 status = 'FAILED'; |
| 192 } |
| 193 |
| 194 updateClassListGivenStatus(this.statusEl_, status); |
| 195 this.statusEl_.textContent = this.title_ + ' [' + status + ']'; |
| 196 }, |
| 197 |
| 198 get done() { |
| 199 return this.results && this.running == false; |
| 200 }, |
| 201 |
| 202 run: function(tests) { |
| 203 this.results = []; |
| 204 this.running = true; |
| 205 this.updateStatus(); |
| 206 for (var i = 0; i < tests.length; i++) { |
| 207 if (!this.filterFunc_(tests[i].testName)) |
| 208 continue; |
| 209 tests[i].run(this); |
| 210 this.updateStatus(); |
| 211 } |
| 212 this.running = false; |
| 213 this.updateStatus(); |
| 214 }, |
| 215 |
| 216 willRunTest: function(test) { |
| 217 this.currentTest_ = test; |
| 218 this.currentResults_ = {testName: test.testName, |
| 219 errors: []}; |
| 220 this.results.push(this.currentResults_); |
| 221 |
| 222 this.currentTestCaseEl_ = createTestCaseDiv(test.testName); |
| 223 this.currentTestCaseEl_.status = 'RUNNING'; |
| 224 this.resultsEl_.appendChild(this.currentTestCaseEl_); |
| 225 }, |
| 226 |
| 227 /** |
| 228 * Adds some html content to the currently running test |
| 229 * @param {String} opt_title The title for the output. |
| 230 * @param {HTMLElement} opt_element The element to add. If not added, then. |
| 231 * @return {HTMLElement} The element added, or if !opt_element, the element |
| 232 * created. |
| 233 */ |
| 234 addHTMLOutput: function(opt_title, opt_element) { |
| 235 return this.currentTestCaseEl_.addHTMLOutput(opt_title, opt_element); |
| 236 }, |
| 237 |
| 238 addError: function(e) { |
| 239 this.currentResults_.errors.push(e); |
| 240 return this.currentTestCaseEl_.addError(this.currentTest_, e); |
| 241 }, |
| 242 |
| 243 didRunTest: function(test) { |
| 244 if (!this.currentResults_.errors.length) |
| 245 this.currentTestCaseEl_.status = 'PASSED'; |
| 246 else |
| 247 this.currentTestCaseEl_.status = 'FAILED'; |
| 248 |
| 249 this.currentResults_ = undefined; |
| 250 this.currentTest_ = undefined; |
| 251 } |
| 252 }; |
| 253 |
| 254 function TestError(opt_message) { |
| 255 var that = new Error(opt_message); |
| 256 Error.captureStackTrace(that, TestError); |
| 257 that.__proto__ = TestError.prototype; |
| 258 return that; |
| 259 } |
| 260 |
| 261 TestError.prototype = { |
| 262 __proto__: Error.prototype |
| 263 }; |
| 264 |
| 265 /* |
| 266 * @constructor TestCase |
| 267 */ |
| 268 function TestCase(testMethod, opt_testMethodName) { |
| 269 if (!testMethod) |
| 270 throw new Error('testMethod must be provided'); |
| 271 if (testMethod.name == '' && !opt_testMethodName) |
| 272 throw new Error('testMethod must have a name, ' + |
| 273 'or opt_testMethodName must be provided.'); |
| 274 |
| 275 this.testMethod_ = testMethod; |
| 276 this.testMethodName_ = opt_testMethodName || testMethod.name; |
| 277 this.results_ = undefined; |
| 278 }; |
| 279 |
| 280 function forAllAssertAndEnsureMethodsIn_(prototype, fn) { |
| 281 for (var fieldName in prototype) { |
| 282 if (fieldName.indexOf('assert') != 0 && |
| 283 fieldName.indexOf('ensure') != 0) |
| 284 continue; |
| 285 var fieldValue = prototype[fieldName]; |
| 286 if (typeof fieldValue != 'function') |
| 287 continue; |
| 288 fn(fieldName, fieldValue); |
| 289 } |
| 290 } |
| 291 |
| 292 TestCase.prototype = { |
| 293 __proto__: Object.prototype, |
| 294 |
| 295 get testName() { |
| 296 return this.testMethodName_; |
| 297 }, |
| 298 |
| 299 bindGlobals_: function() { |
| 300 forAllAssertAndEnsureMethodsIn_(TestCase.prototype, |
| 301 function(fieldName, fieldValue) { |
| 302 global[fieldName] = fieldValue.bind(this); |
| 303 }); |
| 304 }, |
| 305 |
| 306 unbindGlobals_: function() { |
| 307 forAllAssertAndEnsureMethodsIn_(TestCase.prototype, |
| 308 function(fieldName, fieldValue) { |
| 309 delete global[fieldName]; |
| 310 }); |
| 311 }, |
| 312 |
| 313 /** |
| 314 * Adds some html content to the currently running test |
| 315 * @param {String} opt_title The title for the output. |
| 316 * @param {HTMLElement} opt_element The element to add. If not added, then. |
| 317 * @return {HTMLElement} The element added, or if !opt_element, the element |
| 318 * created. |
| 319 */ |
| 320 addHTMLOutput: function(opt_title, opt_element) { |
| 321 return this.results_.addHTMLOutput(opt_title, opt_element); |
| 322 }, |
| 323 |
| 324 assertTrue: function(a, opt_message) { |
| 325 if (a) |
| 326 return; |
| 327 var message = opt_message || 'Expected true, got ' + a; |
| 328 throw new TestError(message); |
| 329 }, |
| 330 |
| 331 assertFalse: function(a, opt_message) { |
| 332 if (!a) |
| 333 return; |
| 334 var message = opt_message || 'Expected false, got ' + a; |
| 335 throw new TestError(message); |
| 336 }, |
| 337 |
| 338 assertUndefined: function(a, opt_message) { |
| 339 if (a === undefined) |
| 340 return; |
| 341 var message = opt_message || 'Expected undefined, got ' + a; |
| 342 throw new TestError(message); |
| 343 }, |
| 344 |
| 345 assertNotUndefined: function(a, opt_message) { |
| 346 if (a !== undefined) |
| 347 return; |
| 348 var message = opt_message || 'Expected not undefined, got ' + a; |
| 349 throw new TestError(message); |
| 350 }, |
| 351 |
| 352 assertNull: function(a, opt_message) { |
| 353 if (a === null) |
| 354 return; |
| 355 var message = opt_message || 'Expected null, got ' + a; |
| 356 throw new TestError(message); |
| 357 }, |
| 358 |
| 359 assertNotNull: function(a, opt_message) { |
| 360 if (a !== null) |
| 361 return; |
| 362 var message = opt_message || 'Expected non-null, got ' + a; |
| 363 throw new TestError(message); |
| 364 }, |
| 365 |
| 366 assertEquals: function(a, b, opt_message) { |
| 367 if (a == b) |
| 368 return; |
| 369 var message = opt_message || 'Expected ' + a + ', got ' + b; |
| 370 throw new TestError(message); |
| 371 }, |
| 372 |
| 373 assertNotEquals: function(a, b, opt_message) { |
| 374 if (a != b) |
| 375 return; |
| 376 var message = opt_message || 'Expected something not equal to ' + b; |
| 377 throw new TestError(message); |
| 378 }, |
| 379 |
| 380 assertArrayEquals: function(a, b, opt_message) { |
| 381 if (a.length == b.length) { |
| 382 var ok = true; |
| 383 for (var i = 0; i < a.length; i++) { |
| 384 ok &= a[i] === b[i]; |
| 385 } |
| 386 if (ok) |
| 387 return; |
| 388 } |
| 389 |
| 390 var message = opt_message || 'Expected array ' + a + ', got array ' + b; |
| 391 throw new TestError(message); |
| 392 }, |
| 393 |
| 394 assertArrayShallowEquals: function(a, b, opt_message) { |
| 395 if (a.length == b.length) { |
| 396 var ok = true; |
| 397 for (var i = 0; i < a.length; i++) { |
| 398 ok &= a[i] === b[i]; |
| 399 } |
| 400 if (ok) |
| 401 return; |
| 402 } |
| 403 |
| 404 var message = opt_message || 'Expected array ' + b + ', got array ' + a; |
| 405 throw new TestError(message); |
| 406 }, |
| 407 |
| 408 assertAlmostEquals: function(a, b, opt_message) { |
| 409 if (Math.abs(a - b) < 0.00001) |
| 410 return; |
| 411 var message = opt_message || 'Expected almost ' + a + ', got ' + b; |
| 412 throw new TestError(message); |
| 413 }, |
| 414 |
| 415 assertThrows: function(fn, opt_message) { |
| 416 try { |
| 417 fn(); |
| 418 } catch (e) { |
| 419 return; |
| 420 } |
| 421 var message = opt_message || 'Expected throw from ' + fn; |
| 422 throw new TestError(message); |
| 423 }, |
| 424 |
| 425 setUp: function() { |
| 426 }, |
| 427 |
| 428 run: function(results) { |
| 429 this.bindGlobals_(); |
| 430 try { |
| 431 this.results_ = results; |
| 432 results.willRunTest(this); |
| 433 |
| 434 if (NOCATCH_MODE) { |
| 435 this.setUp(); |
| 436 this.testMethod_(); |
| 437 this.tearDown(); |
| 438 } else { |
| 439 // Set up. |
| 440 try { |
| 441 this.setUp(); |
| 442 } catch (e) { |
| 443 results.addError(e); |
| 444 return; |
| 445 } |
| 446 |
| 447 // Run. |
| 448 try { |
| 449 this.testMethod_(); |
| 450 } catch (e) { |
| 451 results.addError(e); |
| 452 } |
| 453 |
| 454 // Tear down. |
| 455 try { |
| 456 this.tearDown(); |
| 457 } catch (e) { |
| 458 if (typeof e == 'string') |
| 459 e = new TestError(e); |
| 460 results.addError(e); |
| 461 } |
| 462 } |
| 463 } finally { |
| 464 this.unbindGlobals_(); |
| 465 results.didRunTest(this); |
| 466 this.results_ = undefined; |
| 467 } |
| 468 }, |
| 469 |
| 470 tearDown: function() { |
| 471 } |
| 472 |
| 473 }; |
| 474 |
| 475 /** |
| 476 * Returns an array of TestCase objects correpsonding to the tests |
| 477 * found in the given object. This considers any functions beginning with test |
| 478 * as a potential test. |
| 479 * |
| 480 * @param {object} opt_objectToEnumerate The object to enumerate, or global if |
| 481 * not specified. |
| 482 * @param {RegExp} opt_filter Return only tests that match this regexp. |
| 483 */ |
| 484 function discoverTests(opt_objectToEnumerate, opt_filter) { |
| 485 var objectToEnumerate = opt_objectToEnumerate || global; |
| 486 |
| 487 var tests = []; |
| 488 for (var testMethodName in objectToEnumerate) { |
| 489 if (testMethodName.search(/^test.+/) != 0) |
| 490 continue; |
| 491 |
| 492 if (opt_filter && testMethodName.search(opt_filter) == -1) |
| 493 continue; |
| 494 |
| 495 var testMethod = objectToEnumerate[testMethodName]; |
| 496 if (typeof testMethod != 'function') |
| 497 continue; |
| 498 var testCase = new TestCase(testMethod, testMethodName); |
| 499 tests.push(testCase); |
| 500 } |
| 501 tests.sort(function(a, b) { |
| 502 return a.testName < b.testName; |
| 503 }); |
| 504 return tests; |
| 505 } |
| 506 |
| 507 /** |
| 508 * Runs all unit tests. |
| 509 */ |
| 510 function runAllTests(opt_objectToEnumerate) { |
| 511 var runner; |
| 512 function init() { |
| 513 if (runner) |
| 514 runner.parentElement.removeChild(runner); |
| 515 runner = new HTMLTestRunner(document.title, document.location.hash); |
| 516 // Stash the runner on global so that the global test runner |
| 517 // can get to it. |
| 518 global.G_testRunner = runner; |
| 519 } |
| 520 |
| 521 function append() { |
| 522 document.body.appendChild(runner); |
| 523 } |
| 524 |
| 525 function run() { |
| 526 var objectToEnumerate = opt_objectToEnumerate || global; |
| 527 var tests = discoverTests(objectToEnumerate); |
| 528 runner.run(tests); |
| 529 } |
| 530 |
| 531 global.addEventListener('hashchange', function() { |
| 532 init(); |
| 533 append(); |
| 534 run(); |
| 535 }); |
| 536 |
| 537 init(); |
| 538 if (document.body) |
| 539 append(); |
| 540 else |
| 541 document.addEventListener('DOMContentLoaded', append); |
| 542 global.addEventListener('load', run); |
| 543 } |
| 544 |
| 545 if (/_test.html$/.test(document.location.pathname)) |
| 546 runAllTests(); |
| 547 |
| 548 return { |
| 549 HTMLTestRunner: HTMLTestRunner, |
| 550 TestError: TestError, |
| 551 TestCase: TestCase, |
| 552 discoverTests: discoverTests, |
| 553 runAllTests: runAllTests, |
| 554 createErrorDiv_: createErrorDiv, |
| 555 createTestCaseDiv_: createTestCaseDiv |
| 556 }; |
| 557 }); |
OLD | NEW |