blob: c7c7246f35f950471bf2208565d5badb07fd9034 [file] [log] [blame]
// Copyright 2008 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview Utility for running multiple test files that utilize the same
* interface as goog.testing.TestRunner. Each test is run in series and their
* results aggregated. The main usecase for the MultiTestRunner is to allow
* the testing of all tests in a project locally.
*
*/
goog.provide('goog.testing.MultiTestRunner');
goog.provide('goog.testing.MultiTestRunner.TestFrame');
goog.require('goog.Timer');
goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.dom.classlist');
goog.require('goog.events.EventHandler');
goog.require('goog.functions');
goog.require('goog.string');
goog.require('goog.ui.Component');
goog.require('goog.ui.ServerChart');
goog.require('goog.ui.TableSorter');
/**
* A component for running multiple tests within the browser.
* @param {goog.dom.DomHelper=} opt_domHelper A DOM helper.
* @extends {goog.ui.Component}
* @constructor
* @final
*/
goog.testing.MultiTestRunner = function(opt_domHelper) {
goog.ui.Component.call(this, opt_domHelper);
/**
* Array of tests to execute, when combined with the base path this should be
* a relative path to the test from the page containing the multi testrunner.
* @type {Array<string>}
* @private
*/
this.allTests_ = [];
/**
* Tests that match the filter function.
* @type {Array<string>}
* @private
*/
this.activeTests_ = [];
/**
* An event handler for handling events.
* @type {goog.events.EventHandler<!goog.testing.MultiTestRunner>}
* @private
*/
this.eh_ = new goog.events.EventHandler(this);
/**
* A table sorter for the stats.
* @type {goog.ui.TableSorter}
* @private
*/
this.tableSorter_ = new goog.ui.TableSorter(this.dom_);
};
goog.inherits(goog.testing.MultiTestRunner, goog.ui.Component);
/**
* Default maximimum amount of time to spend at each stage of the test.
* @type {number}
*/
goog.testing.MultiTestRunner.DEFAULT_TIMEOUT_MS = 45 * 1000;
/**
* Messages corresponding to the numeric states.
* @type {Array<string>}
*/
goog.testing.MultiTestRunner.STATES = [
'waiting for test runner',
'initializing tests',
'waiting for tests to finish'
];
/**
* The test suite's name.
* @type {string} name
* @private
*/
goog.testing.MultiTestRunner.prototype.name_ = '';
/**
* The base path used to resolve files within the allTests_ array.
* @type {string}
* @private
*/
goog.testing.MultiTestRunner.prototype.basePath_ = '';
/**
* A set of tests that have finished. All extant keys map to true.
* @type {Object<boolean>}
* @private
*/
goog.testing.MultiTestRunner.prototype.finished_ = null;
/**
* Whether the report should contain verbose information about the passes.
* @type {boolean}
* @private
*/
goog.testing.MultiTestRunner.prototype.verbosePasses_ = false;
/**
* Whether to hide passing tests completely in the report, makes verbosePasses_
* obsolete.
* @type {boolean}
* @private
*/
goog.testing.MultiTestRunner.prototype.hidePasses_ = false;
/**
* Flag used to tell the test runner to stop after the current test.
* @type {boolean}
* @private
*/
goog.testing.MultiTestRunner.prototype.stopped_ = false;
/**
* Flag indicating whether the test runner is active.
* @type {boolean}
* @private
*/
goog.testing.MultiTestRunner.prototype.active_ = false;
/**
* Index of the next test to run.
* @type {number}
* @private
*/
goog.testing.MultiTestRunner.prototype.startedCount_ = 0;
/**
* Count of the results received so far.
* @type {number}
* @private
*/
goog.testing.MultiTestRunner.prototype.resultCount_ = 0;
/**
* Number of passes so far.
* @type {number}
* @private
*/
goog.testing.MultiTestRunner.prototype.passes_ = 0;
/**
* Timestamp for the current start time.
* @type {number}
* @private
*/
goog.testing.MultiTestRunner.prototype.startTime_ = 0;
/**
* Only tests whose paths patch this filter function will be
* executed.
* @type {function(string): boolean}
* @private
*/
goog.testing.MultiTestRunner.prototype.filterFn_ = goog.functions.TRUE;
/**
* Number of milliseconds to wait for loading and initialization steps.
* @type {number}
* @private
*/
goog.testing.MultiTestRunner.prototype.timeoutMs_ =
goog.testing.MultiTestRunner.DEFAULT_TIMEOUT_MS;
/**
* An array of objects containing stats about the tests.
* @type {Array<Object>?}
* @private
*/
goog.testing.MultiTestRunner.prototype.stats_ = null;
/**
* Reference to the start button element.
* @type {Element}
* @private
*/
goog.testing.MultiTestRunner.prototype.startButtonEl_ = null;
/**
* Reference to the stop button element.
* @type {Element}
* @private
*/
goog.testing.MultiTestRunner.prototype.stopButtonEl_ = null;
/**
* Reference to the log element.
* @type {Element}
* @private
*/
goog.testing.MultiTestRunner.prototype.logEl_ = null;
/**
* Reference to the report element.
* @type {Element}
* @private
*/
goog.testing.MultiTestRunner.prototype.reportEl_ = null;
/**
* Reference to the stats element.
* @type {Element}
* @private
*/
goog.testing.MultiTestRunner.prototype.statsEl_ = null;
/**
* Reference to the progress bar's element.
* @type {Element}
* @private
*/
goog.testing.MultiTestRunner.prototype.progressEl_ = null;
/**
* Reference to the progress bar's inner row element.
* @type {Element}
* @private
*/
goog.testing.MultiTestRunner.prototype.progressRow_ = null;
/**
* Reference to the log tab.
* @type {Element}
* @private
*/
goog.testing.MultiTestRunner.prototype.logTabEl_ = null;
/**
* Reference to the report tab.
* @type {Element}
* @private
*/
goog.testing.MultiTestRunner.prototype.reportTabEl_ = null;
/**
* Reference to the stats tab.
* @type {Element}
* @private
*/
goog.testing.MultiTestRunner.prototype.statsTabEl_ = null;
/**
* The number of tests to run at a time.
* @type {number}
* @private
*/
goog.testing.MultiTestRunner.prototype.poolSize_ = 1;
/**
* The size of the stats bucket for the number of files loaded histogram.
* @type {number}
* @private
*/
goog.testing.MultiTestRunner.prototype.numFilesStatsBucketSize_ = 20;
/**
* The size of the stats bucket in ms for the run time histogram.
* @type {number}
* @private
*/
goog.testing.MultiTestRunner.prototype.runTimeStatsBucketSize_ = 500;
/**
* Sets the name for the test suite.
* @param {string} name The suite's name.
* @return {!goog.testing.MultiTestRunner} Instance for chaining.
*/
goog.testing.MultiTestRunner.prototype.setName = function(name) {
this.name_ = name;
return this;
};
/**
* Returns the name for the test suite.
* @return {string} The name for the test suite.
*/
goog.testing.MultiTestRunner.prototype.getName = function() {
return this.name_;
};
/**
* Sets the basepath that tests added using addTests are resolved with.
* @param {string} path The relative basepath.
* @return {!goog.testing.MultiTestRunner} Instance for chaining.
*/
goog.testing.MultiTestRunner.prototype.setBasePath = function(path) {
this.basePath_ = path;
return this;
};
/**
* Returns the basepath that tests added using addTests are resolved with.
* @return {string} The basepath that tests added using addTests are resolved
* with.
*/
goog.testing.MultiTestRunner.prototype.getBasePath = function() {
return this.basePath_;
};
/**
* Sets whether the report should contain verbose information for tests that
* pass.
* @param {boolean} verbose Whether report should be verbose.
* @return {!goog.testing.MultiTestRunner} Instance for chaining.
*/
goog.testing.MultiTestRunner.prototype.setVerbosePasses = function(verbose) {
this.verbosePasses_ = verbose;
return this;
};
/**
* Returns whether the report should contain verbose information for tests that
* pass.
* @return {boolean} Whether the report should contain verbose information for
* tests that pass.
*/
goog.testing.MultiTestRunner.prototype.getVerbosePasses = function() {
return this.verbosePasses_;
};
/**
* Sets whether the report should contain passing tests at all, makes
* setVerbosePasses obsolete.
* @param {boolean} hide Whether report should not contain passing tests.
* @return {!goog.testing.MultiTestRunner} Instance for chaining.
*/
goog.testing.MultiTestRunner.prototype.setHidePasses = function(hide) {
this.hidePasses_ = hide;
return this;
};
/**
* Returns whether the report should contain passing tests at all, makes
* setVerbosePasses obsolete.
* @return {boolean} Whether the report should contain passing tests at all,
* makes setVerbosePasses obsolete.
*/
goog.testing.MultiTestRunner.prototype.getHidePasses = function() {
return this.hidePasses_;
};
/**
* Sets the bucket sizes for the histograms.
* @param {number} f Bucket size for num files loaded histogram.
* @param {number} t Bucket size for run time histogram.
* @return {!goog.testing.MultiTestRunner} Instance for chaining.
*/
goog.testing.MultiTestRunner.prototype.setStatsBucketSizes = function(f, t) {
this.numFilesStatsBucketSize_ = f;
this.runTimeStatsBucketSize_ = t;
return this;
};
/**
* Sets the number of milliseconds to wait for the page to load, initialize and
* run the tests.
* @param {number} timeout Time in milliseconds.
* @return {!goog.testing.MultiTestRunner} Instance for chaining.
*/
goog.testing.MultiTestRunner.prototype.setTimeout = function(timeout) {
this.timeoutMs_ = timeout;
return this;
};
/**
* Returns the number of milliseconds to wait for the page to load, initialize
* and run the tests.
* @return {number} The number of milliseconds to wait for the page to load,
* initialize and run the tests.
*/
goog.testing.MultiTestRunner.prototype.getTimeout = function() {
return this.timeoutMs_;
};
/**
* Sets the number of tests that can be run at the same time. This only improves
* performance due to the amount of time spent loading the tests.
* @param {number} size The number of tests to run at a time.
* @return {!goog.testing.MultiTestRunner} Instance for chaining.
*/
goog.testing.MultiTestRunner.prototype.setPoolSize = function(size) {
this.poolSize_ = size;
return this;
};
/**
* Returns the number of tests that can be run at the same time. This only
* improves performance due to the amount of time spent loading the tests.
* @return {number} The number of tests that can be run at the same time. This
* only improves performance due to the amount of time spent loading the
* tests.
*/
goog.testing.MultiTestRunner.prototype.getPoolSize = function() {
return this.poolSize_;
};
/**
* Sets a filter function. Only test paths that match the filter function
* will be executed.
* @param {function(string): boolean} filterFn Filters test paths.
* @return {!goog.testing.MultiTestRunner} Instance for chaining.
*/
goog.testing.MultiTestRunner.prototype.setFilterFunction = function(filterFn) {
this.filterFn_ = filterFn;
return this;
};
/**
* Returns a filter function. Only test paths that match the filter function
* will be executed.
* @return {function(string): boolean} A filter function. Only test paths that
* match the filter function will be executed.
*/
goog.testing.MultiTestRunner.prototype.getFilterFunction = function() {
return this.filterFn_;
};
/**
* Adds an array of tests to the tests that the test runner should execute.
* @param {Array<string>} tests Adds tests to the test runner.
* @return {!goog.testing.MultiTestRunner} Instance for chaining.
*/
goog.testing.MultiTestRunner.prototype.addTests = function(tests) {
goog.array.extend(this.allTests_, tests);
return this;
};
/**
* Returns the list of all tests added to the runner.
* @return {Array<string>} The list of all tests added to the runner.
*/
goog.testing.MultiTestRunner.prototype.getAllTests = function() {
return this.allTests_;
};
/**
* Returns the list of tests that will be run when start() is called.
* @return {!Array<string>} The list of tests that will be run when start() is
* called.
*/
goog.testing.MultiTestRunner.prototype.getTestsToRun = function() {
return goog.array.filter(this.allTests_, this.filterFn_);
};
/**
* Returns a list of tests from runner that have been marked as failed.
* @return {!Array<string>} A list of tests from runner that have been marked
* as failed.
*/
goog.testing.MultiTestRunner.prototype.getTestsThatFailed = function() {
var stats = this.stats_;
var failedTests = [];
if (stats) {
for (var i = 0, stat; stat = stats[i]; i++) {
if (!stat.success) {
failedTests.push(stat.testFile);
}
}
}
return failedTests;
};
/**
* Deletes and re-creates the progress table inside the progess element.
* @private
*/
goog.testing.MultiTestRunner.prototype.resetProgressDom_ = function() {
goog.dom.removeChildren(this.progressEl_);
var progressTable = this.dom_.createDom('table');
var progressTBody = this.dom_.createDom('tbody');
this.progressRow_ = this.dom_.createDom('tr');
for (var i = 0; i < this.activeTests_.length; i++) {
var progressCell = this.dom_.createDom('td');
this.progressRow_.appendChild(progressCell);
}
progressTBody.appendChild(this.progressRow_);
progressTable.appendChild(progressTBody);
this.progressEl_.appendChild(progressTable);
};
/** @override */
goog.testing.MultiTestRunner.prototype.createDom = function() {
goog.testing.MultiTestRunner.superClass_.createDom.call(this);
var el = this.getElement();
el.className = goog.getCssName('goog-testrunner');
this.progressEl_ = this.dom_.createDom('div');
this.progressEl_.className = goog.getCssName('goog-testrunner-progress');
el.appendChild(this.progressEl_);
var buttons = this.dom_.createDom('div');
buttons.className = goog.getCssName('goog-testrunner-buttons');
this.startButtonEl_ = this.dom_.createDom('button', null, 'Start');
this.stopButtonEl_ =
this.dom_.createDom('button', {'disabled': true}, 'Stop');
buttons.appendChild(this.startButtonEl_);
buttons.appendChild(this.stopButtonEl_);
el.appendChild(buttons);
this.eh_.listen(this.startButtonEl_, 'click',
this.onStartClicked_);
this.eh_.listen(this.stopButtonEl_, 'click',
this.onStopClicked_);
this.logEl_ = this.dom_.createElement('div');
this.logEl_.className = goog.getCssName('goog-testrunner-log');
el.appendChild(this.logEl_);
this.reportEl_ = this.dom_.createElement('div');
this.reportEl_.className = goog.getCssName('goog-testrunner-report');
this.reportEl_.style.display = 'none';
el.appendChild(this.reportEl_);
this.statsEl_ = this.dom_.createElement('div');
this.statsEl_.className = goog.getCssName('goog-testrunner-stats');
this.statsEl_.style.display = 'none';
el.appendChild(this.statsEl_);
this.logTabEl_ = this.dom_.createDom('div', null, 'Log');
this.logTabEl_.className = goog.getCssName('goog-testrunner-logtab') + ' ' +
goog.getCssName('goog-testrunner-activetab');
el.appendChild(this.logTabEl_);
this.reportTabEl_ = this.dom_.createDom('div', null, 'Report');
this.reportTabEl_.className = goog.getCssName('goog-testrunner-reporttab');
el.appendChild(this.reportTabEl_);
this.statsTabEl_ = this.dom_.createDom('div', null, 'Stats');
this.statsTabEl_.className = goog.getCssName('goog-testrunner-statstab');
el.appendChild(this.statsTabEl_);
this.eh_.listen(this.logTabEl_, 'click', this.onLogTabClicked_);
this.eh_.listen(this.reportTabEl_, 'click', this.onReportTabClicked_);
this.eh_.listen(this.statsTabEl_, 'click', this.onStatsTabClicked_);
};
/** @override */
goog.testing.MultiTestRunner.prototype.disposeInternal = function() {
goog.testing.MultiTestRunner.superClass_.disposeInternal.call(this);
this.tableSorter_.dispose();
this.eh_.dispose();
this.startButtonEl_ = null;
this.stopButtonEl_ = null;
this.logEl_ = null;
this.reportEl_ = null;
this.progressEl_ = null;
this.logTabEl_ = null;
this.reportTabEl_ = null;
this.statsTabEl_ = null;
this.statsEl_ = null;
};
/**
* Starts executing the tests.
*/
goog.testing.MultiTestRunner.prototype.start = function() {
this.startButtonEl_.disabled = true;
this.stopButtonEl_.disabled = false;
this.stopped_ = false;
this.active_ = true;
this.finished_ = {};
this.activeTests_ = this.getTestsToRun();
this.startedCount_ = 0;
this.resultCount_ = 0;
this.passes_ = 0;
this.stats_ = [];
this.startTime_ = goog.now();
this.resetProgressDom_();
goog.dom.removeChildren(this.logEl_);
this.resetReport_();
this.clearStats_();
this.showTab_(0);
// Ensure the pool isn't too big.
while (this.getChildCount() > this.poolSize_) {
this.removeChildAt(0, true).dispose();
}
// Start a test in each runner.
for (var i = 0; i < this.poolSize_; i++) {
if (i >= this.getChildCount()) {
var testFrame = new goog.testing.MultiTestRunner.TestFrame(
this.basePath_, this.timeoutMs_, this.verbosePasses_, this.dom_);
this.addChild(testFrame, true);
}
this.runNextTest_(
/** @type {goog.testing.MultiTestRunner.TestFrame} */
(this.getChildAt(i)));
}
};
/**
* Logs a message to the log window.
* @param {string} msg A message to log.
*/
goog.testing.MultiTestRunner.prototype.log = function(msg) {
if (msg != '.') {
msg = this.getTimeStamp_() + ' : ' + msg;
}
this.logEl_.appendChild(this.dom_.createDom('div', null, msg));
// Autoscroll if we're near the bottom.
var top = this.logEl_.scrollTop;
var height = this.logEl_.scrollHeight - this.logEl_.offsetHeight;
if (top == 0 || top > height - 50) {
this.logEl_.scrollTop = height;
}
};
/**
* Processes a result returned from a TestFrame. If there are tests remaining
* it will trigger the next one to be run, otherwise if there are no tests and
* all results have been recieved then it will call finish.
* @param {goog.testing.MultiTestRunner.TestFrame} frame The frame that just
* finished.
*/
goog.testing.MultiTestRunner.prototype.processResult = function(frame) {
var success = frame.isSuccess();
var report = frame.getReport();
var test = frame.getTestFile();
this.stats_.push(frame.getStats());
this.finished_[test] = true;
var prefix = success ? '' : '*** FAILURE *** ';
this.log(prefix +
this.trimFileName_(test) + ' : ' + (success ? 'Passed' : 'Failed'));
this.resultCount_++;
if (success) {
this.passes_++;
}
this.drawProgressSegment_(test, success);
this.writeCurrentSummary_();
if (!(success && this.hidePasses_)) {
this.drawTestResult_(test, success, report);
}
if (!this.stopped_ && this.startedCount_ < this.activeTests_.length) {
this.runNextTest_(frame);
} else if (this.resultCount_ == this.activeTests_.length) {
this.finish_();
}
};
/**
* Runs the next available test, if there are any left.
* @param {goog.testing.MultiTestRunner.TestFrame} frame Where to run the test.
* @private
*/
goog.testing.MultiTestRunner.prototype.runNextTest_ = function(frame) {
if (this.startedCount_ < this.activeTests_.length) {
var nextTest = this.activeTests_[this.startedCount_++];
this.log(this.trimFileName_(nextTest) + ' : Loading');
frame.runTest(nextTest);
}
};
/**
* Handles the test finishing, processing the results and rendering the report.
* @private
*/
goog.testing.MultiTestRunner.prototype.finish_ = function() {
if (this.stopped_) {
this.log('Stopped');
} else {
this.log('Finished');
}
this.startButtonEl_.disabled = false;
this.stopButtonEl_.disabled = true;
this.active_ = false;
this.showTab_(1);
this.drawStats_();
// Remove all the test frames
while (this.getChildCount() > 0) {
this.removeChildAt(0, true).dispose();
}
// Compute tests that did not finish before the stop button was hit.
var unfinished = [];
for (var i = 0; i < this.activeTests_.length; i++) {
var test = this.activeTests_[i];
if (!this.finished_[test]) {
unfinished.push(test);
}
}
if (unfinished.length) {
this.reportEl_.appendChild(goog.dom.createDom('pre', undefined,
'Theses tests did not finish:\n' + unfinished.join('\n')));
}
};
/**
* Resets the report, clearing out all children and drawing the initial summary.
* @private
*/
goog.testing.MultiTestRunner.prototype.resetReport_ = function() {
goog.dom.removeChildren(this.reportEl_);
var summary = this.dom_.createDom('div');
summary.className = goog.getCssName('goog-testrunner-progress-summary');
this.reportEl_.appendChild(summary);
this.writeCurrentSummary_();
};
/**
* Draws the stats for the test run.
* @private
*/
goog.testing.MultiTestRunner.prototype.drawStats_ = function() {
this.drawFilesHistogram_();
// Only show time stats if pool size is 1, otherwise times are wrong.
if (this.poolSize_ == 1) {
this.drawRunTimePie_();
this.drawTimeHistogram_();
}
this.drawWorstTestsTable_();
};
/**
* Draws the histogram showing number of files loaded.
* @private
*/
goog.testing.MultiTestRunner.prototype.drawFilesHistogram_ = function() {
this.drawStatsHistogram_(
'numFilesLoaded',
this.numFilesStatsBucketSize_,
goog.functions.identity,
500,
'Histogram showing distribution of\nnumber of files loaded per test');
};
/**
* Draws the histogram showing how long each test took to complete.
* @private
*/
goog.testing.MultiTestRunner.prototype.drawTimeHistogram_ = function() {
this.drawStatsHistogram_(
'totalTime',
this.runTimeStatsBucketSize_,
function(x) { return x / 1000; },
500,
'Histogram showing distribution of\ntime spent running tests in s');
};
/**
* Draws a stats histogram.
* @param {string} statsField Field of the stats object to graph.
* @param {number} bucketSize The size for the histogram's buckets.
* @param {function(number, ...*): *} valueTransformFn Function for
* transforming the x-labels value for display.
* @param {number} width The width in pixels of the graph.
* @param {string} title The graph's title.
* @private
*/
goog.testing.MultiTestRunner.prototype.drawStatsHistogram_ = function(
statsField, bucketSize, valueTransformFn, width, title) {
var hist = {}, data = [], xlabels = [], ylabels = [];
var max = 0;
for (var i = 0; i < this.stats_.length; i++) {
var num = this.stats_[i][statsField];
var bucket = Math.floor(num / bucketSize) * bucketSize;
if (bucket > max) {
max = bucket;
}
if (!hist[bucket]) {
hist[bucket] = 1;
} else {
hist[bucket]++;
}
}
var maxBucketSize = 0;
for (var i = 0; i <= max; i += bucketSize) {
xlabels.push(valueTransformFn(i));
var count = hist[i] || 0;
if (count > maxBucketSize) {
maxBucketSize = count;
}
data.push(count);
}
var diff = Math.max(1, Math.ceil(maxBucketSize / 10));
for (var i = 0; i <= maxBucketSize; i += diff) {
ylabels.push(i);
}
var chart = new goog.ui.ServerChart(
goog.ui.ServerChart.ChartType.VERTICAL_STACKED_BAR, width, 250, null,
goog.ui.ServerChart.CHART_SERVER_HTTPS_URI);
chart.setTitle(title);
chart.addDataSet(data, 'ff9900');
chart.setLeftLabels(ylabels);
chart.setGridY(ylabels.length - 1);
chart.setXLabels(xlabels);
chart.render(this.statsEl_);
};
/**
* Draws a pie chart showing the percentage of time spent running the tests
* compared to loading them etc.
* @private
*/
goog.testing.MultiTestRunner.prototype.drawRunTimePie_ = function() {
var totalTime = 0, runTime = 0;
for (var i = 0; i < this.stats_.length; i++) {
var stat = this.stats_[i];
totalTime += stat.totalTime;
runTime += stat.runTime;
}
var loadTime = totalTime - runTime;
var pie = new goog.ui.ServerChart(
goog.ui.ServerChart.ChartType.PIE, 500, 250, null,
goog.ui.ServerChart.CHART_SERVER_HTTPS_URI);
pie.setMinValue(0);
pie.setMaxValue(totalTime);
pie.addDataSet([runTime, loadTime], 'ff9900');
pie.setXLabels([
'Test execution (' + runTime + 'ms)',
'Loading (' + loadTime + 'ms)']);
pie.render(this.statsEl_);
};
/**
* Draws a pie chart showing the percentage of time spent running the tests
* compared to loading them etc.
* @private
*/
goog.testing.MultiTestRunner.prototype.drawWorstTestsTable_ = function() {
this.stats_.sort(function(a, b) {
return b['numFilesLoaded'] - a['numFilesLoaded'];
});
var tbody = goog.bind(this.dom_.createDom, this.dom_, 'tbody');
var thead = goog.bind(this.dom_.createDom, this.dom_, 'thead');
var tr = goog.bind(this.dom_.createDom, this.dom_, 'tr');
var th = goog.bind(this.dom_.createDom, this.dom_, 'th');
var td = goog.bind(this.dom_.createDom, this.dom_, 'td');
var a = goog.bind(this.dom_.createDom, this.dom_, 'a');
var head = thead({'style': 'cursor: pointer'},
tr(null,
th(null, ' '),
th(null, 'Test file'),
th('center', 'Num files loaded'),
th('center', 'Run time (ms)'),
th('center', 'Total time (ms)')));
var body = tbody();
var table = this.dom_.createDom('table', null, head, body);
for (var i = 0; i < this.stats_.length; i++) {
var stat = this.stats_[i];
body.appendChild(tr(null,
td('center', String(i + 1)),
td(null, a(
{'href': this.basePath_ + stat['testFile'], 'target': '_blank'},
stat['testFile'])),
td('center', String(stat['numFilesLoaded'])),
td('center', String(stat['runTime'])),
td('center', String(stat['totalTime']))));
}
this.statsEl_.appendChild(table);
this.tableSorter_.setDefaultSortFunction(goog.ui.TableSorter.numericSort);
this.tableSorter_.setSortFunction(
1 /* test file name */, goog.ui.TableSorter.alphaSort);
this.tableSorter_.decorate(table);
};
/**
* Clears the stats page.
* @private
*/
goog.testing.MultiTestRunner.prototype.clearStats_ = function() {
goog.dom.removeChildren(this.statsEl_);
this.tableSorter_.exitDocument();
};
/**
* Updates the report's summary.
* @private
*/
goog.testing.MultiTestRunner.prototype.writeCurrentSummary_ = function() {
var total = this.activeTests_.length;
var executed = this.resultCount_;
var passes = this.passes_;
var duration = Math.round((goog.now() - this.startTime_) / 1000);
var text = executed + ' of ' + total + ' tests executed.<br>' +
passes + ' passed, ' + (executed - passes) + ' failed.<br>' +
'Duration: ' + duration + 's.';
this.reportEl_.firstChild.innerHTML = text;
};
/**
* Adds a segment to the progress bar.
* @param {string} title Title for the segment.
* @param {*} success Whether the segment should indicate a success.
* @private
*/
goog.testing.MultiTestRunner.prototype.drawProgressSegment_ =
function(title, success) {
var part = this.progressRow_.cells[this.resultCount_ - 1];
part.title = title + ' : ' + (success ? 'SUCCESS' : 'FAILURE');
part.style.backgroundColor = success ? '#090' : '#900';
};
/**
* Draws a test result in the report pane.
* @param {string} test Test name.
* @param {*} success Whether the test succeeded.
* @param {string} report The report.
* @private
*/
goog.testing.MultiTestRunner.prototype.drawTestResult_ = function(
test, success, report) {
var text = goog.string.isEmptyOrWhitespace(report) ?
'No report for ' + test + '\n' : report;
var el = this.dom_.createDom('div');
text = goog.string.htmlEscape(text).replace(/\n/g, '<br>');
if (success) {
el.className = goog.getCssName('goog-testrunner-report-success');
} else {
text += '<a href="' + this.basePath_ + test +
'">Run individually &raquo;</a><br>&nbsp;';
el.className = goog.getCssName('goog-testrunner-report-failure');
}
el.innerHTML = text;
this.reportEl_.appendChild(el);
};
/**
* Returns the current timestamp.
* @return {string} HH:MM:SS.
* @private
*/
goog.testing.MultiTestRunner.prototype.getTimeStamp_ = function() {
var d = new Date;
return goog.string.padNumber(d.getHours(), 2) + ':' +
goog.string.padNumber(d.getMinutes(), 2) + ':' +
goog.string.padNumber(d.getSeconds(), 2);
};
/**
* Trims a filename to be less than 35-characters, ensuring that we do not break
* a path part.
* @param {string} name The file name.
* @return {string} The shortened name.
* @private
*/
goog.testing.MultiTestRunner.prototype.trimFileName_ = function(name) {
if (name.length < 35) {
return name;
}
var parts = name.split('/');
var result = '';
while (result.length < 35 && parts.length > 0) {
result = '/' + parts.pop() + result;
}
return '...' + result;
};
/**
* Shows the report and hides the log if the argument is true.
* @param {number} tab Which tab to show.
* @private
*/
goog.testing.MultiTestRunner.prototype.showTab_ = function(tab) {
var activeTabCssClass = goog.getCssName('goog-testrunner-activetab');
var logTabElement = goog.asserts.assert(this.logTabEl_);
var reportTabElement = goog.asserts.assert(this.reportTabEl_);
var statsTabElement = goog.asserts.assert(this.statsTabEl_);
if (tab == 0) {
this.logEl_.style.display = '';
goog.dom.classlist.add(logTabElement, activeTabCssClass);
} else {
this.logEl_.style.display = 'none';
goog.dom.classlist.remove(logTabElement, activeTabCssClass);
}
if (tab == 1) {
this.reportEl_.style.display = '';
goog.dom.classlist.add(reportTabElement, activeTabCssClass);
} else {
this.reportEl_.style.display = 'none';
goog.dom.classlist.remove(reportTabElement, activeTabCssClass);
}
if (tab == 2) {
this.statsEl_.style.display = '';
goog.dom.classlist.add(statsTabElement, activeTabCssClass);
} else {
this.statsEl_.style.display = 'none';
goog.dom.classlist.remove(statsTabElement, activeTabCssClass);
}
};
/**
* Handles the start button being clicked.
* @param {goog.events.BrowserEvent} e The click event.
* @private
*/
goog.testing.MultiTestRunner.prototype.onStartClicked_ = function(e) {
this.start();
};
/**
* Handles the stop button being clicked.
* @param {goog.events.BrowserEvent} e The click event.
* @private
*/
goog.testing.MultiTestRunner.prototype.onStopClicked_ = function(e) {
this.stopped_ = true;
this.finish_();
};
/**
* Handles the log tab being clicked.
* @param {goog.events.BrowserEvent} e The click event.
* @private
*/
goog.testing.MultiTestRunner.prototype.onLogTabClicked_ = function(e) {
this.showTab_(0);
};
/**
* Handles the log tab being clicked.
* @param {goog.events.BrowserEvent} e The click event.
* @private
*/
goog.testing.MultiTestRunner.prototype.onReportTabClicked_ = function(e) {
this.showTab_(1);
};
/**
* Handles the stats tab being clicked.
* @param {goog.events.BrowserEvent} e The click event.
* @private
*/
goog.testing.MultiTestRunner.prototype.onStatsTabClicked_ = function(e) {
this.showTab_(2);
};
/**
* Class used to manage the interaction with a single iframe.
* @param {string} basePath The base path for tests.
* @param {number} timeoutMs The time to wait for the test to load and run.
* @param {boolean} verbosePasses Whether to show results for passes.
* @param {goog.dom.DomHelper=} opt_domHelper Optional dom helper.
* @constructor
* @extends {goog.ui.Component}
* @final
*/
goog.testing.MultiTestRunner.TestFrame = function(
basePath, timeoutMs, verbosePasses, opt_domHelper) {
goog.ui.Component.call(this, opt_domHelper);
/**
* Base path where tests should be resolved from.
* @type {string}
* @private
*/
this.basePath_ = basePath;
/**
* The timeout for the test.
* @type {number}
* @private
*/
this.timeoutMs_ = timeoutMs;
/**
* Whether to show a summary for passing tests.
* @type {boolean}
* @private
*/
this.verbosePasses_ = verbosePasses;
/**
* An event handler for handling events.
* @type {goog.events.EventHandler<!goog.testing.MultiTestRunner.TestFrame>}
* @private
*/
this.eh_ = new goog.events.EventHandler(this);
};
goog.inherits(goog.testing.MultiTestRunner.TestFrame, goog.ui.Component);
/**
* Reference to the iframe.
* @type {HTMLIFrameElement}
* @private
*/
goog.testing.MultiTestRunner.TestFrame.prototype.iframeEl_ = null;
/**
* Whether the iframe for the current test has loaded.
* @type {boolean}
* @private
*/
goog.testing.MultiTestRunner.TestFrame.prototype.iframeLoaded_ = false;
/**
* The test file being run.
* @type {string}
* @private
*/
goog.testing.MultiTestRunner.TestFrame.prototype.testFile_ = '';
/**
* The report returned from the test.
* @type {string}
* @private
*/
goog.testing.MultiTestRunner.TestFrame.prototype.report_ = '';
/**
* The total time loading and running the test in milliseconds.
* @type {number}
* @private
*/
goog.testing.MultiTestRunner.TestFrame.prototype.totalTime_ = 0;
/**
* The actual runtime of the test in milliseconds.
* @type {number}
* @private
*/
goog.testing.MultiTestRunner.TestFrame.prototype.runTime_ = 0;
/**
* The number of files loaded by the test.
* @type {number}
* @private
*/
goog.testing.MultiTestRunner.TestFrame.prototype.numFilesLoaded_ = 0;
/**
* Whether the test was successful, null if no result has been returned yet.
* @type {?boolean}
* @private
*/
goog.testing.MultiTestRunner.TestFrame.prototype.isSuccess_ = null;
/**
* Timestamp for the when the test was started.
* @type {number}
* @private
*/
goog.testing.MultiTestRunner.TestFrame.prototype.startTime_ = 0;
/**
* Timestamp for the last state, used to determine timeouts.
* @type {number}
* @private
*/
goog.testing.MultiTestRunner.TestFrame.prototype.lastStateTime_ = 0;
/**
* The state of the active test.
* @type {number}
* @private
*/
goog.testing.MultiTestRunner.TestFrame.prototype.currentState_ = 0;
/** @override */
goog.testing.MultiTestRunner.TestFrame.prototype.disposeInternal = function() {
goog.testing.MultiTestRunner.TestFrame.superClass_.disposeInternal.call(this);
this.dom_.removeNode(this.iframeEl_);
this.eh_.dispose();
this.iframeEl_ = null;
};
/**
* Runs a test file in this test frame.
* @param {string} testFile The test to run.
*/
goog.testing.MultiTestRunner.TestFrame.prototype.runTest = function(testFile) {
this.lastStateTime_ = this.startTime_ = goog.now();
if (!this.iframeEl_) {
this.createIframe_();
}
this.iframeLoaded_ = false;
this.currentState_ = 0;
this.isSuccess_ = null;
this.report_ = '';
this.testFile_ = testFile;
try {
this.iframeEl_.src = this.basePath_ + testFile;
} catch (e) {
// Failures will trigger a JS exception on the local file system.
this.report_ = this.testFile_ + ' failed to load : ' + e.message;
this.isSuccess_ = false;
this.finish_();
return;
}
this.checkForCompletion_();
};
/**
* @return {string} The test file the TestFrame is running.
*/
goog.testing.MultiTestRunner.TestFrame.prototype.getTestFile = function() {
return this.testFile_;
};
/**
* @return {!Object} Stats about the test run.
*/
goog.testing.MultiTestRunner.TestFrame.prototype.getStats = function() {
return {
'testFile': this.testFile_,
'success': this.isSuccess_,
'runTime': this.runTime_,
'totalTime': this.totalTime_,
'numFilesLoaded': this.numFilesLoaded_
};
};
/**
* @return {string} The report for the test run.
*/
goog.testing.MultiTestRunner.TestFrame.prototype.getReport = function() {
return this.report_;
};
/**
* @return {?boolean} Whether the test frame had a success.
*/
goog.testing.MultiTestRunner.TestFrame.prototype.isSuccess = function() {
return this.isSuccess_;
};
/**
* Handles the TestFrame finishing a single test.
* @private
*/
goog.testing.MultiTestRunner.TestFrame.prototype.finish_ = function() {
this.totalTime_ = goog.now() - this.startTime_;
// TODO(user): Fire an event instead?
if (this.getParent() && this.getParent().processResult) {
this.getParent().processResult(this);
}
};
/**
* Creates an iframe to run the tests in. For overriding in unit tests.
* @private
*/
goog.testing.MultiTestRunner.TestFrame.prototype.createIframe_ = function() {
this.iframeEl_ =
/** @type {!HTMLIFrameElement} */ (this.dom_.createDom('iframe'));
this.getElement().appendChild(this.iframeEl_);
this.eh_.listen(this.iframeEl_, 'load', this.onIframeLoaded_);
};
/**
* Handles the iframe loading.
* @param {goog.events.BrowserEvent} e The load event.
* @private
*/
goog.testing.MultiTestRunner.TestFrame.prototype.onIframeLoaded_ = function(e) {
this.iframeLoaded_ = true;
};
/**
* Checks the active test for completion, keeping track of the tests' various
* execution stages.
* @private
*/
goog.testing.MultiTestRunner.TestFrame.prototype.checkForCompletion_ =
function() {
var js = goog.dom.getFrameContentWindow(this.iframeEl_);
switch (this.currentState_) {
case 0:
if (this.iframeLoaded_ && js['G_testRunner']) {
this.lastStateTime_ = goog.now();
this.currentState_++;
}
break;
case 1:
if (js['G_testRunner']['isInitialized']()) {
this.lastStateTime_ = goog.now();
this.currentState_++;
}
break;
case 2:
if (js['G_testRunner']['isFinished']()) {
var tr = js['G_testRunner'];
this.isSuccess_ = tr['isSuccess']();
this.report_ = tr['getReport'](this.verbosePasses_);
this.runTime_ = tr['getRunTime']();
this.numFilesLoaded_ = tr['getNumFilesLoaded']();
this.finish_();
return;
}
}
// Check to see if the test has timed out.
if (goog.now() - this.lastStateTime_ > this.timeoutMs_) {
this.report_ = this.testFile_ + ' timed out ' +
goog.testing.MultiTestRunner.STATES[this.currentState_];
this.isSuccess_ = false;
this.finish_();
return;
}
// Check again in 100ms.
goog.Timer.callOnce(this.checkForCompletion_, 100, this);
};