1// Copyright (c) 2013 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 7base.requireStylesheet('base.unittest'); 8base.require('base.settings'); 9base.require('base.unittest.test_error'); 10base.require('base.unittest.assertions'); 11 12base.exportTo('base.unittest', function() { 13 var TestResults = { 14 FAILED: 0, 15 PASSED: 1, 16 PENDING: 2 17 }; 18 19 var showCondensed_ = false; 20 function showCondensed(val) { 21 showCondensed_ = val; 22 } 23 24 function logWarningMessage(message) { 25 var messagesEl = document.querySelector('#messages'); 26 messagesEl.setAttribute('hasMessages', true); 27 28 var li = document.createElement('li'); 29 li.innerText = message; 30 31 var list = document.querySelector('#message-list'); 32 list.appendChild(li); 33 } 34 35 function TestRunner(suitePaths, tests) { 36 this.suitePaths_ = suitePaths || []; 37 this.suites_ = []; 38 this.suiteNames_ = {}; 39 this.tests_ = tests || []; 40 this.moduleCount_ = 0; 41 42 this.stats_ = { 43 tests: 0, 44 failures: 0, 45 exceptions: [], 46 duration: 0.0 47 }; 48 } 49 50 TestRunner.prototype = { 51 __proto__: Object.prototype, 52 53 loadSuites: function() { 54 this.loadSuiteFiles(); 55 }, 56 57 run: function() { 58 this.clear_(document.querySelector('#test-results')); 59 this.clear_(document.querySelector('#exception-list')); 60 this.clear_(document.querySelector('#message-list')); 61 62 this.updateStats_(); 63 this.runSuites_(); 64 }, 65 66 addSuite: function(suite) { 67 if (this.suiteNames_[suite.name] === true) 68 logWarningMessage('Duplicate test suite name detected: ' + suite.name); 69 70 this.suites_.push(suite); 71 this.suiteNames_[suite.name] = true; 72 73 // This assumes one test suite per file. 74 if (this.suites_.length === this.suitePaths_.length) 75 this.run(); 76 }, 77 78 loadSuiteFiles: function() { 79 var modules = []; 80 this.suitePaths_.forEach(function(path) { 81 var moduleName = path.slice(5, path.length - 3); 82 moduleName = moduleName.replace(/\//g, '.'); 83 modules.push(moduleName); 84 }); 85 86 base.require(modules); 87 }, 88 89 clear_: function(el) { 90 while (el.firstChild) 91 el.removeChild(el.firstChild); 92 }, 93 94 runSuites_: function(opt_idx) { 95 var idx = opt_idx || 0; 96 97 var suiteCount = this.suites_.length; 98 if (idx >= suiteCount) { 99 var harness = document.querySelector('#test-results'); 100 harness.appendChild(document.createElement('br')); 101 harness.appendChild(document.createTextNode('Test Run Complete')); 102 return; 103 } 104 105 var suite = this.suites_[idx]; 106 107 suite.showLongResults = (suiteCount === 1); 108 suite.displayInfo(); 109 suite.runTests(this.tests_); 110 111 this.stats_.duration += suite.duration; 112 this.stats_.tests += suite.testCount; 113 this.stats_.failures += suite.failureCount; 114 115 this.updateStats_(); 116 117 // Give the view time to update. 118 window.setTimeout(function() { 119 this.runSuites_(idx + 1); 120 }.bind(this), 1); 121 }, 122 123 onAnimationFrameError: function(e, opt_stack) { 124 if (e.message) 125 console.error(e.message, e.stack); 126 else 127 console.error(e); 128 129 var exception = {e: e, stack: opt_stack}; 130 this.stats_.exceptions.push(exception); 131 this.appendException(exception); 132 this.updateStats_(); 133 }, 134 135 updateStats_: function() { 136 var statEl = document.querySelector('#stats'); 137 statEl.innerHTML = 138 this.suites_.length + ' suites, ' + 139 '<span class="passed">' + this.stats_.tests + '</span> tests, ' + 140 '<span class="failed">' + this.stats_.failures + 141 '</span> failures, ' + 142 '<span class="exception">' + this.stats_.exceptions.length + 143 '</span> exceptions,' + 144 ' in ' + this.stats_.duration + 'ms.'; 145 }, 146 147 appendException: function(exc) { 148 var exceptionsEl = document.querySelector('#exceptions'); 149 exceptionsEl.setAttribute('hasExceptions', this.stats_.exceptions.length); 150 151 var excEl = document.createElement('li'); 152 excEl.innerHTML = exc.e + '<pre>' + exc.stack + '</pre>'; 153 154 var exceptionsEl = document.querySelector('#exception-list'); 155 exceptionsEl.appendChild(excEl); 156 } 157 }; 158 159 function TestSuite(name, suite) { 160 this.name_ = name; 161 this.tests_ = []; 162 this.testNames_ = {}; 163 this.failures_ = []; 164 this.results_ = TestResults.PENDING; 165 this.showLongResults = false; 166 this.duration_ = 0.0; 167 this.resultsEl_ = undefined; 168 169 global.setup = function(fn) { this.setupFn_ = fn; }.bind(this); 170 global.teardown = function(fn) { this.teardownFn_ = fn; }.bind(this); 171 172 global.test = function(name, test) { 173 if (this.testNames_[name] === true) 174 logWarningMessage('Duplicate test name detected: ' + name); 175 176 this.tests_.push(new Test(name, test)); 177 this.testNames_[name] = true; 178 }.bind(this); 179 180 suite.call(); 181 182 global.setup = undefined; 183 global.teardown = undefined; 184 global.test = undefined; 185 } 186 187 TestSuite.prototype = { 188 __proto__: Object.prototype, 189 190 get name() { 191 return this.name_; 192 }, 193 194 get results() { 195 return this.results_; 196 }, 197 198 get testCount() { 199 return this.tests_.length; 200 }, 201 202 get failureCount() { 203 return this.failures.length; 204 }, 205 206 get failures() { 207 return this.failures_; 208 }, 209 210 get duration() { 211 return this.duration_; 212 }, 213 214 displayInfo: function() { 215 this.resultsEl_ = document.createElement('div'); 216 this.resultsEl_.className = 'test-result'; 217 218 var resultsPanel = document.querySelector('#test-results'); 219 resultsPanel.appendChild(this.resultsEl_); 220 221 if (this.showLongResults) { 222 this.resultsEl_.innerText = this.name; 223 } else { 224 var link = '/src/tests.html?suite='; 225 link += this.name.replace(/\./g, '/'); 226 227 var suiteInfo = document.createElement('a'); 228 suiteInfo.href = link; 229 suiteInfo.innerText = this.name; 230 this.resultsEl_.appendChild(suiteInfo); 231 } 232 233 var statusEl = document.createElement('span'); 234 statusEl.classList.add('results'); 235 statusEl.classList.add('pending'); 236 statusEl.innerText = 'pending'; 237 this.resultsEl_.appendChild(statusEl); 238 }, 239 240 runTests: function(testsToRun) { 241 this.testsToRun_ = testsToRun; 242 243 var start = new Date().getTime(); 244 this.results_ = TestResults.PENDING; 245 this.tests_.forEach(function(test) { 246 if (this.testsToRun_.length !== 0 && 247 this.testsToRun_.indexOf(test.name) === -1) 248 return; 249 250 // Clear settings storage before each test. 251 global.sessionStorage.clear(); 252 base.Settings.setAlternativeStorageInstance(global.sessionStorage); 253 base.onAnimationFrameError = 254 testRunner.onAnimationFrameError.bind(testRunner); 255 256 if (this.setupFn_ !== undefined) 257 this.setupFn_.bind(test).call(); 258 259 var testWorkAreaEl_ = document.createElement('div'); 260 261 this.resultsEl_.appendChild(testWorkAreaEl_); 262 test.run(testWorkAreaEl_); 263 this.resultsEl_.removeChild(testWorkAreaEl_); 264 265 if (this.teardownFn_ !== undefined) 266 this.teardownFn_.bind(test).call(); 267 268 if (test.result === TestResults.FAILED) { 269 this.failures_.push({ 270 error: test.failure, 271 test: test.name 272 }); 273 this.results_ = TestResults.FAILED; 274 } 275 }, this); 276 if (this.results_ === TestResults.PENDING) 277 this.results_ = TestResults.PASSED; 278 279 this.duration_ = new Date().getTime() - start; 280 this.outputResults(); 281 }, 282 283 outputResults: function() { 284 if ((this.results === TestResults.PASSED) && showCondensed_ && 285 !this.showLongResults) { 286 var parent = this.resultsEl_.parentNode; 287 parent.removeChild(this.resultsEl_); 288 this.resultsEl_ = undefined; 289 290 parent.appendChild(document.createTextNode('.')); 291 return; 292 } 293 294 var status = this.resultsEl_.querySelector('.results'); 295 status.classList.remove('pending'); 296 if (this.results === TestResults.PASSED) { 297 status.innerText = 'passed'; 298 status.classList.add('passed'); 299 } else { 300 status.innerText = 'FAILED'; 301 status.classList.add('failed'); 302 } 303 304 status.innerText += ' (' + this.duration_ + 'ms)'; 305 306 var child = this.showLongResults ? this.outputLongResults() : 307 this.outputShortResults(); 308 if (child !== undefined) 309 this.resultsEl_.appendChild(child); 310 }, 311 312 outputShortResults: function() { 313 if (this.results === TestResults.PASSED) 314 return undefined; 315 316 var parent = document.createElement('div'); 317 318 var failureList = this.failures; 319 for (var i = 0; i < failureList.length; ++i) { 320 var fail = failureList[i]; 321 322 var preEl = document.createElement('pre'); 323 preEl.className = 'failure'; 324 preEl.innerText = 'Test: ' + fail.test + '\n' + fail.error.stack; 325 parent.appendChild(preEl); 326 } 327 328 return parent; 329 }, 330 331 outputLongResults: function() { 332 var parent = document.createElement('div'); 333 334 this.tests_.forEach(function(test) { 335 if (this.testsToRun_.length !== 0 && 336 this.testsToRun_.indexOf(test.name) === -1) 337 return; 338 339 var testEl = document.createElement('div'); 340 testEl.className = 'individual-result'; 341 342 var link = '/src/tests.html?suite='; 343 link += this.name.replace(/\./g, '/'); 344 link += '&test=' + test.name.replace(/\./g, '/'); 345 346 var suiteInfo = document.createElement('a'); 347 suiteInfo.href = link; 348 suiteInfo.innerText = test.name; 349 testEl.appendChild(suiteInfo); 350 351 parent.appendChild(testEl); 352 353 var resultEl = document.createElement('span'); 354 resultEl.classList.add('results'); 355 testEl.appendChild(resultEl); 356 if (test.result === TestResults.PASSED) { 357 resultEl.classList.add('passed'); 358 resultEl.innerText = 'passed'; 359 } else { 360 resultEl.classList.add('failed'); 361 resultEl.innerText = 'FAILED'; 362 363 var preEl = document.createElement('pre'); 364 preEl.className = 'failure'; 365 preEl.innerText = test.failure.stack; 366 testEl.appendChild(preEl); 367 } 368 369 if (test.hasAppendedContent) 370 testEl.appendChild(test.appendedContent); 371 372 }.bind(this)); 373 374 return parent; 375 }, 376 377 toString: function() { 378 return this.name_; 379 } 380 }; 381 382 function Test(name, test) { 383 this.name_ = name; 384 this.test_ = test; 385 this.result_ = TestResults.FAILED; 386 this.failure_ = undefined; 387 388 this.appendedContent_ = undefined; 389 } 390 391 Test.prototype = { 392 __proto__: Object.prototype, 393 394 run: function(workArea) { 395 this.testWorkArea_ = workArea; 396 try { 397 this.test_.bind(this).call(); 398 this.result_ = TestResults.PASSED; 399 } catch (e) { 400 console.error(e, e.stack); 401 this.failure_ = e; 402 } 403 }, 404 405 get failure() { 406 return this.failure_; 407 }, 408 409 get name() { 410 return this.name_; 411 }, 412 413 get result() { 414 return this.result_; 415 }, 416 417 get hasAppendedContent() { 418 return (this.appendedContent_ !== undefined); 419 }, 420 421 get appendedContent() { 422 return this.appendedContent_; 423 }, 424 425 addHTMLOutput: function(element) { 426 this.testWorkArea_.appendChild(element); 427 this.appendedContent_ = element; 428 }, 429 430 toString: function() { 431 return this.name_; 432 } 433 }; 434 435 var testRunner; 436 function testSuite(name, suite) { 437 testRunner.addSuite(new TestSuite(name, suite)); 438 } 439 440 function Suites(suitePaths, tests) { 441 testRunner = new TestRunner(suitePaths, tests); 442 testRunner.loadSuites(); 443 } 444 445 function runSuites() { 446 testRunner.run(); 447 } 448 449 return { 450 showCondensed: showCondensed, 451 testSuite: testSuite, 452 runSuites: runSuites, 453 Suites: Suites 454 }; 455}); 456