# lib/goog/testing/testrunner.js

    1// Copyright 2007 The Closure Library Authors. All Rights Reserved.2//3// Licensed under the Apache License, Version 2.0 (the "License");4// you may not use this file except in compliance with the License.5// You may obtain a copy of the License at6//7//      http://www.apache.org/licenses/LICENSE-2.08//9// Unless required by applicable law or agreed to in writing, software10// distributed under the License is distributed on an "AS-IS" BASIS,11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.12// See the License for the specific language governing permissions and13// limitations under the License.1415/**16 * @fileoverview The test runner is a singleton object that is used to execute17 * a goog.testing.TestCases, display the results, and expose the results to18 * Selenium for automation.  If a TestCase hasn't been registered with the19 * runner by the time window.onload occurs, the testRunner will try to auto-20 * discover JsUnit style test pages.21 *22 * The hooks for selenium are (see http://go/selenium-hook-setup):-23 *  - Boolean G_testRunner.isFinished()24 *  - Boolean G_testRunner.isSuccess()25 *  - String G_testRunner.getReport()26 *  - number G_testRunner.getRunTime()27 *  - Object.<string, Array.<string>> G_testRunner.getTestResults()28 *29 * Testing code should not have dependencies outside of goog.testing so as to30 * reduce the chance of masking missing dependencies.31 *32 */3334goog.provide('goog.testing.TestRunner');3536goog.require('goog.testing.TestCase');37383940/**41 * Construct a test runner.42 *43 * NOTE(user): This is currently pretty weird, I'm essentially trying to44 * create a wrapper that the Selenium test can hook into to query the state of45 * the running test case, while making goog.testing.TestCase general.46 *47 * @constructor48 */49goog.testing.TestRunner = function() {50  /**51   * Errors that occurred in the window.52   * @type {Array.<string>}53   */54  this.errors = [];55};565758/**59 * Reference to the active test case.60 * @type {goog.testing.TestCase?}61 */62goog.testing.TestRunner.prototype.testCase = null;636465/**66 * Whether the test runner has been initialized yet.67 * @type {boolean}68 */69goog.testing.TestRunner.prototype.initialized = false;707172/**73 * Element created in the document to add test results to.74 * @type {Element}75 * @private76 */77goog.testing.TestRunner.prototype.logEl_ = null;787980/**81 * Function to use when filtering errors.82 * @type {(function(string))?}83 * @private84 */85goog.testing.TestRunner.prototype.errorFilter_ = null;868788/**89 * Whether an empty test case counts as an error.90 * @type {boolean}91 * @private92 */93goog.testing.TestRunner.prototype.strict_ = true;949596/**97 * Initializes the test runner.98 * @param {goog.testing.TestCase} testCase The test case to initialize with.99 */100goog.testing.TestRunner.prototype.initialize = function(testCase) {101  if (this.testCase && this.testCase.running) {102    throw Error('The test runner is already waiting for a test to complete');103  }104  this.testCase = testCase;105  this.initialized = true;106};107108109/**110 * By default, the test runner is strict, and fails if it runs an empty111 * test case.112 * @param {boolean} strict Whether the test runner should fail on an empty113 *     test case.114 */115goog.testing.TestRunner.prototype.setStrict = function(strict) {116  this.strict_ = strict;117};118119120/**121 * @return {boolean} Whether the test runner should fail on an empty122 *     test case.123 */124goog.testing.TestRunner.prototype.isStrict = function() {125  return this.strict_;126};127128129/**130 * Returns true if the test runner is initialized.131 * Used by Selenium Hooks.132 * @return {boolean} Whether the test runner is active.133 */134goog.testing.TestRunner.prototype.isInitialized = function() {135  return this.initialized;136};137138139/**140 * Returns true if the test runner is finished.141 * Used by Selenium Hooks.142 * @return {boolean} Whether the test runner is active.143 */144goog.testing.TestRunner.prototype.isFinished = function() {145  return this.errors.length > 0 ||146      this.initialized && !!this.testCase && this.testCase.started &&147      !this.testCase.running;148};149150151/**152 * Returns true if the test case didn't fail.153 * Used by Selenium Hooks.154 * @return {boolean} Whether the current test returned successfully.155 */156goog.testing.TestRunner.prototype.isSuccess = function() {157  return !this.hasErrors() && !!this.testCase && this.testCase.isSuccess();158};159160161/**162 * Returns true if the test case runner has errors that were caught outside of163 * the test case.164 * @return {boolean} Whether there were JS errors.165 */166goog.testing.TestRunner.prototype.hasErrors = function() {167  return this.errors.length > 0;168};169170171/**172 * Logs an error that occurred.  Used in the case of environment setting up173 * an onerror handler.174 * @param {string} msg Error message.175 */176goog.testing.TestRunner.prototype.logError = function(msg) {177  if (!this.errorFilter_ || this.errorFilter_.call(null, msg)) {178    this.errors.push(msg);179  }180};181182183/**184 * Log failure in current running test.185 * @param {Error} ex Exception.186 */187goog.testing.TestRunner.prototype.logTestFailure = function(ex) {188  var testName = /** @type {string} */ (goog.testing.TestCase.currentTestName);189  if (this.testCase) {190    this.testCase.logError(testName, ex);191  } else {192    // NOTE: Do not forget to log the original exception raised.193    throw new Error('Test runner not initialized with a test case. Original ' +194                    'exception: ' + ex.message);195  }196};197198199/**200 * Sets a function to use as a filter for errors.201 * @param {function(string)} fn Filter function.202 */203goog.testing.TestRunner.prototype.setErrorFilter = function(fn) {204  this.errorFilter_ = fn;205};206207208/**209 * Returns a report of the test case that ran.210 * Used by Selenium Hooks.211 * @param {boolean=} opt_verbose If true results will include data about all212 *     tests, not just what failed.213 * @return {string} A report summary of the test.214 */215goog.testing.TestRunner.prototype.getReport = function(opt_verbose) {216  var report = [];217  if (this.testCase) {218    report.push(this.testCase.getReport(opt_verbose));219  }220  if (this.errors.length > 0) {221    report.push('JavaScript errors detected by test runner:');222    report.push.apply(report, this.errors);223    report.push('\n');224  }225  return report.join('\n');226};227228229/**230 * Returns the amount of time it took for the test to run.231 * Used by Selenium Hooks.232 * @return {number} The run time, in milliseconds.233 */234goog.testing.TestRunner.prototype.getRunTime = function() {235  return this.testCase ? this.testCase.getRunTime() : 0;236};237238239/**240 * Returns the number of script files that were loaded in order to run the test.241 * @return {number} The number of script files.242 */243goog.testing.TestRunner.prototype.getNumFilesLoaded = function() {244  return this.testCase ? this.testCase.getNumFilesLoaded() : 0;245};246247248/**249 * Executes a test case and prints the results to the window.250 */251goog.testing.TestRunner.prototype.execute = function() {252  if (!this.testCase) {253    throw Error('The test runner must be initialized with a test case ' +254                'before execute can be called.');255  }256257  if (this.strict_ && this.testCase.getCount() == 0) {258    throw Error(259        'No tests found in given test case: ' +260        this.testCase.getName() + ' ' +261        'By default, the test runner fails if a test case has no tests. ' +262        'To modify this behavior, see goog.testing.TestRunner\'s ' +263        'setStrict() method, or G_testRunner.setStrict()');264  }265266  this.testCase.setCompletedCallback(goog.bind(this.onComplete_, this));267  this.testCase.runTests();268};269270271/**272 * Writes the results to the document when the test case completes.273 * @private274 */275goog.testing.TestRunner.prototype.onComplete_ = function() {276  var log = this.testCase.getReport(true);277  if (this.errors.length > 0) {278    log += '\n' + this.errors.join('\n');279  }280281  if (!this.logEl_) {282    var el = document.getElementById('closureTestRunnerLog');283    if (el == null) {284      el = document.createElement('div');285      document.body.appendChild(el);286    }287    this.logEl_ = el;288  }289290  // Highlight the page to indicate the overall outcome.291  this.writeLog(log);292293  // TODO(user): Make this work with multiple test cases (b/8603638).294  var runAgainLink = document.createElement('a');295  runAgainLink.style.display = 'inline-block';296  runAgainLink.style.fontSize = 'small';297  runAgainLink.style.marginBottom = '16px';298  runAgainLink.href = '';299  runAgainLink.onclick = goog.bind(function() {300    this.execute();301    return false;302  }, this);303  runAgainLink.innerHTML = 'Run again without reloading';304  this.logEl_.appendChild(runAgainLink);305};306307308/**309 * Writes a nicely formatted log out to the document.310 * @param {string} log The string to write.311 */312goog.testing.TestRunner.prototype.writeLog = function(log) {313  var lines = log.split('\n');314  for (var i = 0; i < lines.length; i++) {315    var line = lines[i];316    var color;317    var isFailOrError = /FAILED/.test(line) || /ERROR/.test(line);318    if (/PASSED/.test(line)) {319      color = 'darkgreen';320    } else if (isFailOrError) {321      color = 'darkred';322    } else {323      color = '#333';324    }325    var div = document.createElement('div');326    if (line.substr(0, 2) == '> ') {327      // The stack trace may contain links so it has to be interpreted as HTML.328      div.innerHTML = line;329    } else {330      div.appendChild(document.createTextNode(line));331    }332333    var testNameMatch =334        /(\S+) (\[[^\]]*] )?: (FAILED|ERROR|PASSED)/.exec(line);335    if (testNameMatch) {336      // Build a URL to run the test individually.  If this test was already337      // part of another subset test, we need to overwrite the old runTests338      // query parameter.  We also need to do this without bringing in any339      // extra dependencies, otherwise we could mask missing dependency bugs.340      var newSearch = 'runTests=' + testNameMatch[1];341      var search = window.location.search;342      if (search) {343        var oldTests = /runTests=([^&]*)/.exec(search);344        if (oldTests) {345          newSearch = search.substr(0, oldTests.index) +346                      newSearch +347                      search.substr(oldTests.index + oldTests[0].length);348        } else {349          newSearch = search + '&' + newSearch;350        }351      } else {352        newSearch = '?' + newSearch;353      }354      var href = window.location.href;355      var hash = window.location.hash;356      if (hash && hash.charAt(0) != '#') {357        hash = '#' + hash;358      }359      href = href.split('#')[0].split('?')[0] + newSearch + hash;360361      // Add the link.362      var a = document.createElement('A');363      a.innerHTML = '(run individually)';364      a.style.fontSize = '0.8em';365      a.style.color = '#888';366      a.href = href;367      div.appendChild(document.createTextNode(' '));368      div.appendChild(a);369    }370371    div.style.color = color;372    div.style.font = 'normal 100% monospace';373    div.style.wordWrap = 'break-word';374    if (i == 0) {375      // Highlight the first line as a header that indicates the test outcome.376      div.style.padding = '20px';377      div.style.marginBottom = '10px';378      if (isFailOrError) {379        div.style.border = '5px solid ' + color;380        div.style.backgroundColor = '#ffeeee';381      } else {382        div.style.border = '1px solid black';383        div.style.backgroundColor = '#eeffee';384      }385    }386387    try {388      div.style.whiteSpace = 'pre-wrap';389    } catch (e) {390      // NOTE(brenneman): IE raises an exception when assigning to pre-wrap.391      // Thankfully, it doesn't collapse whitespace when using monospace fonts,392      // so it will display correctly if we ignore the exception.393    }394395    if (i < 2) {396      div.style.fontWeight = 'bold';397    }398    this.logEl_.appendChild(div);399  }400};401402403/**404 * Logs a message to the current test case.405 * @param {string} s The text to output to the log.406 */407goog.testing.TestRunner.prototype.log = function(s) {408  if (this.testCase) {409    this.testCase.log(s);410  }411};412413414// TODO(nnaze): Properly handle serving test results when multiple test cases415// are run.416/**417 * @return {Object.<string, !Array.<string>>} A map of test names to a list of418 * test failures (if any) to provide formatted data for the test runner.419 */420goog.testing.TestRunner.prototype.getTestResults = function() {421  if (this.testCase) {422    return this.testCase.getTestResults();423  }424  return null;425};