/**
 * @license
 * Copyright The Closure Library Authors.
 * SPDX-License-Identifier: Apache-2.0
 */

/**
 * @fileoverview A class representing a set of test functions to be run.
 *
 * Testing code should not have dependencies outside of goog.testing so as to
 * reduce the chance of masking missing dependencies.
 *
 * This file does not compile correctly with --collapse_properties. Use
 * --property_renaming=ALL_UNQUOTED instead.
 */

goog.setTestOnly('goog.testing.TestCase');
goog.provide('goog.testing.TestCase');
goog.provide('goog.testing.TestCase.Error');
goog.provide('goog.testing.TestCase.Order');
goog.provide('goog.testing.TestCase.Result');
goog.provide('goog.testing.TestCase.Test');


goog.require('goog.Promise');
goog.require('goog.Thenable');
goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.debug');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.object');
goog.require('goog.testing.CspViolationObserver');
goog.require('goog.testing.JsUnitException');
goog.require('goog.url');



/**
 * A class representing a JsUnit test case. A TestCase is made up of a number
 * of test functions which can be run. Individual test cases can override the
 * following functions to set up their test environment:
 *   - runTests - completely override the test's runner
 *   - setUpPage - called before any of the test functions are run
 *   - tearDownPage - called after all tests are finished
 *   - setUp - called before each of the test functions
 *   - tearDown - called after each of the test functions
 *   - shouldRunTests - called before a test run, all tests are skipped if it
 *                      returns false. Can be used to disable tests on browsers
 *                      where they aren't expected to pass.
 * <p>
 * TestCase objects are usually constructed by inspecting the global environment
 * to discover functions that begin with the prefix <code>test</code>.
 * (See {@link #autoDiscoverLifecycle} and {@link #autoDiscoverTests}.)
 * </p>
 *
 * <h2>Testing asychronous code with promises</h2>
 *
 * <p>
 * In the simplest cases, the behavior that the developer wants to test
 * is synchronous, and the test functions exercising the behavior execute
 * synchronously. But TestCase can also be used to exercise asynchronous code
 * through the use of <a
 * href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise">
 * promises</a>. If a test function returns an object that has a
 * <code>then</code> method defined on it, the test framework switches to an
 * asynchronous execution strategy: the next test function will not begin
 * execution until the returned promise is resolved or rejected. Instead of
 * writing test assertions at the top level inside a test function, the test
 * author chains them on the end of the returned promise. For example:
 * </p>
 * <pre>
 *   function testPromiseBasedAPI() {
 *     return promiseBasedAPI().then(function(value) {
 *       // Will run when the promise resolves, and before the next
 *       // test function begins execution.
 *       assertEquals('foo', value.bar);
 *     });
 *   }
 * </pre>
 * <p>
 * Synchronous and asynchronous tests can be mixed in the same TestCase.
 * Test functions that return an object with a <code>then</code> method are
 * executed asynchronously, and all other test functions are executed
 * synchronously. While this is convenient for test authors (since it doesn't
 * require any explicit configuration for asynchronous tests), it can lead to
 * confusion if the test author forgets to return the promise from the test
 * function. For example:
 * </p>
 * <pre>
 *   function testPromiseBasedAPI() {
 *     // This test should never succeed.
 *     promiseBasedAPI().then(fail, fail);
 *     // Oops! The promise isn't returned to the framework,
 *     // so this test actually does succeed.
 *   }
 * </pre>
 * <p>
 * Since the test framework knows nothing about the promise created
 * in the test function, it will run the function synchronously, record
 * a success, and proceed immediately to the next test function.
 * </p>
 * <p>
 * Promises returned from test functions can time out. If a returned promise
 * is not resolved or rejected within {@link promiseTimeout} milliseconds,
 * the test framework rejects the promise without a timeout error message.
 * Test cases can configure the value of `promiseTimeout` by setting
 * <pre>
 *   goog.testing.TestCase.getActiveTestCase().promiseTimeout = ...
 * </pre>
 * in their `setUpPage` methods.
 * </p>
 *
 * @param {string=} opt_name The name of the test case, defaults to
 *     'Untitled Test Case'.
 * @constructor
 */
goog.testing.TestCase = function(opt_name) {
  'use strict';
  /**
   * A name for the test case.
   * @type {string}
   * @private
   */
  this.name_ = opt_name || 'Untitled Test Case';

  /**
   * If the test should be auto discovered via {@link #autoDiscoverTests} when
   * test case is initialized.
   * @type {boolean}
   * @private
   */
  this.shouldAutoDiscoverTests_ = true;

  /**
   * Array of test functions that can be executed.
   * @type {!Array<!goog.testing.TestCase.Test>}
   * @private
   */
  this.tests_ = [];

  /**
   * Set of test names and/or indices to execute, or null if all tests should
   * be executed.
   *
   * Indices are included to allow automation tools to run a subset of the
   * tests without knowing the exact contents of the test file.
   *
   * Indices should only be used with SORTED ordering.
   *
   * Example valid values:
   * <ul>
   * <li>[testName]
   * <li>[testName1, testName2]
   * <li>[2] - will run the 3rd test in the order specified
   * <li>[1,3,5]
   * <li>[testName1, testName2, 3, 5] - will work
   * <ul>
   * @type {?Object}
   * @private
   */
  this.testsToRun_ = null;

  /**
   * A call back for each test.
   * @private {?function(?goog.testing.TestCase.Test, !Array<string>)}
   */
  this.testDone_ = null;

  /**
   * The order to run the auto-discovered tests in.
   * @type {string}
   */
  this.order = goog.testing.TestCase.Order.SORTED;

  /** @private {function(!goog.testing.TestCase.Result)} */
  this.runNextTestCallback_ = () => {};

  /**
   * The currently executing test case or null.
   * @private {?goog.testing.TestCase.Test}
   */
  this.curTest_ = null;

  /**
   * Object used to encapsulate the test results.
   * @type {!goog.testing.TestCase.Result}
   * @protected
   * @suppress {underscore|visibility}
   */
  this.result_ = new goog.testing.TestCase.Result(this);

  /**
   * An array of exceptions generated by `assert` statements.
   * @private {!Array<!goog.testing.JsUnitException>}
   */
  this.thrownAssertionExceptions_ = [];

  /**
   * The maximum time in milliseconds a promise returned from a test function
   * may remain pending before the test fails due to timeout.
   * @type {number}
   */
  this.promiseTimeout = 1000;  // 1s

  /**
   * Callbacks that will be executed when the test has finalized.
   * @private {!Array<function()>}
   */
  this.onCompletedCallbacks_ = [];

  /** @type {number|undefined} */
  this.endTime_;

  /** @private {number} */
  this.testsRanSoFar_ = 0;

  /** @private {!goog.testing.CspViolationObserver} */
  this.cspViolationObserver_ = new goog.testing.CspViolationObserver();

  /** @private {boolean} */
  this.ignoreStartupCspViolations_ = false;
};


/**
 * The order to run the auto-discovered tests.
 * @enum {string}
 */
goog.testing.TestCase.Order = {
  /**
   * This is browser dependent and known to be different in FF and Safari
   * compared to others.
   */
  NATURAL: 'natural',

  /** Random order. */
  RANDOM: 'random',

  /** Sorted based on the name. */
  SORTED: 'sorted'
};


/**
 * @return {string} The name of the test.
 */
goog.testing.TestCase.prototype.getName = function() {
  'use strict';
  return this.name_;
};

/**
 * Returns the current test or null.
 * @return {?goog.testing.TestCase.Test}
 * @protected
 */
goog.testing.TestCase.prototype.getCurrentTest = function() {
  'use strict';
  return this.curTest_;
};

/**
 * The maximum amount of time in milliseconds that the test case can take
 * before it is forced to yield and reschedule. This prevents the test runner
 * from blocking the browser and potentially hurting the test harness.
 * @type {number}
 */
goog.testing.TestCase.maxRunTime = 200;


/**
 * Save a reference to `window.setTimeout`, so any code that overrides the
 * default behavior (the MockClock, for example) doesn't affect our runner.
 * @type {function((Function|string), number=, *=): number}
 * @private
 */
goog.testing.TestCase.protectedSetTimeout_ = goog.global.setTimeout;


/**
 * Save a reference to `window.clearTimeout`, so any code that overrides
 * the default behavior (e.g. MockClock) doesn't affect our runner.
 * @type {function((null|number|undefined)): void}
 * @private
 */
goog.testing.TestCase.protectedClearTimeout_ = goog.global.clearTimeout;


/**
 * Save a reference to `window.Date`, so any code that overrides
 * the default behavior doesn't affect our runner.
 * @type {function(new: Date)}
 * @private
 */
goog.testing.TestCase.protectedDate_ = Date;

/**
 * Save a reference to `window.performance`, so any code that overrides
 * the default behavior doesn't affect our runner.
 * @type {?Performance}
 * @private
 */
goog.testing.TestCase.protectedPerformance_ = typeof window !== 'undefined' &&
        window.performance && window.performance.now ?
    performance :
    null;


/**
 * Name of the current test that is running, or null if none is running.
 * @type {?string}
 */
goog.testing.TestCase.currentTestName = null;


