blob: 176aef863d34599681207e728fb993c38d5e86fa [file] [log] [blame]
// Copyright 2011 Software Freedom Conservancy. 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 File to include for turning any HTML file page into a WebDriver
* JSUnit test suite by configuring an onload listener to the body that will
* instantiate and start the test runner.
*/
goog.provide('webdriver.testing.jsunit');
goog.provide('webdriver.testing.jsunit.TestRunner');
goog.require('goog.testing.TestRunner');
goog.require('webdriver.testing.Client');
goog.require('webdriver.testing.TestCase');
/**
* Constructs a test runner.
* @constructor
* @extends {goog.testing.TestRunner}
*/
webdriver.testing.jsunit.TestRunner = function() {
goog.base(this);
/**
* @type {!webdriver.testing.Client}
* @private
*/
this.client_ = new webdriver.testing.Client();
};
goog.inherits(webdriver.testing.jsunit.TestRunner, goog.testing.TestRunner);
/**
* Element created in the document to add test results to.
* @type {Element}
* @private
*/
webdriver.testing.jsunit.TestRunner.prototype.logEl_ = null;
/**
* DOM element used to stored screenshots. Screenshots are stored in the DOM to
* avoid exhausting JS stack-space.
* @type {Element}
* @private
*/
webdriver.testing.jsunit.TestRunner.prototype.screenshotCacheEl_ = null;
/** @override */
webdriver.testing.jsunit.TestRunner.prototype.initialize = function(testCase) {
goog.base(this, 'initialize', testCase);
this.screenshotCacheEl_ = document.createElement('div');
document.body.appendChild(this.screenshotCacheEl_);
this.screenshotCacheEl_.style.display = 'none';
};
/** @override */
webdriver.testing.jsunit.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.screenshotCacheEl_.innerHTML = '';
this.client_.sendInitEvent();
this.testCase.setCompletedCallback(goog.bind(this.onComplete_, this));
this.testCase.runTests();
};
/**
* Writes a nicely formatted log out to the document. Overrides
* {@link goog.testing.TestRunner#writeLog} to handle writing screenshots to the
* log.
* @param {string} log The string to write.
* @override
*/
webdriver.testing.jsunit.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);
var isScreenshot = / \[SCREENSHOT\] /.test(line);
if (/PASSED/.test(line)) {
color = 'darkgreen';
} else if (isFailOrError) {
color = 'darkred';
} else if (isScreenshot) {
color = 'darkblue';
} 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);
}
}
if (isScreenshot && this.screenshotCacheEl_.childNodes.length) {
var nextScreenshot = this.screenshotCacheEl_.childNodes[0];
this.screenshotCacheEl_.removeChild(nextScreenshot);
a = document.createElement('A');
a.style.fontSize = '0.8em';
a.href = 'javascript:void(0);';
a.onclick = goog.partial(toggleVisibility, a, nextScreenshot);
toggleVisibility(a, nextScreenshot);
div.appendChild(document.createTextNode(' '));
div.appendChild(a);
}
div.style.color = color;
div.style.font = 'normal 100% monospace';
try {
div.style.whiteSpace = 'pre-wrap';
} catch (e) {
// NOTE(user): 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);
if (nextScreenshot) {
a = document.createElement('A');
// Accessing the |src| property in IE sometimes results in an
// "Invalid pointer" error, which indicates it has been garbage
// collected. This does not occur when using getAttribute.
a.href = nextScreenshot.getAttribute('src');
a.target = '_blank';
a.appendChild(nextScreenshot);
this.logEl_.appendChild(a);
nextScreenshot = null;
}
}
function toggleVisibility(link, img) {
if (img.style.display === 'none') {
img.style.display = '';
link.innerHTML = '(hide screenshot)';
} else {
img.style.display = 'none';
link.innerHTML = '(view screenshot)';
}
}
};
/**
* Copied from goog.testing.TestRunner.prototype.onComplete_, which has private
* visibility.
* @private
*/
webdriver.testing.jsunit.TestRunner.prototype.onComplete_ = function() {
var log = this.testCase.getReport(true);
if (this.errors.length > 0) {
log += '\n' + this.errors.join('\n');
}
if (!this.logEl_) {
this.logEl_ = document.createElement('div');
document.body.appendChild(this.logEl_);
}
// Remove all children from the log element.
var logEl = this.logEl_;
while (logEl.firstChild) {
logEl.removeChild(logEl.firstChild);
}
this.writeLog(log);
this.client_.sendResultsEvent(this.isSuccess(), this.getReport(true));
};
/**
* Takes a screenshot. In addition to saving the screenshot for viewing in the
* HTML logs, the screenshot will also be saved using
* @param {!webdriver.WebDriver} driver The driver to take the screenshot with.
* @param {string=} opt_label An optional debug label to identify the screenshot
* with.
* @return {!webdriver.promise.Promise} A promise that will be resolved to the
* screenshot as a base-64 encoded PNG.
*/
webdriver.testing.jsunit.TestRunner.prototype.takeScreenshot = function(
driver, opt_label) {
if (!this.isInitialized()) {
throw Error(
'The test runner must be initialized before it may be used to' +
' take screenshots');
}
var client = this.client_;
var testCase = this.testCase;
var screenshotCache = this.screenshotCacheEl_;
return driver.takeScreenshot().then(function(png) {
client.sendScreenshotEvent(png, opt_label);
var img = document.createElement('img');
img.src = 'data:image/png;base64,' + png;
img.style.border = '1px solid black';
img.style.maxWidth = '500px';
screenshotCache.appendChild(img);
if (testCase) {
testCase.saveMessage('[SCREENSHOT] ' + (opt_label || '<Not Labeled>'));
}
return png;
});
};
(function() {
var tr = new webdriver.testing.jsunit.TestRunner();
// Export our test runner so it can be accessed by Selenium/WebDriver. This
// will only work if webdriver.WebDriver is using a pure-JavaScript
// webdriver.CommandExecutor. Otherwise, the JS-client could change the
// driver's focus to another window or frame and the Java/Python-client
// wouldn't be able to access this object.
goog.exportSymbol('G_testRunner', tr);
goog.exportSymbol('G_testRunner.initialize', tr.initialize);
goog.exportSymbol('G_testRunner.isInitialized', tr.isInitialized);
goog.exportSymbol('G_testRunner.isFinished', tr.isFinished);
goog.exportSymbol('G_testRunner.isSuccess', tr.isSuccess);
goog.exportSymbol('G_testRunner.getReport', tr.getReport);
goog.exportSymbol('G_testRunner.getRunTime', tr.getRunTime);
goog.exportSymbol('G_testRunner.getNumFilesLoaded', tr.getNumFilesLoaded);
goog.exportSymbol('G_testRunner.setStrict', tr.setStrict);
goog.exportSymbol('G_testRunner.logTestFailure', tr.logTestFailure);
// Export debug as a global function for JSUnit compatibility. This just
// calls log on the current test case.
if (!goog.global['debug']) {
goog.exportSymbol('debug', goog.bind(tr.log, tr));
}
// Add an error handler to report errors that may occur during
// initialization of the page.
var onerror = window.onerror;
window.onerror = function(error, url, line) {
// Call any existing onerror handlers.
if (onerror) {
onerror(error, url, line);
}
if (typeof error == 'object') {
// Webkit started passing an event object as the only argument to
// window.onerror. It doesn't contain an error message, url or line
// number. We therefore log as much info as we can.
if (error.target && error.target.tagName == 'SCRIPT') {
tr.logError('UNKNOWN ERROR: Script ' + error.target.src);
} else {
tr.logError('UNKNOWN ERROR: No error information available.');
}
} else {
tr.logError('JS ERROR: ' + error + '\nURL: ' + url + '\nLine: ' + line);
}
};
var onload = window.onload;
window.onload = function() {
// Call any existing onload handlers.
if (onload) {
onload();
}
var testCase = new webdriver.testing.TestCase(document.title);
testCase.autoDiscoverTests();
tr.initialize(testCase);
tr.execute();
};
})();