blob: 79a5649275d4932fcc1615536fb60cce087c6133 [file] [log] [blame]
<!DOCTYPE html>
<!--
Copyright (c) 2014 The Chromium Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file.
-->
<link rel="import" href="/tracing/base/base.html">
<link rel="import" href="/tracing/base/unittest.html">
<link rel="import" href="/tracing/base/unittest/suite_loader.html">
<link rel="import" href="/tracing/base/unittest/test_runner.html">
<link rel="import" href="/tracing/base/unittest/html_test_results.html">
<link rel="import" href="/tracing/ui/base/utils.html">
<style>
x-base-interactive-test-runner {
display: flex;
flex-direction: column;
flex: 0 0 auto;
}
x-base-interactive-test-runner > * {
flex: 0 0 auto;
}
x-base-interactive-test-runner > #title {
font-size: 16pt;
}
x-base-interactive-test-runner {
font-family: sans-serif;
}
x-base-interactive-test-runner > h1 {
margin: 5px 0px 10px 0px;
}
x-base-interactive-test-runner > #stats {
}
x-base-interactive-test-runner > #controls {
display: block;
margin-bottom: 5px;
}
x-base-interactive-test-runner > #controls > ul {
list-style-type: none;
padding: 0;
margin: 0;
}
x-base-interactive-test-runner > #controls > ul > li {
float: left;
margin-right: 10px;
padding-top: 5px;
padding-bottom: 5px;
}
x-base-interactive-test-runner > #shortform-results {
color: green;
height; 40px;
word-wrap: break-word;
}
x-base-interactive-test-runner > #shortform-results > .fail {
color: darkred;
font-weight: bold;
}
x-base-interactive-test-runner > #shortform-results > .flaky {
color: darkorange;
}
x-base-interactive-test-runner > #results-container {
flex: 1 1 auto;
min-height: 0;
overflow: auto;
padding: 0 4px 0 4px;
}
.unittest-pending {
color: orange;
}
.unittest-running {
color: orange;
font-weight: bold;
}
.unittest-passed {
color: darkgreen;
}
.unittest-failed {
color: darkred;
font-weight: bold;
}
.unittest-flaky {
color: darkorange;
}
.unittest-exception {
color: red;
font-weight: bold;
}
.unittest-failure {
border: 1px solid grey;
border-radius: 5px;
padding: 5px;
}
</style>
<template id="x-base-interactive-test-runner-template">
<h1 id="title">Tests</h1>
<div id="stats"></div>
<div id="controls">
<ul id="links">
</ul>
<div style="clear: both;"></div>
<div>
<span>
<label>
<input type="radio" name="test-type-to-run" value="unit" />
Run unit tests
</label>
</span>
<span>
<label>
<input type="radio" name="test-type-to-run" value="perf" />
Run perf tests
</label>
</span>
<span>
<label>
<input type="radio" name="test-type-to-run" value="all" />
Run all tests
</label>
</span>
</div>
<span>
<label>
<input type="checkbox" id="short-format" /> Short format</label>
</span>
</div>
<div id="shortform-results">
</div>
<div id="results-container">
</div>
</template>
<script>
'use strict';
tr.exportTo('tr.b.unittest', function() {
var THIS_DOC = document.currentScript.ownerDocument;
var ALL_TEST_TYPES = 'all';
/**
* @constructor
*/
var InteractiveTestRunner = tr.ui.b.define('x-base-interactive-test-runner');
InteractiveTestRunner.prototype = {
__proto__: HTMLUnknownElement.prototype,
decorate: function() {
this.allTests_ = undefined;
this.suppressStateChange_ = false;
this.testFilterString_ = '';
this.testTypeToRun_ = tr.b.unittest.TestTypes.UNITTEST;
this.shortFormat_ = false;
this.testSuiteName_ = '';
this.rerunPending_ = false;
this.runner_ = undefined;
this.results_ = undefined;
this.headless_ = false;
this.onResultsStatsChanged_ = this.onResultsStatsChanged_.bind(this);
this.onTestFailed_ = this.onTestFailed_.bind(this);
this.onTestFlaky_ = this.onTestFlaky_.bind(this);
this.onTestPassed_ = this.onTestPassed_.bind(this);
this.appendChild(tr.ui.b.instantiateTemplate(
'#x-base-interactive-test-runner-template', THIS_DOC));
this.querySelector(
'input[name=test-type-to-run][value=unit]').checked = true;
var testTypeToRunEls = tr.b.asArray(this.querySelectorAll(
'input[name=test-type-to-run]'));
testTypeToRunEls.forEach(
function(inputEl) {
inputEl.addEventListener(
'click', this.onTestTypeToRunClick_.bind(this));
}, this);
var shortFormatEl = this.querySelector('#short-format');
shortFormatEl.checked = this.shortFormat_;
shortFormatEl.addEventListener(
'click', this.onShortFormatClick_.bind(this));
this.updateShortFormResultsDisplay_();
// Oh, DOM, how I love you. Title is such a convenient property name and I
// refuse to change my worldview because of tooltips.
this.__defineSetter__(
'title',
function(title) {
this.querySelector('#title').textContent = title;
});
},
get allTests() {
return this.allTests_;
},
set allTests(allTests) {
this.allTests_ = allTests;
this.scheduleRerun_();
},
get testLinks() {
return this.testLinks_;
},
set testLinks(testLinks) {
this.testLinks_ = testLinks;
var linksEl = this.querySelector('#links');
linksEl.textContent = '';
this.testLinks_.forEach(function(l) {
var link = document.createElement('a');
link.href = l.linkPath;
link.textContent = l.title;
var li = document.createElement('li');
li.appendChild(link);
linksEl.appendChild(li);
}, this);
},
get testFilterString() {
return this.testFilterString_;
},
set testFilterString(testFilterString) {
this.testFilterString_ = testFilterString;
this.scheduleRerun_();
if (!this.suppressStateChange_)
tr.b.dispatchSimpleEvent(this, 'statechange');
},
get shortFormat() {
return this.shortFormat_;
},
set shortFormat(shortFormat) {
this.shortFormat_ = shortFormat;
this.querySelector('#short-format').checked = shortFormat;
if (this.results_)
this.results_.shortFormat = shortFormat;
if (!this.suppressStateChange_)
tr.b.dispatchSimpleEvent(this, 'statechange');
},
onShortFormatClick_: function(e) {
this.shortFormat_ = this.querySelector('#short-format').checked;
this.updateShortFormResultsDisplay_();
this.updateResultsGivenShortFormat_();
if (!this.suppressStateChange_)
tr.b.dispatchSimpleEvent(this, 'statechange');
},
updateShortFormResultsDisplay_: function() {
var display = this.shortFormat_ ? '' : 'none';
this.querySelector('#shortform-results').style.display = display;
},
updateResultsGivenShortFormat_: function() {
if (!this.results_)
return;
if (this.testFilterString_.length || this.testSuiteName_.length)
this.results_.showHTMLOutput = true;
else
this.results_.showHTMLOutput = false;
this.results_.showPendingAndPassedTests = this.shortFormat_;
},
get testTypeToRun() {
return this.testTypeToRun_;
},
set testTypeToRun(testTypeToRun) {
this.testTypeToRun_ = testTypeToRun;
var sel;
switch (testTypeToRun) {
case tr.b.unittest.TestTypes.UNITTEST:
sel = 'input[name=test-type-to-run][value=unit]';
break;
case tr.b.unittest.TestTypes.PERFTEST:
sel = 'input[name=test-type-to-run][value=perf]';
break;
case ALL_TEST_TYPES:
sel = 'input[name=test-type-to-run][value=all]';
break;
default:
throw new Error('Invalid test type to run: ' + testTypeToRun);
}
this.querySelector(sel).checked = true;
this.scheduleRerun_();
if (!this.suppressStateChange_)
tr.b.dispatchSimpleEvent(this, 'statechange');
},
onTestTypeToRunClick_: function(e) {
switch (e.target.value) {
case 'unit':
this.testTypeToRun_ = tr.b.unittest.TestTypes.UNITTEST;
break;
case 'perf':
this.testTypeToRun_ = tr.b.unittest.TestTypes.PERFTEST;
break;
case 'all':
this.testTypeToRun_ = ALL_TEST_TYPES;
break;
default:
throw new Error('Inalid test type: ' + e.target.value);
}
this.scheduleRerun_();
if (!this.suppressStateChange_)
tr.b.dispatchSimpleEvent(this, 'statechange');
},
onTestPassed_: function() {
this.querySelector('#shortform-results').
appendChild(document.createTextNode('.'));
},
onTestFailed_: function() {
var span = document.createElement('span');
span.classList.add('fail');
span.appendChild(document.createTextNode('F'));
this.querySelector('#shortform-results').appendChild(span);
},
onTestFlaky_: function() {
var span = document.createElement('span');
span.classList.add('flaky');
span.appendChild(document.createTextNode('~'));
this.querySelector('#shortform-results').appendChild(span);
},
onResultsStatsChanged_: function() {
var statsEl = this.querySelector('#stats');
var stats = this.results_.getStats();
var numTestsOverall = this.runner_.testCases.length;
var numTestsThatRan = stats.numTestsThatPassed +
stats.numTestsThatFailed + stats.numFlakyTests;
statsEl.innerHTML =
'<span>' + numTestsThatRan + '/' + numTestsOverall +
'</span> tests run, ' +
'<span class="unittest-failed">' + stats.numTestsThatFailed +
'</span> failures, ' +
'<span class="unittest-flaky">' + stats.numFlakyTests +
'</span> flaky, ' +
' in ' + stats.totalRunTime.toFixed(2) + 'ms.';
},
scheduleRerun_: function() {
if (this.rerunPending_)
return;
if (this.runner_) {
this.rerunPending_ = true;
this.runner_.beginToStopRunning();
var doRerun = function() {
this.rerunPending_ = false;
this.scheduleRerun_();
}.bind(this);
this.runner_.runCompletedPromise.then(
doRerun, doRerun);
return;
}
this.beginRunning_();
},
beginRunning_: function() {
var resultsContainer = this.querySelector('#results-container');
if (this.results_) {
this.results_.removeEventListener('testpassed', this.onTestPassed_);
this.results_.removeEventListener('testfailed', this.onTestFailed_);
this.results_.removeEventListener('testflaky', this.onTestFlaky_);
this.results_.removeEventListener('statschange',
this.onResultsStatsChanged_);
delete this.results_.getHRefForTestCase;
resultsContainer.removeChild(this.results_);
}
this.results_ = new tr.b.unittest.HTMLTestResults();
this.results_.headless = this.headless_;
this.results_.getHRefForTestCase = this.getHRefForTestCase.bind(this);
this.updateResultsGivenShortFormat_();
this.results_.shortFormat = this.shortFormat_;
this.results_.addEventListener('testpassed', this.onTestPassed_);
this.results_.addEventListener('testfailed', this.onTestFailed_);
this.results_.addEventListener('testflaky', this.onTestFlaky_);
this.results_.addEventListener('statschange',
this.onResultsStatsChanged_);
resultsContainer.appendChild(this.results_);
var tests = this.allTests_.filter(function(test) {
var i = test.fullyQualifiedName.indexOf(this.testFilterString_);
if (i == -1)
return false;
if (this.testTypeToRun_ !== ALL_TEST_TYPES &&
test.testType !== this.testTypeToRun_)
return false;
return true;
}, this);
this.runner_ = new tr.b.unittest.TestRunner(this.results_, tests);
this.runner_.beginRunning();
this.runner_.runCompletedPromise.then(
this.runCompleted_.bind(this),
this.runCompleted_.bind(this));
},
setState: function(state, opt_suppressStateChange) {
this.suppressStateChange_ = true;
if (state.testFilterString !== undefined)
this.testFilterString = state.testFilterString;
else
this.testFilterString = '';
if (state.shortFormat === undefined)
this.shortFormat = false;
else
this.shortFormat = state.shortFormat;
if (state.testTypeToRun === undefined)
this.testTypeToRun = tr.b.unittest.TestTypes.UNITTEST;
else
this.testTypeToRun = state.testTypeToRun;
this.testSuiteName_ = state.testSuiteName || '';
this.headless_ = state.headless || false;
if (!opt_suppressStateChange)
this.suppressStateChange_ = false;
this.onShortFormatClick_();
this.scheduleRerun_();
this.suppressStateChange_ = false;
},
getDefaultState: function() {
return {
testFilterString: '',
testSuiteName: '',
shortFormat: false,
testTypeToRun: tr.b.unittest.TestTypes.UNITTEST
};
},
getState: function() {
return {
testFilterString: this.testFilterString_,
testSuiteName: this.testSuiteName_,
shortFormat: this.shortFormat_,
testTypeToRun: this.testTypeToRun_
};
},
getHRefForTestCase: function(testCases) {
return undefined;
},
runCompleted_: function() {
this.runner_ = undefined;
}
};
function loadAndRunTests(runnerConfig) {
// The test runner no-ops pushState so keep it around.
var realWindowHistoryPushState = window.history.pushState.bind(
window.history);
function stateToSearchString(defaultState, state) {
var parts = [];
for (var k in state) {
if (state[k] === defaultState[k])
continue;
var v = state[k];
var kv;
if (v === true) {
kv = k;
} else if (v === false) {
kv = k + '=false';
} else if (v === '') {
continue;
} else {
kv = k + '=' + v;
}
parts.push(kv);
}
return parts.join('&');
}
function stateFromSearchString(string) {
var state = {};
string.split('&').forEach(function(part) {
if (part == '')
return;
var kv = part.split('=');
var k, v;
if (kv.length == 1) {
k = kv[0];
v = true;
} else {
k = kv[0];
if (kv[1] == 'false')
v = false;
else
v = kv[1];
}
state[k] = v;
});
return state;
}
function getSuiteRelpathsToLoad(state) {
if (state.testSuiteName) {
return new Promise(function(resolve) {
var parts = state.testSuiteName.split('.');
var testSuiteRelPath = '/' + parts.join('/') + '.html';
var suiteRelpathsToLoad = [testSuiteRelPath];
resolve(suiteRelpathsToLoad);
});
}
return runnerConfig.getAllSuiteRelPathsAsync();
}
function loadAndRunTestsImpl() {
var state = stateFromSearchString(
window.location.search.substring(1));
updateTitle(state);
showLoadingOverlay();
var loader;
var p = getSuiteRelpathsToLoad(state);
p = p.then(
function(suiteRelpathsToLoad) {
loader = new tr.b.unittest.SuiteLoader(suiteRelpathsToLoad);
return loader.allSuitesLoadedPromise;
},
function(e) {
hideLoadingOverlay();
throw e;
});
p = p.then(
function() {
hideLoadingOverlay();
Polymer.whenReady(function() {
runTests(loader, state);
});
},
function(err) {
hideLoadingOverlay();
tr.showPanic('Module loading failure', err);
throw err;
});
return p;
}
function showLoadingOverlay() {
var overlay = document.createElement('div');
overlay.id = 'tests-loading-overlay';
overlay.style.backgroundColor = 'white';
overlay.style.boxSizing = 'border-box';
overlay.style.color = 'black';
overlay.style.display = 'flex';
overlay.style.height = '100%';
overlay.style.left = 0;
overlay.style.padding = '8px';
overlay.style.position = 'fixed';
overlay.style.top = 0;
overlay.style.flexDirection = 'column';
overlay.style.width = '100%';
var element = document.createElement('div');
element.style.flex = '1 1 auto';
element.style.overflow = 'auto';
overlay.appendChild(element);
element.textContent = 'Loading tests...';
document.body.appendChild(overlay);
}
function hideLoadingOverlay() {
var overlay = document.body.querySelector('#tests-loading-overlay');
document.body.removeChild(overlay);
}
function updateTitle(state) {
var testFilterString = state.testFilterString || '';
var testSuiteName = state.testSuiteName || '';
var title;
if (testSuiteName && testFilterString.length) {
title = testFilterString + ' in ' + testSuiteName;
} else if (testSuiteName) {
title = testSuiteName;
} else if (testFilterString) {
title = testFilterString + ' in all tests';
} else {
title = runnerConfig.title;
}
if (state.shortFormat)
title += '(s)';
document.title = title;
var runner = document.querySelector('x-base-interactive-test-runner');
if (runner)
runner.title = title;
}
function runTests(loader, state) {
var runner = new tr.b.unittest.InteractiveTestRunner();
runner.style.width = '100%';
runner.style.height = '100%';
runner.testLinks = runnerConfig.testLinks;
runner.allTests = loader.getAllTests();
document.body.appendChild(runner);
runner.setState(state);
updateTitle(state);
runner.addEventListener('statechange', function() {
var state = runner.getState();
var stateString = stateToSearchString(runner.getDefaultState(),
state);
if (window.location.search.substring(1) == stateString)
return;
updateTitle(state);
var stateURL;
if (stateString.length > 0)
stateURL = window.location.pathname + '?' + stateString;
else
stateURL = window.location.pathname;
realWindowHistoryPushState(state, document.title, stateURL);
});
window.addEventListener('popstate', function(state) {
runner.setState(state, true);
});
runner.getHRefForTestCase = function(testCase) {
var state = runner.getState();
if (state.testFilterString === '' &&
state.testSuiteName === '') {
state.testSuiteName = testCase.suite.name;
state.testFilterString = '';
state.shortFormat = false;
} else {
state.testSuiteName = testCase.suite.name;
state.testFilterString = testCase.name;
state.shortFormat = false;
}
var stateString = stateToSearchString(runner.getDefaultState(),
state);
if (stateString.length > 0)
return window.location.pathname + '?' + stateString;
else
return window.location.pathname;
};
}
loadAndRunTestsImpl();
}
return {
InteractiveTestRunner: InteractiveTestRunner,
loadAndRunTests: loadAndRunTests
};
});
</script>