/**
 * Avoid a dependency on goog.userAgent and keep our own reference of whether
 * the browser is IE.
 * @type {boolean}
 */
goog.testing.TestCase.IS_IE = typeof opera == 'undefined' &&
    !!goog.global.navigator &&
    goog.global.navigator.userAgent.indexOf('MSIE') != -1;


/**
 * Exception object that was detected before a test runs.
 * @type {*}
 * @protected
 */
goog.testing.TestCase.prototype.exceptionBeforeTest;


/**
 * Whether the test case has ever tried to execute.
 * @type {boolean}
 */
goog.testing.TestCase.prototype.started = false;


/**
 * Whether the test case is running.
 * @type {boolean}
 */
goog.testing.TestCase.prototype.running = false;


/**
 * Timestamp for when the test was started.
 * @type {number}
 * @private
 */
goog.testing.TestCase.prototype.startTime_ = 0;


/**
 * Time since the last batch of tests was started, if batchTime exceeds
 * {@link #maxRunTime} a timeout will be used to stop the tests blocking the
 * browser and a new batch will be started.
 * @type {number}
 * @private
 */
goog.testing.TestCase.prototype.batchTime_ = 0;


/**
 * Pointer to the current test.
 * @type {number}
 * @private
 */
goog.testing.TestCase.prototype.currentTestPointer_ = 0;


/**
 * Whether to use Native Promises or goog.Promise.
 * @type {boolean}
 * @private
 */
goog.testing.TestCase.prototype.useNativePromise_ = false;


/**
 * Adds a new test to the test case.
 * @param {!goog.testing.TestCase.Test} test The test to add.
 */
goog.testing.TestCase.prototype.add = function(test) {
  'use strict';
  goog.asserts.assert(test);
  if (this.started) {
    throw new Error(
        'Tests cannot be added after execute() has been called. ' +
        'Test: ' + test.name);
  }

  this.tests_.push(test);
};


/**
 * Creates and adds a new test.
 *
 * Convenience function to make syntax less awkward when not using automatic
 * test discovery.
 *
 * @param {string} name The test name.
 * @param {function()} ref Reference to the test function.
 * @param {!Object=} scope Optional scope that the test function should be
 *     called in.
 * @param {!Array<!Object>=} objChain An array of Objects that may have
 *     additional set up/tear down logic for a particular test.
 */
goog.testing.TestCase.prototype.addNewTest = function(
    name, ref, scope, objChain) {
  'use strict';
  this.add(this.createTest(name, ref, scope || this, objChain));
};


/**
 * Sets the tests.
 * @param {!Array<goog.testing.TestCase.Test>} tests A new test array.
 * @protected
 */
goog.testing.TestCase.prototype.setTests = function(tests) {
  'use strict';
  this.tests_ = tests;
};


/**
 * Gets the tests.
 * @return {!Array<goog.testing.TestCase.Test>} The test array.
 */
goog.testing.TestCase.prototype.getTests = function() {
  'use strict';
  return this.tests_;
};


/**
 * Returns the number of tests contained in the test case.
 * @return {number} The number of tests.
 */
goog.testing.TestCase.prototype.getCount = function() {
  'use strict';
  return this.tests_.length;
};


/**
 * Returns the number of tests actually run in the test case, i.e. subtracting
 * any which are skipped.
 * @return {number} The number of un-ignored tests.
 */
goog.testing.TestCase.prototype.getActuallyRunCount = function() {
  'use strict';
  return this.testsToRun_ ? goog.object.getCount(this.testsToRun_) : 0;
};


/**
 * Returns the current test and increments the pointer.
 * @return {goog.testing.TestCase.Test} The current test case.
 */
goog.testing.TestCase.prototype.next = function() {
  'use strict';
  var test;
  while ((test = this.tests_[this.currentTestPointer_++])) {
    if (!this.testsToRun_ || this.testsToRun_[test.name] ||
        this.testsToRun_[this.currentTestPointer_ - 1]) {
      return test;
    }
  }
  return null;
};


/**
 * Resets the test case pointer, so that next returns the first test.
 */
goog.testing.TestCase.prototype.reset = function() {
  'use strict';
  this.currentTestPointer_ = 0;
  this.result_ = new goog.testing.TestCase.Result(this);
};


/**
 * Adds a callback function that should be executed when the tests have
 * completed.
 * @param {function()} fn The callback function.
 */
goog.testing.TestCase.prototype.addCompletedCallback = function(fn) {
  'use strict';
  this.onCompletedCallbacks_.push(fn);
};


/**
 * @param {goog.testing.TestCase.Order} order The sort order for running tests.
 */
goog.testing.TestCase.prototype.setOrder = function(order) {
  'use strict';
  this.order = order;
};


/**
 * @param {Object<string, boolean>} testsToRun Set of tests to run. Entries in
 *     the set may be test names, like "testFoo", or numeric indices. Only
 *     tests identified by name or by index will be executed.
 */
goog.testing.TestCase.prototype.setTestsToRun = function(testsToRun) {
  'use strict';
  this.testsToRun_ = testsToRun;
};


/**
 * Can be overridden in test classes to indicate whether the tests in a case
 * should be run in that particular situation.  For example, this could be used
 * to stop tests running in a particular browser, where browser support for
 * the class under test was absent.
 * @return {boolean} Whether any of the tests in the case should be run.
 */
goog.testing.TestCase.prototype.shouldRunTests = function() {
  'use strict';
  return true;
};


/**
 * Executes the tests, yielding asynchronously if execution time exceeds
 * {@link maxRunTime}. There is no guarantee that the test case has finished
 * once this method has returned. To be notified when the test case
 * has finished, use {@link #addCompletedCallback} or
 * {@link #runTestsReturningPromise}.
 */
goog.testing.TestCase.prototype.execute = function() {
  'use strict';
  if (!this.prepareForRun_()) {
    return;
  }
  this.groupLogsStart();
  this.log('Starting tests: ' + this.name_);
  this.cycleTests();
};


/**
 * Sets up the internal state of the test case for a run.
 * @return {boolean} If false, preparation failed because the test case
 *     is not supposed to run in the present environment.
 * @private
 */
goog.testing.TestCase.prototype.prepareForRun_ = function() {
  'use strict';
  this.started = true;
  this.reset();
  this.startTime_ = this.now();
  this.running = true;
  this.result_.totalCount = this.getCount();
  this.cspViolationObserver_.start();
  if (!this.shouldRunTests()) {
    this.log('shouldRunTests() returned false, skipping these tests.');
    this.result_.testSuppressed = true;
    this.finalize();
    return false;
  }
  this.checkCspViolations_('shouldRunTests');
  return true;
};


/**
 * Finalizes the test case, called when the tests have finished executing.
 */
goog.testing.TestCase.prototype.finalize = function() {
  'use strict';
  this.saveMessage('Done');

  try {
    this.tearDownPage();
  } catch (e) {
    // Report the error and continue with tests.
    window['onerror'](e.toString(), document.location.href, 0, 0, e);
  }

  this.endTime_ = this.now();
  this.running = false;
  this.result_.runTime = this.endTime_ - this.startTime_;
  this.result_.numFilesLoaded = this.countNumFilesLoaded_();
  this.result_.complete = true;
  this.testsRanSoFar_++;

  this.log(this.result_.getSummary());
  if (this.result_.isSuccess()) {
    this.log('Tests complete');
  } else {
    this.log('Tests Failed');
  }
  this.onCompletedCallbacks_.forEach(function(cb) {
    'use strict';
    cb();
  });
  this.onCompletedCallbacks_ = [];
  this.groupLogsEnd();
  this.cspViolationObserver_.stop();
};


/**
 * Saves a message to the result set.
 * @param {string} message The message to save.
 */
goog.testing.TestCase.prototype.saveMessage = function(message) {
  'use strict';
  this.result_.messages.push(this.getTimeStamp_() + '  ' + message);
};


/**
 * @return {boolean} Whether the test case is running inside the multi test
 *     runner.
 */
goog.testing.TestCase.prototype.isInsideMultiTestRunner = function() {
  'use strict';
  var top = goog.global['top'];
  return top && typeof top['_allTests'] != 'undefined';
};

/**
 * @return {boolean} Whether the test-progress should be logged to the console.
 */
goog.testing.TestCase.prototype.shouldLogTestProgress = function() {
  'use strict';
  return !goog.global['skipClosureTestProgress'] &&
      !this.isInsideMultiTestRunner();
};

/**
 * Logs an object to the console, if available.
 * @param {*} val The value to log. Will be ToString'd.
 */
goog.testing.TestCase.prototype.log = function(val) {
  'use strict';
  if (this.shouldLogTestProgress() && goog.global.console) {
    if (typeof val == 'string') {
      val = this.getTimeStamp_() + ' : ' + val;
    }
    if (val instanceof Error && val.stack) {
      goog.global.console.log(val.stack);
    } else {
      goog.global.console.log(val);
    }
  }
};


/**
 * Groups the upcoming logs in the same log group
 */
goog.testing.TestCase.prototype.groupLogsStart = function() {
  'use strict';
  if (!this.isInsideMultiTestRunner() && goog.global.console &&
      goog.global.console.group) {
    goog.global.console.group(
        'Test #' + (this.testsRanSoFar_ + 1) + ': ' + this.name_);
  }
};


