1<!DOCTYPE html> 2<!-- 3Copyright (c) 2014 The Chromium Authors. All rights reserved. 4Use of this source code is governed by a BSD-style license that can be 5found in the LICENSE file. 6--> 7 8<link rel="import" href="/tracing/base/base.html"> 9<link rel="import" href="/tracing/base/unittest.html"> 10<link rel="import" href="/tracing/base/unittest/suite_loader.html"> 11<link rel="import" href="/tracing/base/unittest/test_runner.html"> 12<link rel="import" href="/tracing/base/unittest/html_test_results.html"> 13<link rel="import" href="/tracing/ui/base/utils.html"> 14 15<style> 16 x-base-interactive-test-runner { 17 display: flex; 18 flex-direction: column; 19 flex: 0 0 auto; 20 } 21 22 x-base-interactive-test-runner > * { 23 flex: 0 0 auto; 24 } 25 x-base-interactive-test-runner > #title { 26 font-size: 16pt; 27 } 28 29 x-base-interactive-test-runner { 30 font-family: sans-serif; 31 } 32 33 x-base-interactive-test-runner > h1 { 34 margin: 5px 0px 10px 0px; 35 } 36 37 x-base-interactive-test-runner > #stats { 38 } 39 40 x-base-interactive-test-runner > #controls { 41 display: block; 42 margin-bottom: 5px; 43 } 44 45 x-base-interactive-test-runner > #controls > ul { 46 list-style-type: none; 47 padding: 0; 48 margin: 0; 49 } 50 51 x-base-interactive-test-runner > #controls > ul > li { 52 float: left; 53 margin-right: 10px; 54 padding-top: 5px; 55 padding-bottom: 5px; 56 } 57 58 x-base-interactive-test-runner > #shortform-results { 59 color: green; 60 height; 40px; 61 word-wrap: break-word; 62 } 63 64 x-base-interactive-test-runner > #shortform-results > .fail { 65 color: darkred; 66 font-weight: bold; 67 } 68 69 x-base-interactive-test-runner > #shortform-results > .flaky { 70 color: darkorange; 71 } 72 73 x-base-interactive-test-runner > #results-container { 74 flex: 1 1 auto; 75 min-height: 0; 76 overflow: auto; 77 padding: 0 4px 0 4px; 78 } 79 80 .unittest-pending { 81 color: orange; 82 } 83 .unittest-running { 84 color: orange; 85 font-weight: bold; 86 } 87 88 .unittest-passed { 89 color: darkgreen; 90 } 91 92 .unittest-failed { 93 color: darkred; 94 font-weight: bold; 95 } 96 97 .unittest-flaky { 98 color: darkorange; 99 } 100 101 .unittest-exception { 102 color: red; 103 font-weight: bold; 104 } 105 106 .unittest-failure { 107 border: 1px solid grey; 108 border-radius: 5px; 109 padding: 5px; 110 } 111</style> 112 113<template id="x-base-interactive-test-runner-template"> 114 <h1 id="title">Tests</h1> 115 <div id="stats"></div> 116 <div id="controls"> 117 <ul id="links"> 118 </ul> 119 <div style="clear: both;"></div> 120 121 <div> 122 <span> 123 <label> 124 <input type="radio" name="test-type-to-run" value="unit" /> 125 Run unit tests 126 </label> 127 </span> 128 <span> 129 <label> 130 <input type="radio" name="test-type-to-run" value="perf" /> 131 Run perf tests 132 </label> 133 </span> 134 <span> 135 <label> 136 <input type="radio" name="test-type-to-run" value="all" /> 137 Run all tests 138 </label> 139 </span> 140 </div> 141 <span> 142 <label> 143 <input type="checkbox" id="short-format" /> Short format</label> 144 </span> 145 </div> 146 <div id="shortform-results"> 147 </div> 148 <div id="results-container"> 149 </div> 150</template> 151 152<script> 153'use strict'; 154 155tr.exportTo('tr.b.unittest', function() { 156 var THIS_DOC = document.currentScript.ownerDocument; 157 var ALL_TEST_TYPES = 'all'; 158 159 /** 160 * @constructor 161 */ 162 var InteractiveTestRunner = tr.ui.b.define('x-base-interactive-test-runner'); 163 164 InteractiveTestRunner.prototype = { 165 __proto__: HTMLUnknownElement.prototype, 166 167 decorate: function() { 168 this.allTests_ = undefined; 169 170 this.suppressStateChange_ = false; 171 172 this.testFilterString_ = ''; 173 this.testTypeToRun_ = tr.b.unittest.TestTypes.UNITTEST; 174 this.shortFormat_ = false; 175 this.testSuiteName_ = ''; 176 177 this.rerunPending_ = false; 178 this.runner_ = undefined; 179 this.results_ = undefined; 180 this.headless_ = false; 181 182 this.onResultsStatsChanged_ = this.onResultsStatsChanged_.bind(this); 183 this.onTestFailed_ = this.onTestFailed_.bind(this); 184 this.onTestFlaky_ = this.onTestFlaky_.bind(this); 185 this.onTestPassed_ = this.onTestPassed_.bind(this); 186 187 this.appendChild(tr.ui.b.instantiateTemplate( 188 '#x-base-interactive-test-runner-template', THIS_DOC)); 189 190 this.querySelector( 191 'input[name=test-type-to-run][value=unit]').checked = true; 192 var testTypeToRunEls = tr.b.asArray(this.querySelectorAll( 193 'input[name=test-type-to-run]')); 194 195 testTypeToRunEls.forEach( 196 function(inputEl) { 197 inputEl.addEventListener( 198 'click', this.onTestTypeToRunClick_.bind(this)); 199 }, this); 200 201 var shortFormatEl = this.querySelector('#short-format'); 202 shortFormatEl.checked = this.shortFormat_; 203 shortFormatEl.addEventListener( 204 'click', this.onShortFormatClick_.bind(this)); 205 this.updateShortFormResultsDisplay_(); 206 207 // Oh, DOM, how I love you. Title is such a convenient property name and I 208 // refuse to change my worldview because of tooltips. 209 this.__defineSetter__( 210 'title', 211 function(title) { 212 this.querySelector('#title').textContent = title; 213 }); 214 }, 215 216 get allTests() { 217 return this.allTests_; 218 }, 219 220 set allTests(allTests) { 221 this.allTests_ = allTests; 222 this.scheduleRerun_(); 223 }, 224 225 get testLinks() { 226 return this.testLinks_; 227 }, 228 set testLinks(testLinks) { 229 this.testLinks_ = testLinks; 230 var linksEl = this.querySelector('#links'); 231 linksEl.textContent = ''; 232 this.testLinks_.forEach(function(l) { 233 var link = document.createElement('a'); 234 link.href = l.linkPath; 235 link.textContent = l.title; 236 237 var li = document.createElement('li'); 238 li.appendChild(link); 239 240 linksEl.appendChild(li); 241 }, this); 242 }, 243 244 get testFilterString() { 245 return this.testFilterString_; 246 }, 247 248 set testFilterString(testFilterString) { 249 this.testFilterString_ = testFilterString; 250 this.scheduleRerun_(); 251 if (!this.suppressStateChange_) 252 tr.b.dispatchSimpleEvent(this, 'statechange'); 253 }, 254 255 get shortFormat() { 256 return this.shortFormat_; 257 }, 258 259 set shortFormat(shortFormat) { 260 this.shortFormat_ = shortFormat; 261 this.querySelector('#short-format').checked = shortFormat; 262 if (this.results_) 263 this.results_.shortFormat = shortFormat; 264 if (!this.suppressStateChange_) 265 tr.b.dispatchSimpleEvent(this, 'statechange'); 266 }, 267 268 onShortFormatClick_: function(e) { 269 this.shortFormat_ = this.querySelector('#short-format').checked; 270 this.updateShortFormResultsDisplay_(); 271 this.updateResultsGivenShortFormat_(); 272 if (!this.suppressStateChange_) 273 tr.b.dispatchSimpleEvent(this, 'statechange'); 274 }, 275 276 updateShortFormResultsDisplay_: function() { 277 var display = this.shortFormat_ ? '' : 'none'; 278 this.querySelector('#shortform-results').style.display = display; 279 }, 280 281 updateResultsGivenShortFormat_: function() { 282 if (!this.results_) 283 return; 284 285 if (this.testFilterString_.length || this.testSuiteName_.length) 286 this.results_.showHTMLOutput = true; 287 else 288 this.results_.showHTMLOutput = false; 289 this.results_.showPendingAndPassedTests = this.shortFormat_; 290 }, 291 292 get testTypeToRun() { 293 return this.testTypeToRun_; 294 }, 295 296 set testTypeToRun(testTypeToRun) { 297 this.testTypeToRun_ = testTypeToRun; 298 var sel; 299 switch (testTypeToRun) { 300 case tr.b.unittest.TestTypes.UNITTEST: 301 sel = 'input[name=test-type-to-run][value=unit]'; 302 break; 303 case tr.b.unittest.TestTypes.PERFTEST: 304 sel = 'input[name=test-type-to-run][value=perf]'; 305 break; 306 case ALL_TEST_TYPES: 307 sel = 'input[name=test-type-to-run][value=all]'; 308 break; 309 default: 310 throw new Error('Invalid test type to run: ' + testTypeToRun); 311 } 312 this.querySelector(sel).checked = true; 313 this.scheduleRerun_(); 314 if (!this.suppressStateChange_) 315 tr.b.dispatchSimpleEvent(this, 'statechange'); 316 }, 317 318 onTestTypeToRunClick_: function(e) { 319 switch (e.target.value) { 320 case 'unit': 321 this.testTypeToRun_ = tr.b.unittest.TestTypes.UNITTEST; 322 break; 323 case 'perf': 324 this.testTypeToRun_ = tr.b.unittest.TestTypes.PERFTEST; 325 break; 326 case 'all': 327 this.testTypeToRun_ = ALL_TEST_TYPES; 328 break; 329 default: 330 throw new Error('Inalid test type: ' + e.target.value); 331 } 332 333 this.scheduleRerun_(); 334 if (!this.suppressStateChange_) 335 tr.b.dispatchSimpleEvent(this, 'statechange'); 336 }, 337 338 onTestPassed_: function() { 339 this.querySelector('#shortform-results'). 340 appendChild(document.createTextNode('.')); 341 }, 342 343 onTestFailed_: function() { 344 var span = document.createElement('span'); 345 span.classList.add('fail'); 346 span.appendChild(document.createTextNode('F')); 347 this.querySelector('#shortform-results').appendChild(span); 348 }, 349 350 onTestFlaky_: function() { 351 var span = document.createElement('span'); 352 span.classList.add('flaky'); 353 span.appendChild(document.createTextNode('~')); 354 this.querySelector('#shortform-results').appendChild(span); 355 }, 356 357 onResultsStatsChanged_: function() { 358 var statsEl = this.querySelector('#stats'); 359 var stats = this.results_.getStats(); 360 var numTestsOverall = this.runner_.testCases.length; 361 var numTestsThatRan = stats.numTestsThatPassed + 362 stats.numTestsThatFailed + stats.numFlakyTests; 363 statsEl.innerHTML = 364 '<span>' + numTestsThatRan + '/' + numTestsOverall + 365 '</span> tests run, ' + 366 '<span class="unittest-failed">' + stats.numTestsThatFailed + 367 '</span> failures, ' + 368 '<span class="unittest-flaky">' + stats.numFlakyTests + 369 '</span> flaky, ' + 370 ' in ' + stats.totalRunTime.toFixed(2) + 'ms.'; 371 }, 372 373 scheduleRerun_: function() { 374 if (this.rerunPending_) 375 return; 376 if (this.runner_) { 377 this.rerunPending_ = true; 378 this.runner_.beginToStopRunning(); 379 var doRerun = function() { 380 this.rerunPending_ = false; 381 this.scheduleRerun_(); 382 }.bind(this); 383 this.runner_.runCompletedPromise.then( 384 doRerun, doRerun); 385 return; 386 } 387 this.beginRunning_(); 388 }, 389 390 beginRunning_: function() { 391 var resultsContainer = this.querySelector('#results-container'); 392 if (this.results_) { 393 this.results_.removeEventListener('testpassed', this.onTestPassed_); 394 this.results_.removeEventListener('testfailed', this.onTestFailed_); 395 this.results_.removeEventListener('testflaky', this.onTestFlaky_); 396 this.results_.removeEventListener('statschange', 397 this.onResultsStatsChanged_); 398 delete this.results_.getHRefForTestCase; 399 resultsContainer.removeChild(this.results_); 400 } 401 402 this.results_ = new tr.b.unittest.HTMLTestResults(); 403 this.results_.headless = this.headless_; 404 this.results_.getHRefForTestCase = this.getHRefForTestCase.bind(this); 405 this.updateResultsGivenShortFormat_(); 406 407 this.results_.shortFormat = this.shortFormat_; 408 this.results_.addEventListener('testpassed', this.onTestPassed_); 409 this.results_.addEventListener('testfailed', this.onTestFailed_); 410 this.results_.addEventListener('testflaky', this.onTestFlaky_); 411 this.results_.addEventListener('statschange', 412 this.onResultsStatsChanged_); 413 resultsContainer.appendChild(this.results_); 414 415 var tests = this.allTests_.filter(function(test) { 416 var i = test.fullyQualifiedName.indexOf(this.testFilterString_); 417 if (i == -1) 418 return false; 419 if (this.testTypeToRun_ !== ALL_TEST_TYPES && 420 test.testType !== this.testTypeToRun_) 421 return false; 422 return true; 423 }, this); 424 425 this.runner_ = new tr.b.unittest.TestRunner(this.results_, tests); 426 this.runner_.beginRunning(); 427 428 this.runner_.runCompletedPromise.then( 429 this.runCompleted_.bind(this), 430 this.runCompleted_.bind(this)); 431 }, 432 433 setState: function(state, opt_suppressStateChange) { 434 this.suppressStateChange_ = true; 435 if (state.testFilterString !== undefined) 436 this.testFilterString = state.testFilterString; 437 else 438 this.testFilterString = ''; 439 440 if (state.shortFormat === undefined) 441 this.shortFormat = false; 442 else 443 this.shortFormat = state.shortFormat; 444 445 if (state.testTypeToRun === undefined) 446 this.testTypeToRun = tr.b.unittest.TestTypes.UNITTEST; 447 else 448 this.testTypeToRun = state.testTypeToRun; 449 450 this.testSuiteName_ = state.testSuiteName || ''; 451 this.headless_ = state.headless || false; 452 453 if (!opt_suppressStateChange) 454 this.suppressStateChange_ = false; 455 456 this.onShortFormatClick_(); 457 this.scheduleRerun_(); 458 this.suppressStateChange_ = false; 459 }, 460 461 getDefaultState: function() { 462 return { 463 testFilterString: '', 464 testSuiteName: '', 465 shortFormat: false, 466 testTypeToRun: tr.b.unittest.TestTypes.UNITTEST 467 }; 468 }, 469 470 getState: function() { 471 return { 472 testFilterString: this.testFilterString_, 473 testSuiteName: this.testSuiteName_, 474 shortFormat: this.shortFormat_, 475 testTypeToRun: this.testTypeToRun_ 476 }; 477 }, 478 479 getHRefForTestCase: function(testCases) { 480 return undefined; 481 }, 482 483 runCompleted_: function() { 484 this.runner_ = undefined; 485 } 486 }; 487 488 function loadAndRunTests(runnerConfig) { 489 490 // The test runner no-ops pushState so keep it around. 491 var realWindowHistoryPushState = window.history.pushState.bind( 492 window.history); 493 494 function stateToSearchString(defaultState, state) { 495 var parts = []; 496 for (var k in state) { 497 if (state[k] === defaultState[k]) 498 continue; 499 var v = state[k]; 500 var kv; 501 if (v === true) { 502 kv = k; 503 } else if (v === false) { 504 kv = k + '=false'; 505 } else if (v === '') { 506 continue; 507 } else { 508 kv = k + '=' + v; 509 } 510 parts.push(kv); 511 } 512 return parts.join('&'); 513 } 514 515 function stateFromSearchString(string) { 516 var state = {}; 517 string.split('&').forEach(function(part) { 518 if (part == '') 519 return; 520 var kv = part.split('='); 521 var k, v; 522 if (kv.length == 1) { 523 k = kv[0]; 524 v = true; 525 } else { 526 k = kv[0]; 527 if (kv[1] == 'false') 528 v = false; 529 else 530 v = kv[1]; 531 } 532 state[k] = v; 533 }); 534 return state; 535 } 536 537 function getSuiteRelpathsToLoad(state) { 538 if (state.testSuiteName) { 539 return new Promise(function(resolve) { 540 var parts = state.testSuiteName.split('.'); 541 var testSuiteRelPath = '/' + parts.join('/') + '.html'; 542 543 var suiteRelpathsToLoad = [testSuiteRelPath]; 544 resolve(suiteRelpathsToLoad); 545 }); 546 } 547 return runnerConfig.getAllSuiteRelPathsAsync(); 548 } 549 550 551 function loadAndRunTestsImpl() { 552 var state = stateFromSearchString( 553 window.location.search.substring(1)); 554 updateTitle(state); 555 556 557 showLoadingOverlay(); 558 559 var loader; 560 var p = getSuiteRelpathsToLoad(state); 561 p = p.then( 562 function(suiteRelpathsToLoad) { 563 loader = new tr.b.unittest.SuiteLoader(suiteRelpathsToLoad); 564 return loader.allSuitesLoadedPromise; 565 }, 566 function(e) { 567 hideLoadingOverlay(); 568 throw e; 569 }); 570 p = p.then( 571 function() { 572 hideLoadingOverlay(); 573 Polymer.whenReady(function() { 574 runTests(loader, state); 575 }); 576 }, 577 function(err) { 578 hideLoadingOverlay(); 579 tr.showPanic('Module loading failure', err); 580 throw err; 581 }); 582 return p; 583 } 584 585 function showLoadingOverlay() { 586 var overlay = document.createElement('div'); 587 overlay.id = 'tests-loading-overlay'; 588 overlay.style.backgroundColor = 'white'; 589 overlay.style.boxSizing = 'border-box'; 590 overlay.style.color = 'black'; 591 overlay.style.display = 'flex'; 592 overlay.style.height = '100%'; 593 overlay.style.left = 0; 594 overlay.style.padding = '8px'; 595 overlay.style.position = 'fixed'; 596 overlay.style.top = 0; 597 overlay.style.flexDirection = 'column'; 598 overlay.style.width = '100%'; 599 600 var element = document.createElement('div'); 601 element.style.flex = '1 1 auto'; 602 element.style.overflow = 'auto'; 603 overlay.appendChild(element); 604 605 element.textContent = 'Loading tests...'; 606 document.body.appendChild(overlay); 607 } 608 function hideLoadingOverlay() { 609 var overlay = document.body.querySelector('#tests-loading-overlay'); 610 document.body.removeChild(overlay); 611 } 612 613 function updateTitle(state) { 614 var testFilterString = state.testFilterString || ''; 615 var testSuiteName = state.testSuiteName || ''; 616 617 var title; 618 if (testSuiteName && testFilterString.length) { 619 title = testFilterString + ' in ' + testSuiteName; 620 } else if (testSuiteName) { 621 title = testSuiteName; 622 } else if (testFilterString) { 623 title = testFilterString + ' in all tests'; 624 } else { 625 title = runnerConfig.title; 626 } 627 628 if (state.shortFormat) 629 title += '(s)'; 630 document.title = title; 631 var runner = document.querySelector('x-base-interactive-test-runner'); 632 if (runner) 633 runner.title = title; 634 } 635 636 function runTests(loader, state) { 637 var runner = new tr.b.unittest.InteractiveTestRunner(); 638 runner.style.width = '100%'; 639 runner.style.height = '100%'; 640 runner.testLinks = runnerConfig.testLinks; 641 runner.allTests = loader.getAllTests(); 642 document.body.appendChild(runner); 643 644 runner.setState(state); 645 updateTitle(state); 646 647 runner.addEventListener('statechange', function() { 648 var state = runner.getState(); 649 var stateString = stateToSearchString(runner.getDefaultState(), 650 state); 651 if (window.location.search.substring(1) == stateString) 652 return; 653 654 updateTitle(state); 655 var stateURL; 656 if (stateString.length > 0) 657 stateURL = window.location.pathname + '?' + stateString; 658 else 659 stateURL = window.location.pathname; 660 realWindowHistoryPushState(state, document.title, stateURL); 661 }); 662 663 window.addEventListener('popstate', function(state) { 664 runner.setState(state, true); 665 }); 666 667 runner.getHRefForTestCase = function(testCase) { 668 var state = runner.getState(); 669 if (state.testFilterString === '' && 670 state.testSuiteName === '') { 671 state.testSuiteName = testCase.suite.name; 672 state.testFilterString = ''; 673 state.shortFormat = false; 674 } else { 675 state.testSuiteName = testCase.suite.name; 676 state.testFilterString = testCase.name; 677 state.shortFormat = false; 678 } 679 var stateString = stateToSearchString(runner.getDefaultState(), 680 state); 681 if (stateString.length > 0) 682 return window.location.pathname + '?' + stateString; 683 else 684 return window.location.pathname; 685 }; 686 } 687 688 loadAndRunTestsImpl(); 689 } 690 691 return { 692 InteractiveTestRunner: InteractiveTestRunner, 693 loadAndRunTests: loadAndRunTests 694 }; 695}); 696</script> 697