text: // Copyright 2010 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.
  // All Rights Reserved.
  
  /**
   * @fileoverview A class representing a set of test functions that use
   * asynchronous functions that cannot be meaningfully mocked.
   *
   * To create a Google-compatible JsUnit test using this test case, put the
   * following snippet in your test:
   *
   *   var asyncTestCase = goog.testing.AsyncTestCase.createAndInstall();
   *
   * To make the test runner wait for your asynchronous behaviour, use:
   *
   *   asyncTestCase.waitForAsync('Waiting for xhr to respond');
   *
   * The next test will not start until the following call is made, or a
   * timeout occurs:
   *
   *   asyncTestCase.continueTesting();
   *
   * There does NOT need to be a 1:1 mapping of waitForAsync calls and
   * continueTesting calls. The next test will be run after a single call to
   * continueTesting is made, as long as there is no subsequent call to
   * waitForAsync in the same thread.
   *
   * Example:
   *   // Returning here would cause the next test to be run.
   *   asyncTestCase.waitForAsync('description 1');
   *   // Returning here would *not* cause the next test to be run.
   *   // Only effect of additional waitForAsync() calls is an updated
   *   // description in the case of a timeout.
   *   asyncTestCase.waitForAsync('updated description');
   *   asyncTestCase.continueTesting();
   *   // Returning here would cause the next test to be run.
   *   asyncTestCase.waitForAsync('just kidding, still running.');
   *   // Returning here would *not* cause the next test to be run.
   *
   * The test runner can also be made to wait for more than one asynchronous
   * event with:
   *
   *   asyncTestCase.waitForSignals(n);
   *
   * The next test will not start until asyncTestCase.signal() is called n times,
   * or the test step timeout is exceeded.
   *
   * This class supports asynchronous behaviour in all test functions except for
   * tearDownPage. If such support is needed, it can be added.
   *
   * Example Usage:
   *
   *   var asyncTestCase = goog.testing.AsyncTestCase.createAndInstall();
   *   // Optionally, set a longer-than-normal step timeout.
   *   asyncTestCase.stepTimeout = 30 * 1000;
   *
   *   function testSetTimeout() {
   *     var step = 0;
   *     function stepCallback() {
   *       step++;
   *       switch (step) {
   *         case 1:
   *           var startTime = goog.now();
   *           asyncTestCase.waitForAsync('step 1');
   *           window.setTimeout(stepCallback, 100);
   *           break;
   *         case 2:
   *           assertTrue('Timeout fired too soon',
   *               goog.now() - startTime >= 100);
   *           asyncTestCase.waitForAsync('step 2');
   *           window.setTimeout(stepCallback, 100);
   *           break;
   *         case 3:
   *           assertTrue('Timeout fired too soon',
   *               goog.now() - startTime >= 200);
   *           asyncTestCase.continueTesting();
   *           break;
   *         default:
   *           fail('Unexpected call to stepCallback');
   *       }
   *     }
   *     stepCallback();
   *   }
   *
   * Known Issues:
   *   IE7 Exceptions:
   *     As the failingtest.html will show, it appears as though ie7 does not
   *     propagate an exception past a function called using the func.call()
   *     syntax. This causes case 3 of the failing tests (exceptions) to show up
   *     as timeouts in IE.
   *   window.onerror:
   *     This seems to catch errors only in ff2/ff3. It does not work in Safari or
   *     IE7. The consequence of this is that exceptions that would have been
   *     caught by window.onerror show up as timeouts.
   *
   * @author agrieve@google.com (Andrew Grieve)
   */
  
  goog.setTestOnly('goog.testing.AsyncTestCase');
  goog.provide('goog.testing.AsyncTestCase');
  goog.provide('goog.testing.AsyncTestCase.ControlBreakingException');
  
  goog.require('goog.testing.TestCase');
  goog.require('goog.testing.asserts');
  
  
  
  /**
   * A test case that is capable of running tests that contain asynchronous logic.
   * @param {string=} opt_name A descriptive name for the test case.
   * @extends {goog.testing.TestCase}
   * @constructor
   * @deprecated Use goog.testing.TestCase instead. goog.testing.TestCase now
   *    supports async testing using promises.
   */
  goog.testing.AsyncTestCase = function(opt_name) {
    goog.testing.TestCase.call(this, opt_name);
  };
  goog.inherits(goog.testing.AsyncTestCase, goog.testing.TestCase);
  
  
  /**
   * Represents result of top stack function call.
   * @typedef {{controlBreakingExceptionThrown: boolean, message: string}}
   * @private
   */
  goog.testing.AsyncTestCase.TopStackFuncResult_;
  
  
  
  /**
   * An exception class used solely for control flow.
   * @param {string=} opt_message Error message.
   * @constructor
   * @extends {Error}
   * @final
   */
  goog.testing.AsyncTestCase.ControlBreakingException = function(opt_message) {
    goog.testing.AsyncTestCase.ControlBreakingException.base(
        this, 'constructor', opt_message);
  
    /**
     * The exception message.
     * @type {string}
     */
    this.message = opt_message || '';
  };
  goog.inherits(goog.testing.AsyncTestCase.ControlBreakingException, Error);
  
  
  /**
   * Return value for .toString().
   * @type {string}
   */
  goog.testing.AsyncTestCase.ControlBreakingException.TO_STRING =
      '[AsyncTestCase.ControlBreakingException]';
  
  
  /**
   * Marks this object as a ControlBreakingException
   * @type {boolean}
   */
  goog.testing.AsyncTestCase.ControlBreakingException.prototype
      .isControlBreakingException = true;
  
  
  /** @override */
  goog.testing.AsyncTestCase.ControlBreakingException.prototype.toString =
      function() {
    // This shows up in the console when the exception is not caught.
    return goog.testing.AsyncTestCase.ControlBreakingException.TO_STRING;
  };
  
  
  /**
   * How long to wait for a single step of a test to complete in milliseconds.
   * A step starts when a call to waitForAsync() is made.
   * @type {number}
   */
  goog.testing.AsyncTestCase.prototype.stepTimeout = 1000;
  
  
  /**
   * How long to wait after a failed test before moving onto the next one.
   * The purpose of this is to allow any pending async callbacks from the failing
   * test to finish up and not cause the next test to fail.
   * @type {number}
   */
  goog.testing.AsyncTestCase.prototype.timeToSleepAfterFailure = 500;
  
  
  /**
   * Turn on extra logging to help debug failing async. tests.
   * @type {boolean}
   * @private
   */
  goog.testing.AsyncTestCase.prototype.enableDebugLogs_ = false;
  
  
  /**
   * A reference to the original asserts.js assert_() function.
   * @private
   */
  goog.testing.AsyncTestCase.prototype.origAssert_;
  
  
  /**
   * A reference to the original asserts.js fail() function.
   * @private
   */
  goog.testing.AsyncTestCase.prototype.origFail_;
  
  
  /**
   * A reference to the original window.onerror function.
   * @type {Function|undefined}
   * @private
   */
  goog.testing.AsyncTestCase.prototype.origOnError_;
  
  
  /**
   * The stage of the test we are currently on.
   * @type {Function|undefined}}
   * @private
   */
  goog.testing.AsyncTestCase.prototype.curStepFunc_;
  
  
  /**
   * The name of the stage of the test we are currently on.
   * @type {string}
   * @private
   */
  goog.testing.AsyncTestCase.prototype.curStepName_ = '';
  
  
  /**
   * The stage of the test we should run next.
   * @type {Function|undefined}
   * @private
   */
  goog.testing.AsyncTestCase.prototype.nextStepFunc_;
  
  
  /**
   * The name of the stage of the test we should run next.
   * @type {string}
   * @private
   */
  goog.testing.AsyncTestCase.prototype.nextStepName_ = '';
  
  
  /**
   * The handle to the current setTimeout timer.
   * @type {number}
   * @private
   */
  goog.testing.AsyncTestCase.prototype.timeoutHandle_ = 0;
  
  
  /**
   * Marks if the cleanUp() function has been called for the currently running
   * test.
   * @type {boolean}
   * @private
   */
  goog.testing.AsyncTestCase.prototype.cleanedUp_ = false;
  
  
  /**
   * The currently active test.
   * @type {goog.testing.TestCase.Test|undefined}
   * @protected
   */
  goog.testing.AsyncTestCase.prototype.activeTest;
  
  
  /**
   * A flag to prevent recursive exception handling.
   * @type {boolean}
   * @private
   */
  goog.testing.AsyncTestCase.prototype.inException_ = false;
  
  
  /**
   * Flag used to determine if we can move to the next step in the testing loop.
   * @type {boolean}
   * @private
   */
  goog.testing.AsyncTestCase.prototype.isReady_ = true;
  
  
  /**
   * Number of signals to wait for before continuing testing when waitForSignals
   * is used.
   * @type {number}
   * @private
   */
  goog.testing.AsyncTestCase.prototype.expectedSignalCount_ = 0;
  
  
  /**
   * Number of signals received.
   * @type {number}
   * @private
   */
  goog.testing.AsyncTestCase.prototype.receivedSignalCount_ = 0;
  
  
  /**
   * Flag that tells us if there is a function in the call stack that will make
   * a call to pump_().
   * @type {boolean}
   * @private
   */
  goog.testing.AsyncTestCase.prototype.returnWillPump_ = false;
  
  
  /**
   * The number of times we have thrown a ControlBreakingException so that we
   * know not to complain in our window.onerror handler. In Webkit, window.onerror
   * is not supported, and so this counter will keep going up but we won't care
   * about it.
   * @type {number}
   * @private
   */
  goog.testing.AsyncTestCase.prototype.numControlExceptionsExpected_ = 0;
  
  
  /**
   * The current step name.
   * @return {string} Step name.
   * @protected
   */
  goog.testing.AsyncTestCase.prototype.getCurrentStepName = function() {
    return this.curStepName_;
  };
  
  
  /**
   * Preferred way of creating an AsyncTestCase. Creates one and initializes it
   * with the G_testRunner.
   * @param {string=} opt_name A descriptive name for the test case.
   * @return {!goog.testing.AsyncTestCase} The created AsyncTestCase.
   */
  goog.testing.AsyncTestCase.createAndInstall = function(opt_name) {
    var asyncTestCase = new goog.testing.AsyncTestCase(opt_name);
    goog.testing.TestCase.initializeTestRunner(asyncTestCase);
    return asyncTestCase;
  };
  
  
  /**
   * Informs the testcase not to continue to the next step in the test cycle
   * until continueTesting is called.
   * @param {string=} opt_name A description of what we are waiting for.
   */
  goog.testing.AsyncTestCase.prototype.waitForAsync = function(opt_name) {
    this.isReady_ = false;
    this.curStepName_ = opt_name || this.curStepName_;
  
    // Reset the timer that tracks if the async test takes too long.
    this.stopTimeoutTimer_();
    this.startTimeoutTimer_();
  };
  
  
  /**
   * Continue with the next step in the test cycle.
   */
  goog.testing.AsyncTestCase.prototype.continueTesting = function() {
    if (this.receivedSignalCount_ < this.expectedSignalCount_) {
      var remaining = this.expectedSignalCount_ - this.receivedSignalCount_;
      throw Error('Still waiting for ' + remaining + ' signals.');
    }
    this.endCurrentStep_();
  };
  
  
  /**
   * Ends the current test step and queues the next test step to run.
   * @private
   */
  goog.testing.AsyncTestCase.prototype.endCurrentStep_ = function() {
    if (!this.isReady_) {
      // We are a potential entry point, so we pump.
      this.isReady_ = true;
      this.stopTimeoutTimer_();
      // Run this in a setTimeout so that the caller has a chance to call
      // waitForAsync() again before we continue.
      this.timeout(goog.bind(this.pump_, this, null), 0);
    }
  };
  
  
  /**
   * Informs the testcase not to continue to the next step in the test cycle
   * until signal is called the specified number of times. Within a test, this
   * function behaves additively if called multiple times; the number of signals
   * to wait for will be the sum of all expected number of signals this function
   * was called with.
   * @param {number} times The number of signals to receive before
   *    continuing testing.
   * @param {string=} opt_name A description of what we are waiting for.
   */
  goog.testing.AsyncTestCase.prototype.waitForSignals = function(
      times, opt_name) {
    this.expectedSignalCount_ += times;
    if (this.receivedSignalCount_ < this.expectedSignalCount_) {
      this.waitForAsync(opt_name);
    }
  };
  
  
  /**
   * Signals once to continue with the test. If this is the last signal that the
   * test was waiting on, call continueTesting.
   */
  goog.testing.AsyncTestCase.prototype.signal = function() {
    if (++this.receivedSignalCount_ === this.expectedSignalCount_ &&
        this.expectedSignalCount_ > 0) {
      this.endCurrentStep_();
    }
  };
  
  
  /**
   * Handles an exception thrown by a test.
   * @param {*=} opt_e The exception object associated with the failure
   *     or a string.
   * @throws Always throws a ControlBreakingException.
   */
  goog.testing.AsyncTestCase.prototype.doAsyncError = function(opt_e) {
    // If we've caught an exception that we threw, then just pass it along. This
    // can happen if doAsyncError() was called from a call to assert and then
    // again by pump_().
    if (opt_e && opt_e.isControlBreakingException) {
      throw opt_e;
    }
  
    // Prevent another timeout error from triggering for this test step.
    this.stopTimeoutTimer_();
  
    // doError() uses test.name. Here, we create a dummy test and give it a more
    // helpful name based on the step we're currently on.
    var fakeTestObj =
        new goog.testing.TestCase.Test(this.curStepName_, goog.nullFunction);
    if (this.activeTest) {
      fakeTestObj.name = this.activeTest.name + ' [' + fakeTestObj.name + ']';
    }
  
    if (this.activeTest) {
      // Note: if the test has an error, and then tearDown has an error, they will
      // both be reported.
      this.doError(fakeTestObj, opt_e);
    } else {
      this.exceptionBeforeTest = opt_e;
    }
  
    // This is a potential entry point, so we pump. We also add in a bit of a
    // delay to try and prevent any async behavior from the failed test from
    // causing the next test to fail.
    this.timeout(
        goog.bind(this.pump_, this, this.doAsyncErrorTearDown_),
        this.timeToSleepAfterFailure);
  
    // We just caught an exception, so we do not want the code above us on the
    // stack to continue executing. If pump_ is in our call-stack, then it will
    // batch together multiple errors, so we only increment the count if pump_ is
    // not in the stack and let pump_ increment the count when it batches them.
    if (!this.returnWillPump_) {
      this.numControlExceptionsExpected_ += 1;
      this.dbgLog_(
          'doAsynError: numControlExceptionsExpected_ = ' +
          this.numControlExceptionsExpected_ + ' and throwing exception.');
    }
  
    // Copy the error message to ControlBreakingException.
    var message = '';
    if (typeof opt_e == 'string') {
      message = opt_e;
    } else if (opt_e && opt_e.message) {
      message = opt_e.message;
    }
    throw new goog.testing.AsyncTestCase.ControlBreakingException(message);
  };
  
  
  /**
   * Sets up the test page and then waits until the test case has been marked
   * as ready before executing the tests.
   * @override
   */
  goog.testing.AsyncTestCase.prototype.runTests = function() {
    this.hookAssert_();
    this.hookOnError_();
  
    goog.testing.TestCase.currentTestName = null;
    this.setNextStep_(this.doSetUpPage_, 'setUpPage');
    // We are an entry point, so we pump.
    this.pump_();
  };
  
  
  /**
   * Starts the tests.
   * @override
   */
  goog.testing.AsyncTestCase.prototype.cycleTests = function() {
    // We are an entry point, so we pump.
    this.saveMessage('Start');
    this.setNextStep_(this.doIteration_, 'doIteration');
    this.pump_();
  };
  
  
  /**
   * Finalizes the test case, called when the tests have finished executing.
   * @override
   */
  goog.testing.AsyncTestCase.prototype.finalize = function() {
    this.unhookAll_();
    this.setNextStep_(null, 'finalized');
    goog.testing.AsyncTestCase.superClass_.finalize.call(this);
  };
  
  
  /**
   * Enables verbose logging of what is happening inside of the AsyncTestCase.
   */
  goog.testing.AsyncTestCase.prototype.enableDebugLogging = function() {
    this.enableDebugLogs_ = true;
  };
  
  
  /**
   * Logs the given debug message to the console (when enabled).
   * @param {string} message The message to log.
   * @private
   */
  goog.testing.AsyncTestCase.prototype.dbgLog_ = function(message) {
    if (this.enableDebugLogs_) {
      this.log('AsyncTestCase - ' + message);
    }
  };
  
  
  /**
   * Wraps doAsyncError() for when we are sure that the test runner has no user
   * code above it in the stack.
   * @param {string|Error=} opt_e The exception object associated with the
   *     failure or a string.
   * @private
   */
  goog.testing.AsyncTestCase.prototype.doTopOfStackAsyncError_ = function(opt_e) {
  
    try {
      this.doAsyncError(opt_e);
    } catch (e) {
      // We know that we are on the top of the stack, so there is no need to
      // throw this exception in this case.
      if (e.isControlBreakingException) {
        this.numControlExceptionsExpected_ -= 1;
        this.dbgLog_(
            'doTopOfStackAsyncError_: numControlExceptionsExpected_ = ' +
            this.numControlExceptionsExpected_ + ' and catching exception.');
      } else {
        throw e;
      }
    }
  };
  
  
  /**
   * Calls the tearDown function, catching any errors, and then moves on to
   * the next step in the testing cycle.
   * @private
   */
  goog.testing.AsyncTestCase.prototype.doAsyncErrorTearDown_ = function() {
    if (this.inException_) {
      // We get here if tearDown is throwing the error.
      // Upon calling continueTesting, the inline function 'doAsyncError' (set
      // below) is run.
      this.endCurrentStep_();
    } else {
      this.inException_ = true;
      this.isReady_ = true;
  
      // The continue point is different depending on if the error happened in
      // setUpPage() or in setUp()/test*()/tearDown().
      var stepFuncAfterError = this.nextStepFunc_;
      var stepNameAfterError = 'TestCase.execute (after error)';
      if (this.activeTest) {
        stepFuncAfterError = this.doIteration_;
        stepNameAfterError = 'doIteration (after error)';
      }
  
      // We must set the next step before calling tearDown.
      this.setNextStep_(function() {
        this.inException_ = false;
        // This is null when an error happens in setUpPage.
        this.setNextStep_(stepFuncAfterError, stepNameAfterError);
      }, 'doAsyncError');
  
      // Call the test's tearDown().
      if (!this.cleanedUp_) {
        this.cleanedUp_ = true;
        this.tearDown();
      }
    }
  };
  
  
  /**
   * Replaces the asserts.js assert_() and fail() functions with a wrappers to
   * catch the exceptions.
   * @private
   */
  goog.testing.AsyncTestCase.prototype.hookAssert_ = function() {
    if (!this.origAssert_) {
      this.origAssert_ = _assert;
      this.origFail_ = fail;
      var self = this;
      _assert = function() {
  
        try {
          self.origAssert_.apply(this, arguments);
        } catch (e) {
          self.dbgLog_('Wrapping failed assert()');
          self.doAsyncError(e);
        }
      };
      fail = function() {
  
        try {
          self.origFail_.apply(this, arguments);
        } catch (e) {
          self.dbgLog_('Wrapping fail()');
          self.doAsyncError(e);
        }
      };
    }
  };
  
  
  /**
   * Sets a window.onerror handler for catching exceptions that happen in async
   * callbacks. Note that as of Safari 3.1, Safari does not support this.
   * @private
   */
  goog.testing.AsyncTestCase.prototype.hookOnError_ = function() {
    if (!this.origOnError_) {
      this.origOnError_ = window.onerror;
      var self = this;
      window.onerror = function(error, url, line) {
        // Ignore exceptions that we threw on purpose.
        var cbe = goog.testing.AsyncTestCase.ControlBreakingException.TO_STRING;
        if (String(error).indexOf(cbe) != -1 &&
            self.numControlExceptionsExpected_) {
          self.numControlExceptionsExpected_ -= 1;
          self.dbgLog_(
              'window.onerror: numControlExceptionsExpected_ = ' +
              self.numControlExceptionsExpected_ + ' and ignoring exception. ' +
              error);
          // Tell the browser not to compain about the error.
          return true;
        } else {
          self.dbgLog_('window.onerror caught exception.');
          var message = error + '\nURL: ' + url + '\nLine: ' + line;
          self.doTopOfStackAsyncError_(message);
          // Tell the browser to complain about the error.
          return false;
        }
      };
    }
  };
  
  
  /**
   * Unhooks window.onerror and _assert.
   * @private
   */
  goog.testing.AsyncTestCase.prototype.unhookAll_ = function() {
    if (this.origOnError_) {
      window.onerror = this.origOnError_;
      this.origOnError_ = null;
      _assert = this.origAssert_;
      this.origAssert_ = null;
      fail = this.origFail_;
      this.origFail_ = null;
    }
  };
  
  
  /**
   * Enables the timeout timer. This timer fires unless continueTesting is
   * called.
   * @private
   */
  goog.testing.AsyncTestCase.prototype.startTimeoutTimer_ = function() {
    if (!this.timeoutHandle_ && this.stepTimeout > 0) {
      this.timeoutHandle_ = this.timeout(goog.bind(function() {
        this.dbgLog_('Timeout timer fired with id ' + this.timeoutHandle_);
        this.timeoutHandle_ = 0;
  
        this.doTopOfStackAsyncError_(
            'Timed out while waiting for ' +
            'continueTesting() to be called.');
      }, this, null), this.stepTimeout);
      this.dbgLog_('Started timeout timer with id ' + this.timeoutHandle_);
    }
  };
  
  
  /**
   * Disables the timeout timer.
   * @private
   */
  goog.testing.AsyncTestCase.prototype.stopTimeoutTimer_ = function() {
    if (this.timeoutHandle_) {
      this.dbgLog_('Clearing timeout timer with id ' + this.timeoutHandle_);
      this.clearTimeout(this.timeoutHandle_);
      this.timeoutHandle_ = 0;
    }
  };
  
  
  /**
   * Sets the next function to call in our sequence of async callbacks.
   * @param {Function} func The function that executes the next step.
   * @param {string} name A description of the next step.
   * @private
   */
  goog.testing.AsyncTestCase.prototype.setNextStep_ = function(func, name) {
    this.nextStepFunc_ = func && goog.bind(func, this);
    this.nextStepName_ = name;
  };
  
  
  /**
   * Calls the given function, redirecting any exceptions to doAsyncError.
   * @param {Function} func The function to call.
   * @return {!goog.testing.AsyncTestCase.TopStackFuncResult_} Returns a
   * TopStackFuncResult_.
   * @private
   */
  goog.testing.AsyncTestCase.prototype.callTopOfStackFunc_ = function(func) {
  
    try {
      func.call(this);
      return {controlBreakingExceptionThrown: false, message: ''};
    } catch (e) {
      this.dbgLog_('Caught exception in callTopOfStackFunc_');
  
      try {
        this.doAsyncError(e);
        return {controlBreakingExceptionThrown: false, message: ''};
      } catch (e2) {
        if (!e2.isControlBreakingException) {
          throw e2;
        }
        return {controlBreakingExceptionThrown: true, message: e2.message};
      }
    }
  };
  
  
  /**
   * Calls the next callback when the isReady_ flag is true.
   * @param {Function=} opt_doFirst A function to call before pumping.
   * @private
   * @throws Throws a ControlBreakingException if there were any failing steps.
   */
  goog.testing.AsyncTestCase.prototype.pump_ = function(opt_doFirst) {
    // If this function is already above us in the call-stack, then we should
    // return rather than pumping in order to minimize call-stack depth.
    if (!this.returnWillPump_) {
      this.setBatchTime(this.now());
      this.returnWillPump_ = true;
      var topFuncResult = {};
  
      if (opt_doFirst) {
        topFuncResult = this.callTopOfStackFunc_(opt_doFirst);
      }
      // Note: we don't check for this.running here because it is not set to true
      // while executing setUpPage and tearDownPage.
      // Also, if isReady_ is false, then one of two things will happen:
      // 1. Our timeout callback will be called.
      // 2. The tests will call continueTesting(), which will call pump_() again.
      while (this.isReady_ && this.nextStepFunc_ &&
             !topFuncResult.controlBreakingExceptionThrown) {
        this.curStepFunc_ = this.nextStepFunc_;
        this.curStepName_ = this.nextStepName_;
        this.nextStepFunc_ = null;
        this.nextStepName_ = '';
  
        this.dbgLog_('Performing step: ' + this.curStepName_);
        topFuncResult =
            this.callTopOfStackFunc_(/** @type {Function} */ (this.curStepFunc_));
  
        // If the max run time is exceeded call this function again async so as
        // not to block the browser.
        var delta = this.now() - this.getBatchTime();
        if (delta > goog.testing.TestCase.maxRunTime &&
            !topFuncResult.controlBreakingExceptionThrown) {
          this.saveMessage('Breaking async');
          var self = this;
          this.timeout(function() { self.pump_(); }, 100);
          break;
        }
      }
      this.returnWillPump_ = false;
    } else if (opt_doFirst) {
      opt_doFirst.call(this);
    }
  };
  
  
  /**
   * Sets up the test page and then waits until the test case has been marked
   * as ready before executing the tests.
   * @private
   */
  goog.testing.AsyncTestCase.prototype.doSetUpPage_ = function() {
    this.setNextStep_(this.execute, 'TestCase.execute');
    this.setUpPage();
  };
  
  
  /**
   * Step 1: Move to the next test.
   * @private
   */
  goog.testing.AsyncTestCase.prototype.doIteration_ = function() {
    this.expectedSignalCount_ = 0;
    this.receivedSignalCount_ = 0;
    this.activeTest = this.next();
    goog.testing.TestCase.currentTestName =
        this.activeTest ? this.activeTest.name : null;
    if (this.activeTest && this.running) {
      this.result_.runCount++;
      // If this test should be marked as having failed, doIteration will go
      // straight to the next test.
      if (this.maybeFailTestEarly(this.activeTest)) {
        this.setNextStep_(this.doIteration_, 'doIteration');
      } else {
        this.setNextStep_(this.doSetUp_, 'setUp');
      }
    } else {
      // All tests done.
      this.finalize();
    }
  };
  
  
  /**
   * Step 2: Call setUp().
   * @private
   */
  goog.testing.AsyncTestCase.prototype.doSetUp_ = function() {
    this.log('Running test: ' + this.activeTest.name);
    this.cleanedUp_ = false;
    this.setNextStep_(this.doExecute_, this.activeTest.name);
    this.setUp();
  };
  
  
  /**
   * Step 3: Call test.execute().
   * @private
   */
  goog.testing.AsyncTestCase.prototype.doExecute_ = function() {
    this.setNextStep_(this.doTearDown_, 'tearDown');
    this.activeTest.execute();
  };
  
  
  /**
   * Step 4: Call tearDown().
   * @private
   */
  goog.testing.AsyncTestCase.prototype.doTearDown_ = function() {
    this.cleanedUp_ = true;
    this.setNextStep_(this.doNext_, 'doNext');
    this.tearDown();
  };
  
  
  /**
   * Step 5: Call doSuccess()
   * @private
   */
  goog.testing.AsyncTestCase.prototype.doNext_ = function() {
    this.setNextStep_(this.doIteration_, 'doIteration');
    this.doSuccess(/** @type {goog.testing.TestCase.Test} */ (this.activeTest));
  };
  