/**
 * Closes the group of the upcoming logs
 */
goog.testing.TestCase.prototype.groupLogsEnd = function() {
  'use strict';
  if (!this.isInsideMultiTestRunner() && goog.global.console &&
      goog.global.console.groupEnd) {
    goog.global.console.groupEnd();
  }
};


/**
 * @return {boolean} Whether the test was a success.
 */
goog.testing.TestCase.prototype.isSuccess = function() {
  'use strict';
  return !!this.result_ && this.result_.isSuccess();
};


/**
 * Returns a string detailing the results from the test.
 * @param {boolean=} opt_verbose If true results will include data about all
 *     tests, not just what failed.
 * @return {string} The results from the test.
 */
goog.testing.TestCase.prototype.getReport = function(opt_verbose) {
  'use strict';
  var rv = [];

  if (this.running) {
    rv.push(this.name_ + ' [RUNNING]');
  } else if (this.result_.runCount == 0) {
    rv.push(this.name_ + ' [NO TESTS RUN]');
  } else {
    var label = this.result_.isSuccess() ? 'PASSED' : 'FAILED';
    rv.push(this.name_ + ' [' + label + ']');
  }

  if (goog.global.location) {
    rv.push(this.trimPath_(goog.global.location.href));
  }

  rv.push(this.result_.getSummary());

  if (opt_verbose) {
    rv.push('.', this.result_.messages.join('\n'));
  } else if (!this.result_.isSuccess()) {
    rv.push(this.result_.errors.join('\n'));
  }

  rv.push(' ');

  return rv.join('\n');
};


/**
 * Returns the test results.
 * @return {!goog.testing.TestCase.Result}
 * @package
 */
goog.testing.TestCase.prototype.getResult = function() {
  'use strict';
  return this.result_;
};


/**
 * Returns the amount of time it took for the test to run.
 * @return {number} The run time, in milliseconds.
 */
goog.testing.TestCase.prototype.getRunTime = function() {
  'use strict';
  return this.result_.runTime;
};


/**
 * Returns the number of script files that were loaded in order to run the test.
 * @return {number} The number of script files.
 */
goog.testing.TestCase.prototype.getNumFilesLoaded = function() {
  'use strict';
  return this.result_.numFilesLoaded;
};


/**
 * Represents a test result.
 * @typedef {{
 *     'source': string,
 *     'message': string,
 *     'stacktrace': string
 * }}
 */
goog.testing.TestCase.IResult;

/**
 * Returns the test results object: a map from test names to a list of test
 * failures (if any exist).
 * @return {!Object<string, !Array<goog.testing.TestCase.IResult>>} Test
 *     results object.
 */
goog.testing.TestCase.prototype.getTestResults = function() {
  'use strict';
  var map = {};
  goog.object.forEach(this.result_.resultsByName, function(resultArray, key) {
    'use strict';
    // Make sure we only use properties on the actual map
    if (!Object.prototype.hasOwnProperty.call(
            this.result_.resultsByName, key)) {
      return;
    }
    map[key] = [];
    for (var j = 0; j < resultArray.length; j++) {
      map[key].push(resultArray[j].toObject_());
    }
  }, this);
  return map;
};

/**
 * Executes each of the tests, yielding asynchronously if execution time
 * exceeds {@link #maxRunTime}. There is no guarantee that the test case
 * has finished execution once this method has returned.
 * To be notified when the test case has finished execution, use
 * {@link #addCompletedCallback} or {@link #runTestsReturningPromise}.
 *
 * Overridable by the individual test case.  This allows test cases to defer
 * when the test is actually started.  If overridden, finalize must be
 * called by the test to indicate it has finished.
 */
goog.testing.TestCase.prototype.runTests = function() {
  'use strict';
  goog.testing.TestCase.Continuation_.run(this.runSetUpPage_(this.execute));
};

/**
 * Configures the TestCase to use native Promises when waiting for methods that
 * return Thenables.
 */
goog.testing.TestCase.prototype.useNativePromise = function() {
  this.useNativePromise_ = true;
};

/**
 * Configures the TestCase to use goog.Promise when waiting for methods that
 * return Thenables.
 */
goog.testing.TestCase.prototype.useGoogPromise = function() {
  this.useNativePromise_ = false;
};


/**
 * Executes each of the tests, returning a promise that resolves with the
 * test results once they are done running.
 * @return {!IThenable<!goog.testing.TestCase.Result>}
 * @final
 * @package
 */
goog.testing.TestCase.prototype.runTestsReturningPromise = function() {
  'use strict';
  /**
   * @param {function(!goog.testing.TestCase.Result)} resolve
   */
  const resolver = (resolve) => {
    'use strict';
    goog.testing.TestCase.Continuation_.run(this.runSetUpPage_(() => {
      'use strict';
      if (!this.prepareForRun_()) {
        resolve(this.result_);
        return;
      }
      this.groupLogsStart();
      this.log('Starting tests: ' + this.name_);
      this.saveMessage('Start');
      this.batchTime_ = this.now();
      this.runNextTestCallback_ = resolve;
      goog.testing.TestCase.Continuation_.run(this.runNextTest_());
    }));
  };
  if (this.useNativePromise_) {
    return new Promise(resolver);
  }
  return new goog.Promise(resolver);
};


/**
 * Runs the setUpPage methods.
 * @param {function(this:goog.testing.TestCase)} runTestsFn Callback to invoke
 *     after setUpPage has completed.
 * @return {?goog.testing.TestCase.Continuation_}
 * @private
 */
goog.testing.TestCase.prototype.runSetUpPage_ = function(runTestsFn) {
  'use strict';
  const reports = goog.testing.CspViolationObserver.getBufferedReports();

  const ret = this.invokeFunction_(this.setUpPage, runTestsFn, function(e) {
    'use strict';
    this.exceptionBeforeTest = e;
    runTestsFn.call(this);
  }, 'setUpPage');

  if (!this.ignoreStartupCspViolations_ && reports.length > 0) {
    const msg =
        'One or more Content Security Policy violations occurred on the page ' +
        'before the first test was run: ' +
        goog.testing.CspViolationObserver.formatReports(reports);
    // This CSP violation takes precedence over any pre-existing exception.
    this.exceptionBeforeTest = msg;
  }

  return ret;
};


/**
 * Executes the next test method synchronously or with promises, depending on
 * the test method's return value.
 *
 * If the test method returns a promise, the next test method will run once
 * the promise is resolved or rejected. If the test method does not
 * return a promise, it is assumed to be synchronous, and execution proceeds
 * immediately to the next test method. This means that test cases can run
 * partially synchronously and partially asynchronously, depending on
 * the return values of their test methods. In particular, a test case
 * executes synchronously until the first promise is returned from a
 * test method (or until a resource limit is reached; see
 * {@link finishTestInvocation_}).
 * @return {?goog.testing.TestCase.Continuation_}
 * @private
 */
goog.testing.TestCase.prototype.runNextTest_ = function() {
  'use strict';
  this.curTest_ = this.next();
  if (!this.curTest_ || !this.running) {
    this.finalize();
    return new goog.testing.TestCase.Continuation_(
        goog.bind(this.runNextTestCallback_, this, this.result_));
  }

  var shouldRunTest = true;
  try {
    shouldRunTest = this.shouldRunTestsHelper_();
  } catch (error) {
    this.curTest_.name = 'shouldRunTests for ' + this.curTest_.name;
    return new goog.testing.TestCase.Continuation_(
        goog.bind(this.finishTestInvocation_, this, error));
  }

  if (!shouldRunTest) {
    return new goog.testing.TestCase.Continuation_(
        goog.bind(this.finishTestInvocation_, this));
  }

  this.cspViolationObserver_.setEnabled(true);
  this.curTest_.started();
  this.result_.runCount++;
  this.log('Running test: ' + this.curTest_.name);
  if (this.maybeFailTestEarly(this.curTest_)) {
    return new goog.testing.TestCase.Continuation_(
        goog.bind(this.finishTestInvocation_, this));
  }
  goog.testing.TestCase.currentTestName = this.curTest_.name;
  return this.safeSetUp_();
};


/**
 * @return {boolean}
 * @private
 */
goog.testing.TestCase.prototype.shouldRunTestsHelper_ = function() {
  'use strict';
  var objChain =
      this.curTest_.objChain.length ? this.curTest_.objChain : [this];

  for (var i = 0; i < objChain.length; i++) {
    var obj = objChain[i];

    if (typeof obj.shouldRunTests !== 'function') {
      continue;
    }

    if (typeof obj.shouldRunTests['$cachedResult'] === 'function') {
      if (!obj.shouldRunTests['$cachedResult']()) {
        this.result_.suppressedTests.push(this.curTest_.name);
        return false;
      } else {
        continue;
      }
    }

    var result;
    (function() {
      'use strict';
      // Cache the result by storing a function. This way we only call
      // shouldRunTests once per object in the chain. This enforces that people
      // do not attempt to suppress some tests and not others with the same
      // shouldRunTests function.
      try {
        var cached = result = obj.shouldRunTests.call(obj);
        obj.shouldRunTests['$cachedResult'] = function() {
          'use strict';
          return cached;
        };
      } catch (error) {
        obj.shouldRunTests['$cachedResult'] = function() {
          'use strict';
          throw error;
        };
        throw error;
      }
    })();

    if (!result) {
      this.result_.suppressedTests.push(this.curTest_.name);
      return false;
    }
  }

  return true;
};

