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