400 lines
11 KiB
JavaScript
400 lines
11 KiB
JavaScript
// Copyright 2007 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 The test runner is a singleton object that is used to execute
|
|
* a goog.testing.TestCases, display the results, and expose the results to
|
|
* Selenium for automation. If a TestCase hasn't been registered with the
|
|
* runner by the time window.onload occurs, the testRunner will try to auto-
|
|
* discover JsUnit style test pages.
|
|
*
|
|
* The hooks for selenium are :-
|
|
* - Boolean G_testRunner.isFinished()
|
|
* - Boolean G_testRunner.isSuccess()
|
|
* - String G_testRunner.getReport()
|
|
* - number G_testRunner.getRunTime()
|
|
*
|
|
* Testing code should not have dependencies outside of goog.testing so as to
|
|
* reduce the chance of masking missing dependencies.
|
|
*
|
|
*/
|
|
|
|
goog.provide('goog.testing.TestRunner');
|
|
|
|
goog.require('goog.testing.TestCase');
|
|
|
|
|
|
|
|
/**
|
|
* Construct a test runner.
|
|
*
|
|
* NOTE(user): This is currently pretty weird, I'm essentially trying to
|
|
* create a wrapper that the Selenium test can hook into to query the state of
|
|
* the running test case, while making goog.testing.TestCase general.
|
|
*
|
|
* @constructor
|
|
*/
|
|
goog.testing.TestRunner = function() {
|
|
/**
|
|
* Errors that occurred in the window.
|
|
* @type {Array.<string>}
|
|
*/
|
|
this.errors = [];
|
|
};
|
|
|
|
|
|
/**
|
|
* Reference to the active test case.
|
|
* @type {goog.testing.TestCase?}
|
|
*/
|
|
goog.testing.TestRunner.prototype.testCase = null;
|
|
|
|
|
|
/**
|
|
* Whether the test runner has been initialized yet.
|
|
* @type {boolean}
|
|
*/
|
|
goog.testing.TestRunner.prototype.initialized = false;
|
|
|
|
|
|
/**
|
|
* Element created in the document to add test results to.
|
|
* @type {Element}
|
|
* @private
|
|
*/
|
|
goog.testing.TestRunner.prototype.logEl_ = null;
|
|
|
|
|
|
/**
|
|
* Function to use when filtering errors.
|
|
* @type {(function(string))?}
|
|
* @private
|
|
*/
|
|
goog.testing.TestRunner.prototype.errorFilter_ = null;
|
|
|
|
|
|
/**
|
|
* Whether an empty test case counts as an error.
|
|
* @type {boolean}
|
|
* @private
|
|
*/
|
|
goog.testing.TestRunner.prototype.strict_ = true;
|
|
|
|
|
|
/**
|
|
* Initializes the test runner.
|
|
* @param {goog.testing.TestCase} testCase The test case to initialize with.
|
|
*/
|
|
goog.testing.TestRunner.prototype.initialize = function(testCase) {
|
|
if (this.testCase && this.testCase.running) {
|
|
throw Error('The test runner is already waiting for a test to complete');
|
|
}
|
|
this.testCase = testCase;
|
|
testCase.setTestRunner(this);
|
|
this.initialized = true;
|
|
};
|
|
|
|
|
|
/**
|
|
* By default, the test runner is strict, and fails if it runs an empty
|
|
* test case.
|
|
* @param {boolean} strict Whether the test runner should fail on an empty
|
|
* test case.
|
|
*/
|
|
goog.testing.TestRunner.prototype.setStrict = function(strict) {
|
|
this.strict_ = strict;
|
|
};
|
|
|
|
|
|
/**
|
|
* @return {boolean} Whether the test runner should fail on an empty
|
|
* test case.
|
|
*/
|
|
goog.testing.TestRunner.prototype.isStrict = function() {
|
|
return this.strict_;
|
|
};
|
|
|
|
|
|
/**
|
|
* Returns true if the test runner is initialized.
|
|
* Used by Selenium Hooks.
|
|
* @return {boolean} Whether the test runner is active.
|
|
*/
|
|
goog.testing.TestRunner.prototype.isInitialized = function() {
|
|
return this.initialized;
|
|
};
|
|
|
|
|
|
/**
|
|
* Returns true if the test runner is finished.
|
|
* Used by Selenium Hooks.
|
|
* @return {boolean} Whether the test runner is active.
|
|
*/
|
|
goog.testing.TestRunner.prototype.isFinished = function() {
|
|
return this.errors.length > 0 ||
|
|
this.initialized && !!this.testCase && this.testCase.started &&
|
|
!this.testCase.running;
|
|
};
|
|
|
|
|
|
/**
|
|
* Returns true if the test case didn't fail.
|
|
* Used by Selenium Hooks.
|
|
* @return {boolean} Whether the current test returned successfully.
|
|
*/
|
|
goog.testing.TestRunner.prototype.isSuccess = function() {
|
|
return !this.hasErrors() && !!this.testCase && this.testCase.isSuccess();
|
|
};
|
|
|
|
|
|
/**
|
|
* Returns true if the test case runner has errors that were caught outside of
|
|
* the test case.
|
|
* @return {boolean} Whether there were JS errors.
|
|
*/
|
|
goog.testing.TestRunner.prototype.hasErrors = function() {
|
|
return this.errors.length > 0;
|
|
};
|
|
|
|
|
|
/**
|
|
* Logs an error that occurred. Used in the case of environment setting up
|
|
* an onerror handler.
|
|
* @param {string} msg Error message.
|
|
*/
|
|
goog.testing.TestRunner.prototype.logError = function(msg) {
|
|
if (!this.errorFilter_ || this.errorFilter_.call(null, msg)) {
|
|
this.errors.push(msg);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Log failure in current running test.
|
|
* @param {Error} ex Exception.
|
|
*/
|
|
goog.testing.TestRunner.prototype.logTestFailure = function(ex) {
|
|
var testName = /** @type {string} */ (goog.testing.TestCase.currentTestName);
|
|
if (this.testCase) {
|
|
this.testCase.logError(testName, ex);
|
|
} else {
|
|
// NOTE: Do not forget to log the original exception raised.
|
|
throw new Error('Test runner not initialized with a test case. Original ' +
|
|
'exception: ' + ex.message);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Sets a function to use as a filter for errors.
|
|
* @param {function(string)} fn Filter function.
|
|
*/
|
|
goog.testing.TestRunner.prototype.setErrorFilter = function(fn) {
|
|
this.errorFilter_ = fn;
|
|
};
|
|
|
|
|
|
/**
|
|
* Returns a report of the test case that ran.
|
|
* Used by Selenium Hooks.
|
|
* @param {boolean=} opt_verbose If true results will include data about all
|
|
* tests, not just what failed.
|
|
* @return {string} A report summary of the test.
|
|
*/
|
|
goog.testing.TestRunner.prototype.getReport = function(opt_verbose) {
|
|
var report = [];
|
|
if (this.testCase) {
|
|
report.push(this.testCase.getReport(opt_verbose));
|
|
}
|
|
if (this.errors.length > 0) {
|
|
report.push('JavaScript errors detected by test runner:');
|
|
report.push.apply(report, this.errors);
|
|
report.push('\n');
|
|
}
|
|
return report.join('\n');
|
|
};
|
|
|
|
|
|
/**
|
|
* Returns the amount of time it took for the test to run.
|
|
* Used by Selenium Hooks.
|
|
* @return {number} The run time, in milliseconds.
|
|
*/
|
|
goog.testing.TestRunner.prototype.getRunTime = function() {
|
|
return this.testCase ? this.testCase.getRunTime() : 0;
|
|
};
|
|
|
|
|
|
/**
|
|
* Returns the number of script files that were loaded in order to run the test.
|
|
* @return {number} The number of script files.
|
|
*/
|
|
goog.testing.TestRunner.prototype.getNumFilesLoaded = function() {
|
|
return this.testCase ? this.testCase.getNumFilesLoaded() : 0;
|
|
};
|
|
|
|
|
|
/**
|
|
* Executes a test case and prints the results to the window.
|
|
*/
|
|
goog.testing.TestRunner.prototype.execute = function() {
|
|
if (!this.testCase) {
|
|
throw Error('The test runner must be initialized with a test case before ' +
|
|
'execute can be called.');
|
|
}
|
|
this.testCase.setCompletedCallback(goog.bind(this.onComplete_, this));
|
|
this.testCase.runTests();
|
|
};
|
|
|
|
|
|
/**
|
|
* Writes the results to the document when the test case completes.
|
|
* @private
|
|
*/
|
|
goog.testing.TestRunner.prototype.onComplete_ = function() {
|
|
var log = this.testCase.getReport(true);
|
|
if (this.errors.length > 0) {
|
|
log += '\n' + this.errors.join('\n');
|
|
}
|
|
|
|
if (!this.logEl_) {
|
|
var el = document.getElementById('closureTestRunnerLog');
|
|
if (el == null) {
|
|
el = document.createElement('div');
|
|
document.body.appendChild(el);
|
|
}
|
|
this.logEl_ = el;
|
|
}
|
|
|
|
// Highlight the page to indicate the overall outcome.
|
|
this.writeLog(log);
|
|
|
|
// TODO(user): Make this work with multiple test cases (b/8603638).
|
|
var runAgainLink = document.createElement('a');
|
|
runAgainLink.style.display = 'block';
|
|
runAgainLink.style.fontSize = 'small';
|
|
runAgainLink.href = '';
|
|
runAgainLink.onclick = goog.bind(function() {
|
|
this.execute();
|
|
return false;
|
|
}, this);
|
|
runAgainLink.innerHTML = 'Run again without reloading';
|
|
this.logEl_.appendChild(runAgainLink);
|
|
};
|
|
|
|
|
|
/**
|
|
* Writes a nicely formatted log out to the document.
|
|
* @param {string} log The string to write.
|
|
*/
|
|
goog.testing.TestRunner.prototype.writeLog = function(log) {
|
|
var lines = log.split('\n');
|
|
for (var i = 0; i < lines.length; i++) {
|
|
var line = lines[i];
|
|
var color;
|
|
var isFailOrError = /FAILED/.test(line) || /ERROR/.test(line);
|
|
if (/PASSED/.test(line)) {
|
|
color = 'darkgreen';
|
|
} else if (isFailOrError) {
|
|
color = 'darkred';
|
|
} else {
|
|
color = '#333';
|
|
}
|
|
var div = document.createElement('div');
|
|
if (line.substr(0, 2) == '> ') {
|
|
// The stack trace may contain links so it has to be interpreted as HTML.
|
|
div.innerHTML = line;
|
|
} else {
|
|
div.appendChild(document.createTextNode(line));
|
|
}
|
|
|
|
if (isFailOrError) {
|
|
var testNameMatch = /(\S+) (\[[^\]]*] )?: (FAILED|ERROR)/.exec(line);
|
|
if (testNameMatch) {
|
|
// Build a URL to run the test individually. If this test was already
|
|
// part of another subset test, we need to overwrite the old runTests
|
|
// query parameter. We also need to do this without bringing in any
|
|
// extra dependencies, otherwise we could mask missing dependency bugs.
|
|
var newSearch = 'runTests=' + testNameMatch[1];
|
|
var search = window.location.search;
|
|
if (search) {
|
|
var oldTests = /runTests=([^&]*)/.exec(search);
|
|
if (oldTests) {
|
|
newSearch = search.substr(0, oldTests.index) +
|
|
newSearch +
|
|
search.substr(oldTests.index + oldTests[0].length);
|
|
} else {
|
|
newSearch = search + '&' + newSearch;
|
|
}
|
|
} else {
|
|
newSearch = '?' + newSearch;
|
|
}
|
|
var href = window.location.href;
|
|
var hash = window.location.hash;
|
|
if (hash && hash.charAt(0) != '#') {
|
|
hash = '#' + hash;
|
|
}
|
|
href = href.split('#')[0].split('?')[0] + newSearch + hash;
|
|
|
|
// Add the link.
|
|
var a = document.createElement('A');
|
|
a.innerHTML = '(run individually)';
|
|
a.style.fontSize = '0.8em';
|
|
a.href = href;
|
|
div.appendChild(document.createTextNode(' '));
|
|
div.appendChild(a);
|
|
}
|
|
}
|
|
|
|
div.style.color = color;
|
|
div.style.font = 'normal 100% monospace';
|
|
if (i == 0) {
|
|
// Highlight the first line as a header that indicates the test outcome.
|
|
div.style.padding = '20px';
|
|
div.style.marginBottom = '10px';
|
|
if (isFailOrError) {
|
|
div.style.border = '5px solid ' + color;
|
|
div.style.backgroundColor = '#ffeeee';
|
|
} else {
|
|
div.style.border = '1px solid black';
|
|
div.style.backgroundColor = '#eeffee';
|
|
}
|
|
}
|
|
|
|
try {
|
|
div.style.whiteSpace = 'pre-wrap';
|
|
} catch (e) {
|
|
// NOTE(brenneman): IE raises an exception when assigning to pre-wrap.
|
|
// Thankfully, it doesn't collapse whitespace when using monospace fonts,
|
|
// so it will display correctly if we ignore the exception.
|
|
}
|
|
|
|
if (i < 2) {
|
|
div.style.fontWeight = 'bold';
|
|
}
|
|
this.logEl_.appendChild(div);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Logs a message to the current test case.
|
|
* @param {string} s The text to output to the log.
|
|
*/
|
|
goog.testing.TestRunner.prototype.log = function(s) {
|
|
if (this.testCase) {
|
|
this.testCase.log(s);
|
|
}
|
|
};
|