/**
 * Runs all the setups associated with a test.
 * @return {?goog.testing.TestCase.Continuation_}
 * @private
 */
goog.testing.TestCase.prototype.safeSetUp_ = function() {
  'use strict';
  var setUps =
      this.curTest_.setUps.length ? this.curTest_.setUps.slice() : [this.setUp];
  return this.safeSetUpHelper_(setUps).call(this);
};

/**
 * Recursively invokes setUp functions.
 * @param {!Array<function()>} setUps
 * @return {function(): ?goog.testing.TestCase.Continuation_}
 * @private
 */
goog.testing.TestCase.prototype.safeSetUpHelper_ = function(setUps) {
  'use strict';
  if (!setUps.length) {
    return this.safeRunTest_;
  }
  return goog.bind(
      this.invokeFunction_, this, setUps.shift(), this.safeSetUpHelper_(setUps),
      this.safeTearDown_, 'setUp');
};

/**
 * Calls the given test function, handling errors appropriately.
 * @return {?goog.testing.TestCase.Continuation_}
 * @private
 */
goog.testing.TestCase.prototype.safeRunTest_ = function() {
  'use strict';
  return this.invokeFunction_(
      goog.bind(this.curTest_.ref, this.curTest_.scope), this.safeTearDown_,
      this.safeTearDown_, this.curTest_.name);
};


/**
 * Calls {@link tearDown}, handling errors appropriately.
 * @param {*=} opt_error Error associated with the test, if any.
 * @return {?goog.testing.TestCase.Continuation_}
 * @private
 */
goog.testing.TestCase.prototype.safeTearDown_ = function(opt_error) {
  'use strict';
  // If the test itself failed, report that before running any tearDown()s.
  if (arguments.length == 1) {
    this.recordError(this.curTest_.name, opt_error);
  }
  var tearDowns = this.curTest_.tearDowns.length ?
      this.curTest_.tearDowns.slice() :
      [this.tearDown];
  return this.safeTearDownHelper_(tearDowns).call(this);
};

/**
 * Recursively invokes tearDown functions.
 * @param {!Array<function()>} tearDowns
 * @return {function(): ?goog.testing.TestCase.Continuation_}
 * @private
 */
goog.testing.TestCase.prototype.safeTearDownHelper_ = function(tearDowns) {
  'use strict';
  if (!tearDowns.length) {
    return this.finishTestInvocation_;
  }
  return goog.bind(
      this.invokeFunction_, this, tearDowns.shift(),
      this.safeTearDownHelper_(tearDowns), this.finishTestInvocation_,
      'tearDown');
};


/**
 * Calls the given `fn`, then calls either `onSuccess` or
 * `onFailure`, either synchronously or using promises, depending on
 * `fn`'s return value.
 *
 * If `fn` throws an exception, `onFailure` is called immediately
 * with the exception.
 *
 * If `fn` returns a promise, and the promise is eventually resolved,
 * `onSuccess` is called with no arguments. If the promise is eventually
 * rejected, `onFailure` is called with the rejection reason.
 *
 * Otherwise, if `fn` neither returns a promise nor throws an exception,
 * `onSuccess` is called immediately with no arguments.
 *
 * `fn`, `onSuccess`, and `onFailure` are all called with
 * the TestCase instance as the method receiver.
 *
 * @param {function()} fn The function to call.
 * @param {function(this:goog.testing.TestCase):
 *     (?goog.testing.TestCase.Continuation_|undefined)} onSuccess
 * @param {function(this:goog.testing.TestCase, *):
 *     (?goog.testing.TestCase.Continuation_|undefined)} onFailure
 * @param {string} fnName Name of the function being invoked e.g. 'setUp'.
 * @return {?goog.testing.TestCase.Continuation_}
 * @private
 */
goog.testing.TestCase.prototype.invokeFunction_ = function(
    fn, onSuccess, onFailure, fnName) {
  'use strict';
  var self = this;
  this.thrownAssertionExceptions_ = [];
  try {
    this.cspViolationObserver_.start();
    var retval = fn.call(this);
    if (goog.Thenable.isImplementedBy(retval) ||
        (retval && typeof retval['then'] === 'function')) {
      // Resolve Thenable into a proper Promise to avoid hard to debug
      // problems.
      let promise;
      if (this.useNativePromise_) {
        promise = Promise.resolve(retval);
      } else {
        promise = goog.Promise.resolve(retval);
      }
      promise = this.rejectIfPromiseTimesOut_(
          promise, self.promiseTimeout,
          'Timed out while waiting for a promise returned from ' + fnName +
              ' to resolve. Set goog.testing.TestCase.getActiveTestCase()' +
              '.promiseTimeout to adjust the timeout.');
      promise.then(
          function() {
            'use strict';
            self.resetBatchTimeAfterPromise_();
            self.checkCspViolations_(fnName);
            if (self.thrownAssertionExceptions_.length == 0) {
              goog.testing.TestCase.Continuation_.run(onSuccess.call(self));
            } else {
              goog.testing.TestCase.Continuation_.run(onFailure.call(
                  self, self.reportUnpropagatedAssertionExceptions_(fnName)));
            }
          },
          function(e) {
            'use strict';
            self.reportUnpropagatedAssertionExceptions_(fnName, e);
            self.resetBatchTimeAfterPromise_();
            self.checkCspViolations_(fnName);
            goog.testing.TestCase.Continuation_.run(onFailure.call(self, e));
          });
      return null;
    } else {
      this.checkCspViolations_(fnName);
      if (this.thrownAssertionExceptions_.length == 0) {
        return new goog.testing.TestCase.Continuation_(
            goog.bind(onSuccess, this));
      } else {
        return new goog.testing.TestCase.Continuation_(goog.bind(
            onFailure, this,
            this.reportUnpropagatedAssertionExceptions_(fnName)));
      }
    }
  } catch (e) {
    this.checkCspViolations_(fnName);
    this.reportUnpropagatedAssertionExceptions_(fnName, e);
    return new goog.testing.TestCase.Continuation_(
        goog.bind(onFailure, this, e));
  }
};


/**
 * Logs all of the exceptions generated from failing assertions, and returns a
 * generic exception informing the user that one or more exceptions were not
 * propagated, causing the test to erroneously pass.
 *
 * This is also called when a test fails so that the user sees swallowed errors.
 * (This can make it much easier to debug failures in callbacks in catch blocks)
 * If the actually-thrown error (that made the test fail) is also a JSUnit error
 * (which will therefore be in this array), it will be silently deduped when the
 * regular failure handler tries to record it again.
 * @param {string} testName The test function's name.
 * @param {*=} actualError The thrown error the made the test fail, if any
 * @return {!goog.testing.JsUnitException}
 * @private
 */
goog.testing.TestCase.prototype.reportUnpropagatedAssertionExceptions_ =
    function(testName, actualError) {
  'use strict';
  var extraExceptions = this.thrownAssertionExceptions_.slice();
  // If the actual error isn't a JSUnit exception, it won't be in this array.
  goog.array.remove(extraExceptions, actualError);
  var numExceptions = extraExceptions.length;
  if (numExceptions && actualError) {
    // Don't log this message if the only exception is the actual failure.
    var message =
        numExceptions + ' additional exceptions were swallowed by the test:';
    this.log(message);
    this.saveMessage(message);
  }


  for (var i = 0; i < numExceptions; i++) {
    this.recordError(testName, extraExceptions[i]);
  }

  // Mark the test as failed.
  return new goog.testing.JsUnitException(
      'One or more assertions were raised but not caught by the testing ' +
      'framework. These assertions may have been unintentionally captured ' +
      'by a catch block or a thenCatch resolution of a Promise.');
};


/**
 * Resets the batch run timer. This should only be called after resolving a
 * promise since Promise.then() has an implicit yield.
 * @private
 */
goog.testing.TestCase.prototype.resetBatchTimeAfterPromise_ = function() {
  'use strict';
  this.batchTime_ = this.now();
};


/**
 * Finishes up bookkeeping for the current test function, and schedules
 * the next test function to run, either immediately or asychronously.
 * @param {*=} opt_error Optional error resulting from the test invocation.
 * @return {?goog.testing.TestCase.Continuation_}
 * @private
 */
goog.testing.TestCase.prototype.finishTestInvocation_ = function(opt_error) {
  'use strict';
  if (arguments.length == 1) {
    this.recordError(this.curTest_.name, opt_error);
  }

  // If no errors have been recorded for the test, it is a success.
  if (!(this.curTest_.name in this.result_.resultsByName) ||
      !this.result_.resultsByName[this.curTest_.name].length) {
    if (this.result_.suppressedTests.indexOf(this.curTest_.name) >= 0) {
      this.doSkipped(this.curTest_);
    } else {
      this.doSuccess(this.curTest_);
    }
  } else {
    this.doError(this.curTest_);
  }

  goog.testing.TestCase.currentTestName = null;

  // If the test case has consumed too much time or stack space,
  // yield to avoid blocking the browser. Otherwise, proceed to the next test.
  if (this.now() - this.batchTime_ > goog.testing.TestCase.maxRunTime) {
    this.saveMessage('Breaking async');
    this.timeout(goog.bind(this.startNextBatch_, this), 0);
    return null;
  } else {
    return new goog.testing.TestCase.Continuation_(
        goog.bind(this.runNextTest_, this));
  }
};


