text: // 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 (see http://go/selenium-hook-setup):-
   *  - Boolean G_testRunner.isFinished()
   *  - Boolean G_testRunner.isSuccess()
   *  - String G_testRunner.getReport()
   *  - number G_testRunner.getRunTime()
   *  - Object<string, Array<string>> G_testRunner.getTestResults()
   *
   * Testing code should not have dependencies outside of goog.testing so as to
   * reduce the chance of masking missing dependencies.
   *
   */
  
  goog.setTestOnly('goog.testing.TestRunner');
  goog.provide('goog.testing.TestRunner');
  
  goog.require('goog.dom');
  goog.require('goog.dom.TagName');
  goog.require('goog.dom.safe');
  goog.require('goog.json');
  goog.require('goog.testing.TestCase');
  goog.require('goog.userAgent');
  
  
  
  /**
   * 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}
     */
    this.testCase = null;
  
    /**
     * Whether the test runner has been initialized yet.
     * @type {boolean}
     */
    this.initialized = false;
  
    /**
     * Element created in the document to add test results to.
     * @private {?Element}
     */
    this.logEl_ = null;
  
    /**
     * Function to use when filtering errors.
     * @private {(function(string))?}
     */
    this.errorFilter_ = null;
  
    /**
     * Whether an empty test case counts as an error.
     * @private {boolean}
     */
    this.strict_ = true;
  
    /**
     * Store the serializer to avoid it being overwritten by a mock.
     * @private {function(!Object): string}
     */
    this.jsonStringify_ = goog.json.serialize;
  
    /**
     * An id unique to this runner. Checked by the server during polling to
     * verify that the page was not reloaded.
     * @private {string}
     */
    this.uniqueId_ = ((Math.random() * 1e9) >>> 0) + '-' +
        window.location.pathname.replace(/.*\//, '').replace(/\.html.*$/, '');
  
    if (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher(11)) {
      return;
    }
  
    var self = this;
    function onPageHide() {
      self.clearUniqueId();
    }
    window.addEventListener('pagehide', onPageHide);
  
  };
  
  /**
   * The uuid is embedded in the URL search. This function allows us to mock
   * the search in the test.
   * @return {string}
   */
  goog.testing.TestRunner.prototype.getSearchString = function() {
    return window.location.search;
  };
  
  /**
   * Returns the unique id for this test page.
   * @return {string}
   */
  goog.testing.TestRunner.prototype.getUniqueId = function() {
    return this.uniqueId_;
  };
  
  /**
   * Clears the unique id for this page. The value will hint the reason.
   */
  goog.testing.TestRunner.prototype.clearUniqueId = function() {
    this.uniqueId_ = 'pagehide';
  };
  
  /**
   * 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 new Error(
          'The test runner is already waiting for a test to complete');
    }
    this.testCase = testCase;
    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 false if the test runner has not finished successfully.
   * Used by Selenium Hooks.
   * @return {boolean} Whether the test runner is not active.
   */
  goog.testing.TestRunner.prototype.isFinished = function() {
    return this.errors.length > 0 || this.isComplete();
  };
  
  
  /**
   * Returns true if the test runner is finished.
   * @return {boolean} True if the test runner started and subsequently completed.
   */
  goog.testing.TestRunner.prototype.isComplete = function() {
    return 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.isComplete()) {
      // Once the user has checked their code, subsequent errors can occur
      // because of tearDown actions. For now, log these but do not fail the test.
      this.log('Error after test completed: ' + msg);
      return;
    }
    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 new Error(
          'The test runner must be initialized with a test case ' +
          'before execute can be called.');
    }
  
    if (this.strict_ && this.testCase.getCount() == 0) {
      throw new Error(
          'No tests found in given test case: ' + this.testCase.getName() + '. ' +
          'By default, the test runner fails if a test case has no tests. ' +
          'To modify this behavior, see goog.testing.TestRunner\'s ' +
          'setStrict() method, or G_testRunner.setStrict()');
    }
  
    this.testCase.addCompletedCallback(goog.bind(this.onComplete_, this));
    if (goog.testing.TestRunner.shouldUsePromises_(this.testCase)) {
      this.testCase.runTestsReturningPromise();
    } else {
      this.testCase.runTests();
    }
  };
  
  
  /**
   * @param {!goog.testing.TestCase} testCase
   * @return {boolean}
   * @private
   */
  goog.testing.TestRunner.shouldUsePromises_ = function(testCase) {
    return testCase.constructor === goog.testing.TestCase;
  };
  
  
  /** @const {string} The ID of the element to log output to. */
  goog.testing.TestRunner.TEST_LOG_ID = 'closureTestRunnerLog';
  
  
  /**
   * 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(goog.testing.TestRunner.TEST_LOG_ID);
      if (el == null) {
        el = goog.dom.createElement(goog.dom.TagName.DIV);
        el.id = goog.testing.TestRunner.TEST_LOG_ID;
        document.body.appendChild(el);
      }
      this.logEl_ = el;
    }
  
    // Highlight the page to indicate the overall outcome.
    this.writeLog(log);
  
    // TODO(chrishenry): Make this work with multiple test cases (b/8603638).
    var runAgainLink = goog.dom.createElement(goog.dom.TagName.A);
    runAgainLink.style.display = 'inline-block';
    runAgainLink.style.fontSize = 'small';
    runAgainLink.style.marginBottom = '16px';
    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 isPassed = /PASSED/.test(line);
      var isFailOrError =
          /FAILED/.test(line) || /ERROR/.test(line) || /NO TESTS RUN/.test(line);
      if (isPassed) {
        color = 'darkgreen';
      } else if (isFailOrError) {
        color = 'darkred';
      } else {
        color = '#333';
      }
      var div = goog.dom.createElement(goog.dom.TagName.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));
      }
  
      var testNameMatch = /(\S+) (\[[^\]]*] )?: (FAILED|ERROR|PASSED)/.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 = goog.dom.createElement(goog.dom.TagName.A);
        a.innerHTML = '(run individually)';
        a.style.fontSize = '0.8em';
        a.style.color = '#888';
        goog.dom.safe.setAnchorHref(a, href);
        div.appendChild(document.createTextNode(' '));
        div.appendChild(a);
      }
  
      div.style.color = color;
      div.style.font = 'normal 100% monospace';
      div.style.wordWrap = 'break-word';
      if (i == 0) {
        // Highlight the first line as a header that indicates the test outcome.
        div.style.padding = '20px';
        div.style.marginBottom = '10px';
        if (isPassed) {
          div.style.border = '1px solid ' + color;
          div.style.backgroundColor = '#eeffee';
        } else if (isFailOrError) {
          div.style.border = '5px solid ' + color;
          div.style.backgroundColor = '#ffeeee';
        } else {
          div.style.border = '1px solid black';
          div.style.backgroundColor = '#eeeeee';
        }
      }
  
      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);
    }
  };
  
  
  // TODO(nnaze): Properly handle serving test results when multiple test cases
  // are run.
  /**
   * @return {Object<string, !Array<!goog.testing.TestCase.IResult>>} A map of
   * test names to a list of test failures (if any) to provide formatted data
   * for the test runner.
   */
  goog.testing.TestRunner.prototype.getTestResults = function() {
    if (this.testCase) {
      return this.testCase.getTestResults();
    }
    return null;
  };
  
  
  /**
   * Returns the test results as json.
   * This is called by the testing infrastructure through G_testrunner.
   * @return {?string} Tests results object.
   */
  goog.testing.TestRunner.prototype.getTestResultsAsJson = function() {
    if (this.testCase) {
      var testCaseResults
          /** {Object<string, !Array<!goog.testing.TestCase.IResult>>} */
          = this.testCase.getTestResults();
      if (this.hasErrors()) {
        var globalErrors = [];
        for (var i = 0; i < this.errors.length; i++) {
          globalErrors.push(
              {source: '', message: this.errors[i], stacktrace: ''});
        }
        // We are writing on our testCase results, but the test is over.
        testCaseResults['globalErrors'] = globalErrors;
      }
      return this.jsonStringify_(testCaseResults);
    }
    return null;
  };
  