| // 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(); |
| }; |
| })(); |