/**
 * Checks if any CSP violations have been logged since
 * this.cspViolationObserver_.start() was called and reports them as errors.
 *
 * @param {string} name
 * @private
 */
goog.testing.TestCase.prototype.checkCspViolations_ = function(name) {
  const reports = this.cspViolationObserver_.stop();
  if (reports.length == 0) {
    return;
  }

  const formattedReports =
      goog.testing.CspViolationObserver.formatReports(reports);
  const msg =
      'One or more Content Security Policy violations occurred during ' +
      'execution of this test: ' + formattedReports;
  if (this.started) {
    this.recordError(name, msg);
  } else {
    this.exceptionBeforeTest = msg;
  }
};


/**
 * Start a new batch to tests after yielding, resetting batchTime and depth.
 * @private
 */
goog.testing.TestCase.prototype.startNextBatch_ = function() {
  'use strict';
  this.batchTime_ = this.now();
  goog.testing.TestCase.Continuation_.run(this.runNextTest_());
};


/**
 * Reorders the tests depending on the `order` field.
 * @private
 */
goog.testing.TestCase.prototype.orderTests_ = function() {
  'use strict';
  switch (this.order) {
    case goog.testing.TestCase.Order.RANDOM:
      // Fisher-Yates shuffle
      var i = this.tests_.length;
      while (i > 1) {
        // goog.math.randomInt is inlined to reduce dependencies.
        var j = Math.floor(Math.random() * i);  // exclusive
        i--;
        var tmp = this.tests_[i];
        this.tests_[i] = this.tests_[j];
        this.tests_[j] = tmp;
      }
      break;

    case goog.testing.TestCase.Order.SORTED:
      this.tests_.sort(function(t1, t2) {
        'use strict';
        if (t1.name == t2.name) {
          return 0;
        }
        return t1.name < t2.name ? -1 : 1;
      });
      break;

      // Do nothing for NATURAL.
  }
};


/**
 * Gets list of objects that potentially contain test cases. For IE 8 and
 * below, this is the global "this" (for properties set directly on the global
 * this or window) and the RuntimeObject (for global variables and functions).
 * For all other browsers, the array simply contains the global this.
 *
 * @param {string=} opt_prefix An optional prefix. If specified, only get things
 *     under this prefix. Note that the prefix is only honored in IE, since it
 *     supports the RuntimeObject:
 *     http://msdn.microsoft.com/en-us/library/ff521039%28VS.85%29.aspx
 *     TODO: Remove this option.
 * @return {!Array<!Object>} A list of objects that should be inspected.
 */
goog.testing.TestCase.prototype.getGlobals = function(opt_prefix) {
  'use strict';
  return goog.testing.TestCase.getGlobals(opt_prefix);
};


/**
 * Gets list of objects that potentially contain test cases. For IE 8 and
 * below, this is the global "this" (for properties set directly on the global
 * this or window) and the RuntimeObject (for global variables and functions).
 * For all other browsers, the array simply contains the global this.
 *
 * @param {string=} opt_prefix An optional prefix. If specified, only get things
 *     under this prefix. Note that the prefix is only honored in IE, since it
 *     supports the RuntimeObject:
 *     http://msdn.microsoft.com/en-us/library/ff521039%28VS.85%29.aspx
 *     TODO: Remove this option.
 * @return {!Array<!Object>} A list of objects that should be inspected.
 */
goog.testing.TestCase.getGlobals = function(opt_prefix) {
  'use strict';
  // Look in the global scope for most browsers, on IE we use the little known
  // RuntimeObject which holds references to all globals. We reference this
  // via goog.global so that there isn't an aliasing that throws an exception
  // in Firefox.
  return typeof goog.global['RuntimeObject'] != 'undefined' ?
      [goog.global['RuntimeObject']((opt_prefix || '') + '*'), goog.global] :
      [goog.global];
};


/**
 * @private {?goog.testing.TestCase}
 */
goog.testing.TestCase.activeTestCase_ = null;


/**
 * @return {?goog.testing.TestCase} currently active test case or null if not
 *     test is currently running. Tries the G_testRunner first then the stored
 *     value (when run outside of G_testRunner.
 */
goog.testing.TestCase.getActiveTestCase = function() {
  'use strict';
  var gTestRunner = goog.global['G_testRunner'];
  if (gTestRunner && gTestRunner.testCase) {
    return gTestRunner.testCase;
  } else {
    return goog.testing.TestCase.activeTestCase_;
  }
};


/**
 * Calls {@link goog.testing.TestCase.prototype.invalidateAssertionException}
 * on the active test case if it is installed, and logs an error otherwise.
 * @param {!goog.testing.JsUnitException} e The exception object to invalidate.
 * @package
 */
goog.testing.TestCase.invalidateAssertionException = function(e) {
  'use strict';
  var testCase = goog.testing.TestCase.getActiveTestCase();
  if (testCase) {
    testCase.invalidateAssertionException(e);
  } else {
    goog.global.console.error(
        'Failed to remove expected exception: no test case is installed.');
  }
};


/**
 * Gets called before any tests are executed.  Can be overridden to set up the
 * environment for the whole test case.
 * @return {!Thenable|undefined}
 */
goog.testing.TestCase.prototype.setUpPage = function() {};


/**
 * Gets called after all tests have been executed.  Can be overridden to tear
 * down the entire test case.
 */
goog.testing.TestCase.prototype.tearDownPage = function() {};


/**
 * Gets called before every goog.testing.TestCase.Test is been executed. Can
 * be overridden to add set up functionality to each test.
 * @return {!Thenable|undefined}
 */
goog.testing.TestCase.prototype.setUp = function() {};


/**
 * Gets called after every goog.testing.TestCase.Test has been executed. Can
 * be overridden to add tear down functionality to each test.
 * @return {!Thenable|undefined}
 */
goog.testing.TestCase.prototype.tearDown = function() {};


/**
 * @return {string} The function name prefix used to auto-discover tests.
 */
goog.testing.TestCase.prototype.getAutoDiscoveryPrefix = function() {
  'use strict';
  return 'test';
};


/**
 * @return {number} Time since the last batch of tests was started.
 * @protected
 */
goog.testing.TestCase.prototype.getBatchTime = function() {
  'use strict';
  return this.batchTime_;
};


/**
 * @param {number} batchTime Time since the last batch of tests was started.
 * @protected
 */
goog.testing.TestCase.prototype.setBatchTime = function(batchTime) {
  'use strict';
  this.batchTime_ = batchTime;
};


/**
 * Creates a `goog.testing.TestCase.Test` from an auto-discovered
 *     function.
 * @param {string} name The name of the function.
 * @param {function()} ref The auto-discovered function.
 * @param {!Object=} scope The scope to attach to the test.
 * @param {!Array<!Object>=} objChain
 * @return {!goog.testing.TestCase.Test} The newly created test.
 * @protected
 */
goog.testing.TestCase.prototype.createTest = function(
    name, ref, scope, objChain) {
  'use strict';
  return new goog.testing.TestCase.Test(name, ref, scope, objChain);
};


/**
 * Adds any functions defined on the global object
 * that correspond to lifecycle events for the test case. Overrides
 * setUp, tearDown, setUpPage, tearDownPage, runTests, and shouldRunTests
 * if they are defined on global object.
 */
goog.testing.TestCase.prototype.autoDiscoverLifecycle = function() {
  'use strict';
  this.setLifecycleObj(goog.global);
};


// TODO(johnlenz): make this package private
/**
 * Extracts any functions defined on 'obj' that correspond to page lifecycle
 * events (setUpPage, tearDownPage, runTests, shouldRunTests) and add them to
 * on this test case.
 * @param {!Object} obj
 */
goog.testing.TestCase.prototype.setLifecycleObj = function(obj) {
  'use strict';
  if (obj['setUp']) {
    this.setUp = goog.bind(obj['setUp'], obj);
  }
  if (obj['tearDown']) {
    this.tearDown = goog.bind(obj['tearDown'], obj);
  }
  if (obj['setUpPage']) {
    this.setUpPage = goog.bind(obj['setUpPage'], obj);
  }
  if (obj['tearDownPage']) {
    this.tearDownPage = goog.bind(obj['tearDownPage'], obj);
  }
  if (obj['runTests']) {
    this.runTests = goog.bind(obj['runTests'], obj);
  }
  if (obj['shouldRunTests']) {
    this.shouldRunTests = goog.bind(obj['shouldRunTests'], obj);
  }
};


// TODO(johnlenz): make this package private
/**
 * @param {!Object} obj  An object from which to extract test and lifecycle
 * methods.
 */
goog.testing.TestCase.prototype.setTestObj = function(obj) {
  'use strict';
  // Check any previously added (likely auto-discovered) tests, only one source
  // of discovered test and life-cycle methods is allowed.
  if (this.tests_.length > 0) {
    throw new Error(
        'Test methods have already been configured.\n' +
        'Tests previously found:\n' +
        this.tests_
            .map(function(test) {
              'use strict';
              return test.name;
            })
            .join('\n') +
        '\nNew tests found:\n' +
        Object.keys(obj)
            .filter(function(name) {
              'use strict';
              return name.startsWith('test');
            })
            .join('\n'));
  }
  this.shouldAutoDiscoverTests_ = false;
  if (obj['getTestName']) {
    this.name_ = obj['getTestName']();
  }
  this.setLifecycleObj(obj);
  this.addTestObj_(obj, '', [this]);
};

/**
 * @param {!Object} obj  An object from which to extract test and lifecycle
 *     methods.
 * @param {string} name
 * @param {!Array<!Object>} objChain List of objects that have methods used
 *     to create tests such as setUp, tearDown.
 * @private
 */
goog.testing.TestCase.prototype.addTestObj_ = function(obj, name, objChain) {
  'use strict';
  var regex = new RegExp('^' + this.getAutoDiscoveryPrefix());
  var properties = goog.object.getAllPropertyNames(obj);
  for (var i = 0; i < properties.length; i++) {
    var testName = properties[i];
    if (regex.test(testName)) {
      var testProperty;
      try {
        testProperty = obj[testName];
      } catch (ex) {
        // NOTE(brenneman): When running tests from a file:// URL on Firefox
        // 3.5 for Windows, any reference to goog.global.sessionStorage raises
        // an "Operation is not supported" exception. Ignore any exceptions
        // raised by simply accessing global properties.
        testProperty = null;
      }
      if (name) {
        testName = testName.slice(this.getAutoDiscoveryPrefix().length);
      }
      var fullTestName = name + (testName && name ? '_' : '') + testName;
      if (typeof testProperty === 'function') {
        this.addNewTest(fullTestName, testProperty, obj, objChain);
      } else if (goog.isObject(testProperty) && !Array.isArray(testProperty)) {
        // To prevent infinite loops.
        if (!goog.array.contains(objChain, testProperty)) {
          goog.asserts.assertObject(testProperty);
          var newObjChain = objChain.slice();
          newObjChain.push(testProperty);
          this.addTestObj_(testProperty, fullTestName, newObjChain);
        }
      }
    }
  }
};


/**
 * Adds any functions defined in the global scope that are prefixed with
 * "test" to the test case.
 */
goog.testing.TestCase.prototype.autoDiscoverTests = function() {
  'use strict';
  this.autoDiscoverLifecycle();
  var prefix = this.getAutoDiscoveryPrefix();
  var testSources = this.getGlobals(prefix);

  for (var i = 0; i < testSources.length; i++) {
    var testSource = testSources[i];
    this.addTestObj_(testSource, '', [this]);
  }

  this.orderTests_();
};


/**
 * Checks to see if the test should be marked as failed before it is run.
 *
 * If there was an error in setUpPage, we treat that as a failure for all
 * tests and mark them all as having failed.
 *
 * @param {goog.testing.TestCase.Test} testCase The current test case.
 * @return {boolean} Whether the test was marked as failed.
 * @protected
 */
goog.testing.TestCase.prototype.maybeFailTestEarly = function(testCase) {
  'use strict';
  if (this.exceptionBeforeTest) {
    // We just use the first error to report an error on a failed test.
    testCase.name = 'setUpPage for ' + testCase.name;
    this.recordError(testCase.name, this.exceptionBeforeTest);
    return true;
  }
  return false;
};


/**
 * Cycles through the tests, yielding asynchronously if the execution time
 * exceeds {@link #maxRunTime}. In particular, there is no guarantee that
 * the test case has finished execution once this method has returned.
 * To be notified when the test case has finished execution, use
 * {@link #addCompletedCallback} or {@link #runTestsReturningPromise}.
 */
goog.testing.TestCase.prototype.cycleTests = function() {
  'use strict';
  this.saveMessage('Start');
  this.batchTime_ = this.now();
  if (this.running) {
    this.runNextTestCallback_ = () => {};
    // Kick off the tests. runNextTest_ will schedule all of the tests,
    // using a mixture of synchronous and asynchronous strategies.
    goog.testing.TestCase.Continuation_.run(this.runNextTest_());
  }
};


/**
 * Counts the number of files that were loaded for dependencies that are
 * required to run the test.
 * @return {number} The number of files loaded.
 * @private
 */
goog.testing.TestCase.prototype.countNumFilesLoaded_ = function() {
  'use strict';
  var scripts = goog.dom.getElementsByTagName(goog.dom.TagName.SCRIPT);
  var count = 0;
  for (var i = 0, n = scripts.length; i < n; i++) {
    if (scripts[i].src) {
      count++;
    }
  }
  return count;
};


/**
 * Calls a function after a delay, using the protected timeout.
 * @param {Function} fn The function to call.
 * @param {number} time Delay in milliseconds.
 * @return {number} The timeout id.
 * @protected
 */
goog.testing.TestCase.prototype.timeout = function(fn, time) {
  'use strict';
  // NOTE: invoking protectedSetTimeout_ as a member of goog.testing.TestCase
  // would result in an Illegal Invocation error. The method must be executed
  // with the global context.
  var protectedSetTimeout = goog.testing.TestCase.protectedSetTimeout_;
  return protectedSetTimeout(fn, time);
};


/**
 * Clears a timeout created by `this.timeout()`.
 * @param {number} id A timeout id.
 * @protected
 */
goog.testing.TestCase.prototype.clearTimeout = function(id) {
  'use strict';
  // NOTE: see execution note for protectedSetTimeout above.
  var protectedClearTimeout = goog.testing.TestCase.protectedClearTimeout_;
  protectedClearTimeout(id);
};


/**
 * @return {number} The current time in milliseconds.
 * @protected
 */
goog.testing.TestCase.prototype.now = function() {
  'use strict';
  return goog.testing.TestCase.now();
};


/**
 * @return {number} The current time in milliseconds.
 * @protected
 */
goog.testing.TestCase.now = function() {
  'use strict';
  // don't use goog.now as some tests override it.
  if (goog.testing.TestCase.protectedPerformance_) {
    return goog.testing.TestCase.protectedPerformance_.now();
  }
  // Fallback for IE8
  // Cannot use "new goog.testing.TestCase.protectedDate_()" due to b/8323223.
  var protectedDate = goog.testing.TestCase.protectedDate_;
  return new protectedDate().getTime();
};


/**
 * Returns the current time.
 * @return {string} HH:MM:SS.
 * @private
 */
goog.testing.TestCase.prototype.getTimeStamp_ = function() {
  'use strict';
  // Cannot use "new goog.testing.TestCase.protectedDate_()" due to b/8323223.
  var protectedDate = goog.testing.TestCase.protectedDate_;
  var d = new protectedDate();

  // Ensure millis are always 3-digits
  var millis = '00' + d.getMilliseconds();
  millis = millis.slice(-3);

  return this.pad_(d.getHours()) + ':' + this.pad_(d.getMinutes()) + ':' +
      this.pad_(d.getSeconds()) + '.' + millis;
};


/**
 * Pads a number to make it have a leading zero if it's less than 10.
 * @param {number} number The number to pad.
 * @return {string} The resulting string.
 * @private
 */
goog.testing.TestCase.prototype.pad_ = function(number) {
  'use strict';
  return number < 10 ? '0' + number : String(number);
};


/**
 * Trims a path to be only that after google3.
 * @param {string} path The path to trim.
 * @return {string} The resulting string.
 * @private
 */
goog.testing.TestCase.prototype.trimPath_ = function(path) {
  'use strict';
  return path.substring(path.indexOf('google3') + 8);
};


/**
 * Handles a test that passed.
 * @param {goog.testing.TestCase.Test} test The test that passed.
 * @protected
 */
goog.testing.TestCase.prototype.doSuccess = function(test) {
  'use strict';
  this.result_.successCount++;
  // An empty list of error messages indicates that the test passed.
  // If we already have a failure for this test, do not set to empty list.
  if (!(test.name in this.result_.resultsByName)) {
    this.result_.resultsByName[test.name] = [];
  }
  var message = test.name + ' : PASSED';
  this.saveMessage(message);
  this.log(message);
  if (this.testDone_) {
    this.doTestDone_(test, []);
  }
};


/**
 * Handles a test that was skipped.
 * @param {!goog.testing.TestCase.Test} test The test that was skipped.
 * @protected
 */
goog.testing.TestCase.prototype.doSkipped = function(test) {
  'use strict';
  this.result_.skipCount++;
  // An empty list of error messages indicates that the test passed.
  // If we already have a failure for this test, do not set to empty list.
  if (!(test.name in this.result_.resultsByName)) {
    this.result_.resultsByName[test.name] = [];
  }
  var message = test.name + ' : SKIPPED';
  this.saveMessage(message);
  this.log(message);
  if (this.testDone_) {
    this.doTestDone_(test, []);
  }
};


/**
 * Records an error that fails the current test, without throwing it.
 *
 * Use this function to implement expect()-style assertion libraries that fail a
 * test without breaking execution (so you can see further failures). Do not use
 * this from normal test code.
 *
 * Please contact js-core-libraries-team@ before using this method.  If it grows
 * popular, we may add an expect() API to Closure.
 *
 * NOTE: If there is no active TestCase, you must throw an error.
 * @param {!Error} error The error to log.  If it is a JsUnitException which has
 *     already been logged, nothing will happen.
 */
goog.testing.TestCase.prototype.recordTestError = function(error) {
  'use strict';
  this.recordError(
      this.curTest_ ? this.curTest_.name : '<No active test>', error);
};



/**
 * Records and logs an error from or related to a test.
 * @param {string} testName The name of the test that failed.
 * @param {*} error The exception object associated with the
 *     failure or a string.
 * @protected
 */
goog.testing.TestCase.prototype.recordError = function(testName, error) {
  'use strict';
  if (error && error['isJsUnitException'] && error['loggedJsUnitException']) {
    // We already logged this error; don't record it again. This is particularly
    // important for errors from mocks, which are rethrown by $verify, called by
    // tearDown().
    return;
  }

  var err = this.logError(testName, error);
  this.result_.errors.push(err);
  if (testName in this.result_.resultsByName) {
    this.result_.resultsByName[testName].push(err);
  } else {
    this.result_.resultsByName[testName] = [err];
  }

  if (error && error['isJsUnitException']) {
    error['loggedJsUnitException'] = true;
  }
};


/**
 * Handles a test that failed.
 * @param {goog.testing.TestCase.Test} test The test that failed.
 * @protected
 */
goog.testing.TestCase.prototype.doError = function(test) {
  'use strict';
  var message = test.name + ' : FAILED';
  this.log(message);
  this.saveMessage(message);

  if (this.testDone_) {
    var results = this.result_.resultsByName[test.name];
    var errMsgs = [];
    for (var i = 0; i < results.length; i++) {
      errMsgs.push(results[i].toString());
    }
    this.doTestDone_(test, errMsgs);
  }
};


/**
 * Makes note of an exception arising from an assertion, and then throws it.
 * If the test otherwise passes (i.e., because something else caught the
 * exception on its way to the test framework), it will be forced to fail.
 * @param {!goog.testing.JsUnitException} e The exception object being thrown.
 * @throws {goog.testing.JsUnitException}
 * @package
 */
goog.testing.TestCase.prototype.raiseAssertionException = function(e) {
  'use strict';
  this.thrownAssertionExceptions_.push(e);
  throw e;
};


/**
 * Removes the specified exception from being tracked. This only needs to be
 * called for internal functions that intentionally catch an exception, such
 * as
 * `#assertThrowsJsUnitException`.
 * @param {!goog.testing.JsUnitException} e The exception object to invalidate.
 * @package
 */
goog.testing.TestCase.prototype.invalidateAssertionException = function(e) {
  'use strict';
  goog.array.remove(this.thrownAssertionExceptions_, e);
};


/**
 * @param {string} name Failed test name.
 * @param {*} error The exception object associated with the
 *     failure or a string.
 * @return {!goog.testing.TestCase.Error} Error object.
 * @suppress {missingProperties} message and stack properties
 */
goog.testing.TestCase.prototype.logError = function(name, error) {
  'use strict';
  if (error) {
    this.log(error);
  }

  var normalizedError = goog.debug.normalizeErrorObject(error);
  var stack =
      this.cleanStackTrace_(normalizedError.stack, normalizedError.message);
  var err =
      new goog.testing.TestCase.Error(name, normalizedError.message, stack);

  this.saveMessage(err.toString());

  return err;
};

/**
 * @param {?string} stack
 * @param {string} errMsg
 * @return {string|undefined}
 * @private
 */
goog.testing.TestCase.prototype.cleanStackTrace_ = function(stack, errMsg) {
  'use strict';
  if (!stack) {
    return;
  }

  // The Error class includes the message in the stack. Don't duplicate it.
  stack = stack.replace('Error: ' + errMsg + '\n', 'Error\n');

  // Remove extra goog.testing.TestCase frames from all stacks (main error +
  // causes if they exists)
  var index = 0;
  while (index < stack.length) {
    var extraFrameIndex = stack.search(
        /\s*(\bat\b)?\s*(goog\.labs\.testing\.EnvironmentTestCase_\.)?goog\.testing\.(Continuation_\.(prototype\.)?run|TestCase\.(prototype\.)?(execute|cycleTests|startNextBatch_|safeRunTest_|invokeFunction_?))/);
    if (extraFrameIndex < 0) {
      break;
    }

    var causedByIndex = stack.indexOf('Caused by:', extraFrameIndex);
    index = causedByIndex < 0 ? stack.length : causedByIndex;


    stack = stack.substring(0, extraFrameIndex + 1) + stack.substring(index);
  }

  return stack;
};


/**
 * A class representing a single test function.
 * @param {string} name The test name.
 * @param {?function()} ref Reference to the test function or test object.
 * @param {?Object=} scope Optional scope that the test function should be
 *     called in.
 * @param {!Array<?>=} objChain A chain of objects used to populate setUps
 *     and tearDowns.
 * @constructor
 */
goog.testing.TestCase.Test = function(name, ref, scope, objChain) {
  'use strict';
  /**
   * The name of the test.
   * @type {string}
   */
  this.name = name;

  /**
   * TODO(user): Rename this to something more clear.
   * Reference to the test function.
   * @type {function()}
   */
  this.ref = ref || function() {};

  /**
   * Scope that the test function should be called in.
   * @type {?Object}
   */
  this.scope = scope || null;

  /**
   * @type {!Array<function()>}
   */
  this.setUps = [];

  /**
   * @type {!Array<function()>}
   */
  this.tearDowns = [];

  /**
   * @type {!Array<?>}
   */
  this.objChain = objChain || [];

  if (objChain) {
    for (var i = 0; i < objChain.length; i++) {
      if (typeof objChain[i].setUp === 'function') {
        this.setUps.push(goog.bind(objChain[i].setUp, objChain[i]));
      }
      if (typeof objChain[i].tearDown === 'function') {
        this.tearDowns.push(goog.bind(objChain[i].tearDown, objChain[i]));
      }
    }
    this.tearDowns.reverse();
  }

  /**
   * Timestamp just before the test begins execution.
   * @type {number}
   * @private
   */
  this.startTime_;

  /**
   * Timestamp just after the test ends execution.
   * @type {number}
   * @private
   */
  this.stoppedTime_;

  /** @package {boolean|undefined} */
  this.waiting;
};

/**
 * Executes the test function.
 * @package
 */
goog.testing.TestCase.Test.prototype.execute = function() {
  'use strict';
  this.ref.call(this.scope);
};

/**
 * Sets the start time
 */
goog.testing.TestCase.Test.prototype.started = function() {
  'use strict';
  this.startTime_ = goog.testing.TestCase.now();
};

/**
 * Sets the stop time
 */
goog.testing.TestCase.Test.prototype.stopped = function() {
  'use strict';
  this.stoppedTime_ = goog.testing.TestCase.now();
};

/**
 * Returns the runtime for this test function in milliseconds.
 * @return {number}
 */
goog.testing.TestCase.Test.prototype.getElapsedTime = function() {
  'use strict';
  // Round the elapsed time to the closest multiple of 0.1ms (the resolution of
  // performance.now()) to avoid noise due to floating point rounding errors
  // when it's printed.
  return Math.round((this.stoppedTime_ - this.startTime_) * 10) / 10;
};

/**
 * A class for representing test results.  A bag of public properties.
 * @param {goog.testing.TestCase} testCase The test case that owns this result.
 * @constructor
 * @final
 */
goog.testing.TestCase.Result = function(testCase) {
  'use strict';
  /**
   * The test case that owns this result.
   * @type {goog.testing.TestCase}
   * @private
   */
  this.testCase_ = testCase;

  /**
   * Total number of tests that should have been run.
   * @type {number}
   */
  this.totalCount = 0;

  /**
   * Total number of tests that were actually run.
   * @type {number}
   */
  this.runCount = 0;

  /**
   * Number of successful tests.
   * @type {number}
   */
  this.successCount = 0;

  /**
   * Number of tests skipped due to nested shouldRunTests.
   * @type {number}
   */
  this.skipCount = 0;

  /**
   * The amount of time the tests took to run.
   * @type {number}
   */
  this.runTime = 0;

  /**
   * The number of files loaded to run this test.
   * @type {number}
   */
  this.numFilesLoaded = 0;

  /**
   * Whether all tests were suppressed from a top-level shouldRunTests().
   * @type {boolean}
   */
  this.testSuppressed = false;

  /**
   * Which tests were suppressed by shouldRunTests() returning false.
   * @type {!Array<string>}
   */
  this.suppressedTests = [];

  /**
   * Test results for each test that was run. The test name is always added
   * as the key in the map, and the array of strings is an optional list
   * of failure messages. If the array is empty, the test passed. Otherwise,
   * the test failed.
   * @type {!Object<string, !Array<goog.testing.TestCase.Error>>}
   */
  this.resultsByName = {};

  /**
   * Errors encountered while running the test.
   * @type {!Array<goog.testing.TestCase.Error>}
   */
  this.errors = [];

  /**
   * Messages to show the user after running the test.
   * @type {!Array<string>}
   */
  this.messages = [];

  /**
   * Whether the tests have completed.
   * @type {boolean}
   */
  this.complete = false;
};


/**
 * @return {boolean} Whether the test was successful.
 */
goog.testing.TestCase.Result.prototype.isSuccess = function() {
  'use strict';
  return this.complete && this.errors.length == 0;
};


/**
 * @return {string} A summary of the tests, including total number of tests that
 *     passed, failed, and the time taken.
 */
goog.testing.TestCase.Result.prototype.getSummary = function() {
  'use strict';
  var summary = this.runCount + ' of ' + this.totalCount + ' tests run in ' +
      Math.round(this.runTime) + ' ms.\n';
  if (this.testSuppressed) {
    summary += 'Tests not run because shouldRunTests() returned false.';
  } else {
    var failures = this.totalCount - this.successCount - this.skipCount;
    var suppressionMessage = '';

    if (this.skipCount) {
      suppressionMessage +=
          ', ' + this.skipCount + ' skipped by shouldRunTests()';
    }

    var countOfRunTests = this.testCase_.getActuallyRunCount();
    if (countOfRunTests) {
      failures = countOfRunTests - this.successCount - this.skipCount;
      suppressionMessage += ', ' + (this.totalCount - countOfRunTests) +
          ' suppressed by querystring';
    }
    summary += this.successCount + ' passed, ' + failures + ' failed' +
        suppressionMessage + '.\n' + Math.round(this.runTime / this.runCount) +
        ' ms/test. ' + this.numFilesLoaded + ' files loaded.';
  }

  return summary;
};


/**
 * @param {function(goog.testing.TestCase.Test, !Array<string>)} testDone
 */
goog.testing.TestCase.prototype.setTestDoneCallback = function(testDone) {
  'use strict';
  this.testDone_ = testDone;
};


/**
 * @param {goog.testing.TestCase.Test} test
 * @param {!Array<string>} errMsgs
 * @private
 */
goog.testing.TestCase.prototype.doTestDone_ = function(test, errMsgs) {
  'use strict';
  test.stopped();
  this.testDone_(test, errMsgs);
};

/**
 * Initializes the TestCase.
 * @param {goog.testing.TestCase} testCase The test case to install.
 * @param {function(goog.testing.TestCase.Test, Array<string>)=} opt_testDone
 *     Called when each test completes.
 */
goog.testing.TestCase.initializeTestCase = function(testCase, opt_testDone) {
  'use strict';
  if (opt_testDone) {
    testCase.setTestDoneCallback(opt_testDone);
  }

  if (testCase.shouldAutoDiscoverTests_) {
    testCase.autoDiscoverTests();
  } else {
    // Make sure the tests are still ordered based on provided order.
    testCase.orderTests_();
  }

  if (goog.global.location) {
    var href = goog.global.location.href;
    testCase.setTestsToRun(goog.testing.TestCase.parseRunTests_(href));
  }
  goog.testing.TestCase.activeTestCase_ = testCase;
};


/**
 * Initializes the given test case with the global test runner 'G_testRunner'.
 * @param {goog.testing.TestCase} testCase The test case to install.
 * @param {function(goog.testing.TestCase.Test, Array<string>)=} opt_testDone
 *     Called when each test completes.
 */
goog.testing.TestCase.initializeTestRunner = function(testCase, opt_testDone) {
  'use strict';
  goog.testing.TestCase.initializeTestCase(testCase, opt_testDone);

  var gTestRunner = goog.global['G_testRunner'];
  if (gTestRunner) {
    gTestRunner['initialize'](testCase);
  } else {
    throw new Error(
        'G_testRunner is undefined. Please ensure goog.testing.jsunit' +
        ' is included.');
  }
};


/**
 * Parses URL query parameters for the 'runTests' parameter.
 * @param {string} href The current URL.
 * @return {Object<string, boolean>} A set of test names or test indices to be
 *     run by the test runner.
 * @private
 */
goog.testing.TestCase.parseRunTests_ = function(href) {
  'use strict';
  const queryParamIndex = href.indexOf('?');
  if (queryParamIndex < 0) {
    return null;
  }

  const nonOriginParts = href.slice(queryParamIndex);

  // Use a "fake" origin because tests may load using protocols that goog.url
  // doesn't support
  const searchParams = goog.url.getSearchParams(
      goog.url.resolveUrl('https://google.com' + nonOriginParts));

  let runTestsString = null;
  for (const [key, value] of searchParams) {
    if (key.toLowerCase() === 'runtests') {
      runTestsString = value;
    }
  }

  if (!runTestsString) {
    return null;
  }

  const testsToRun = {};
  const arr = runTestsString.split(',');
  for (let i = 0, len = arr.length; i < len; i++) {
    try {
      // `TestRunner` double encodes commas in test names so we decode back here
      testsToRun[arr[i].replace(/%2C/g, ',')] = true;
    } catch (e) {
      return null;
    }
  }

  return testsToRun;
};


/**
 * Wraps provided promise and returns a new promise which will be rejected
 * if the original promise does not settle within the given timeout.
 * @param {!IThenable<T>} promise
 * @param {number} timeoutInMs Number of milliseconds to wait for the promise to
 *     settle before failing it with a timeout error.
 * @param {string} errorMsg Error message to use if the promise times out.
 * @return {!IThenable<T>} A promise that will settle with the original
       promise unless the timeout is exceeded.
 *     error.
 * @template T
 * @private
 */
goog.testing.TestCase.prototype.rejectIfPromiseTimesOut_ = function(
    promise, timeoutInMs, errorMsg) {
  'use strict';
  const start = this.now();
  /**
   * @param {function(?)} resolve
   * @param {function(*)} reject
   */
  const resolver = (resolve, reject) => {
    'use strict';
    const timeoutId = this.timeout(() => {
      'use strict';
      const elapsed = this.now() - start;
      reject(new Error(`${errorMsg}\nElapsed time: ${elapsed} ms.`));
    }, timeoutInMs);
    const clearTimeout = () => {
      this.clearTimeout(timeoutId);
    };
    promise.then(clearTimeout, clearTimeout);
    promise.then(resolve, reject);
  };
  if (this.useNativePromise_) {
    return new Promise(resolver);
  }
  return new goog.Promise(resolver);
};



/**
 * A class representing an error thrown by the test
 * @param {string} source The name of the test which threw the error.
 * @param {string} message The error message.
 * @param {string=} opt_stack A string showing the execution stack.
 * @constructor
 * @final
 */
goog.testing.TestCase.Error = function(source, message, opt_stack) {
  'use strict';
  /**
   * The name of the test which threw the error.
   * @type {string}
   */
  this.source = source;

  /**
   * Reference to the test function.
   * @type {string}
   */
  this.message = message;

  /**
   * The stack.
   * @type {?string}
   */
  this.stack = null;

  if (opt_stack) {
    this.stack = opt_stack;
  } else {
    // Attempt to capture a stack trace.
    if (Error.captureStackTrace) {
      // See https://code.google.com/p/v8-wiki/wiki/JavaScriptStackTraceApi
      Error.captureStackTrace(this, goog.testing.TestCase.Error);
    } else {
      var stack = new Error().stack;
      if (stack) {
        this.stack = stack;
      }
    }
  }
};


/**
 * Call this from setUpPage() to prevent any Content Security Policy violations
 * that may have occurred during page load from being reported as errors .
 */
goog.testing.TestCase.prototype.ignoreStartupCspViolations = function() {
  this.ignoreStartupCspViolations_ = true;
};


/**
 * Toggles recording of Content Security Policy violations. Call this with false
 * during tests, setUpPage, setUp, and tearDown functions to prevent CSP
 * violations occurring while the function is executing from being reported as
 * errors. Reporting will be reset upon execution of the next test function.
 *
 * @param {boolean} enable
 */
goog.testing.TestCase.prototype.observeCspViolations = function(enable) {
  this.cspViolationObserver_.setEnabled(enable);
};


/**
 * Returns a string representing the error object.
 * @return {string} A string representation of the error.
 * @override
 */
goog.testing.TestCase.Error.prototype.toString = function() {
  'use strict';
  return 'ERROR in ' + this.source + '\n' + this.message +
      (this.stack && this.stack !== 'Not available' ? '\n' + this.stack : '');
};

/**
 * Returns an object representing the error suitable for JSON serialization.
 * @return {!goog.testing.TestCase.IResult} An object
 *     representation of the error.
 * @private
 */
goog.testing.TestCase.Error.prototype.toObject_ = function() {
  'use strict';
  return {
    'source': this.source,
    'message': this.message,
    'stacktrace': this.stack || ''
  };
};



/**
 * @constructor
 * @param {function(): (?goog.testing.TestCase.Continuation_|undefined)} fn
 * @private
 */
goog.testing.TestCase.Continuation_ = function(fn) {
  'use strict';
  /** @private @const */
  this.fn_ = fn;
};


/** @param {?goog.testing.TestCase.Continuation_|undefined} continuation */
goog.testing.TestCase.Continuation_.run = function(continuation) {
  'use strict';
  var fn = continuation && continuation.fn_;
  while (fn) {
    continuation = fn();
    fn = continuation && continuation.fn_;
  }
};
