| // Copyright 2009 The Closure Library Authors. All Rights Reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS-IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| /** |
| * @fileoverview Defines test classes for tests that can wait for conditions. |
| * |
| * Normal unit tests must complete their test logic within a single function |
| * execution. This is ideal for most tests, but makes it difficult to test |
| * routines that require real time to complete. The tests and TestCase in this |
| * file allow for tests that can wait until a condition is true before |
| * continuing execution. |
| * |
| * Each test has the typical three phases of execution: setUp, the test itself, |
| * and tearDown. During each phase, the test function may add wait conditions, |
| * which result in new test steps being added for that phase. All steps in a |
| * given phase must complete before moving on to the next phase. An error in |
| * any phase will stop that test and report the error to the test runner. |
| * |
| * This class should not be used where adequate mocks exist. Time-based routines |
| * should use the MockClock, which runs much faster and provides equivalent |
| * results. Continuation tests should be used for testing code that depends on |
| * browser behaviors that are difficult to mock. For example, testing code that |
| * relies on Iframe load events, event or layout code that requires a setTimeout |
| * to become valid, and other browser-dependent native object interactions for |
| * which mocks are insufficient. |
| * |
| * Sample usage: |
| * |
| * <pre> |
| * var testCase = new goog.testing.ContinuationTestCase(); |
| * testCase.autoDiscoverTests(); |
| * |
| * if (typeof G_testRunner != 'undefined') { |
| * G_testRunner.initialize(testCase); |
| * } |
| * |
| * function testWaiting() { |
| * var someVar = true; |
| * waitForTimeout(function() { |
| * assertTrue(someVar) |
| * }, 500); |
| * } |
| * |
| * function testWaitForEvent() { |
| * var et = goog.events.EventTarget(); |
| * waitForEvent(et, 'test', function() { |
| * // Test step runs after the event fires. |
| * }) |
| * et.dispatchEvent(et, 'test'); |
| * } |
| * |
| * function testWaitForCondition() { |
| * var counter = 0; |
| * |
| * waitForCondition(function() { |
| * // This function is evaluated periodically until it returns true, or it |
| * // times out. |
| * return ++counter >= 3; |
| * }, function() { |
| * // This test step is run once the condition becomes true. |
| * assertEquals(3, counter); |
| * }); |
| * } |
| * </pre> |
| * |
| * @author brenneman@google.com (Shawn Brenneman) |
| */ |
| |
| |
| goog.provide('goog.testing.ContinuationTestCase'); |
| goog.provide('goog.testing.ContinuationTestCase.Step'); |
| goog.provide('goog.testing.ContinuationTestCase.Test'); |
| |
| goog.require('goog.array'); |
| goog.require('goog.events.EventHandler'); |
| goog.require('goog.testing.TestCase'); |
| goog.require('goog.testing.asserts'); |
| |
| |
| |
| /** |
| * Constructs a test case that supports tests with continuations. Test functions |
| * may issue "wait" commands that suspend the test temporarily and continue once |
| * the wait condition is met. |
| * |
| * @param {string=} opt_name Optional name for the test case. |
| * @constructor |
| * @extends {goog.testing.TestCase} |
| * @final |
| */ |
| goog.testing.ContinuationTestCase = function(opt_name) { |
| goog.testing.TestCase.call(this, opt_name); |
| |
| /** |
| * An event handler for waiting on Closure or browser events during tests. |
| * @type {goog.events.EventHandler<!goog.testing.ContinuationTestCase>} |
| * @private |
| */ |
| this.handler_ = new goog.events.EventHandler(this); |
| }; |
| goog.inherits(goog.testing.ContinuationTestCase, goog.testing.TestCase); |
| |
| |
| /** |
| * The default maximum time to wait for a single test step in milliseconds. |
| * @type {number} |
| */ |
| goog.testing.ContinuationTestCase.MAX_TIMEOUT = 1000; |
| |
| |
| /** |
| * Lock used to prevent multiple test steps from running recursively. |
| * @type {boolean} |
| * @private |
| */ |
| goog.testing.ContinuationTestCase.locked_ = false; |
| |
| |
| /** |
| * The current test being run. |
| * @type {goog.testing.ContinuationTestCase.Test} |
| * @private |
| */ |
| goog.testing.ContinuationTestCase.prototype.currentTest_ = null; |
| |
| |
| /** |
| * Enables or disables the wait functions in the global scope. |
| * @param {boolean} enable Whether the wait functions should be exported. |
| * @private |
| */ |
| goog.testing.ContinuationTestCase.prototype.enableWaitFunctions_ = |
| function(enable) { |
| if (enable) { |
| goog.exportSymbol('waitForCondition', |
| goog.bind(this.waitForCondition, this)); |
| goog.exportSymbol('waitForEvent', goog.bind(this.waitForEvent, this)); |
| goog.exportSymbol('waitForTimeout', goog.bind(this.waitForTimeout, this)); |
| } else { |
| // Internet Explorer doesn't allow deletion of properties on the window. |
| goog.global['waitForCondition'] = undefined; |
| goog.global['waitForEvent'] = undefined; |
| goog.global['waitForTimeout'] = undefined; |
| } |
| }; |
| |
| |
| /** @override */ |
| goog.testing.ContinuationTestCase.prototype.runTests = function() { |
| this.enableWaitFunctions_(true); |
| goog.testing.ContinuationTestCase.superClass_.runTests.call(this); |
| }; |
| |
| |
| /** @override */ |
| goog.testing.ContinuationTestCase.prototype.finalize = function() { |
| this.enableWaitFunctions_(false); |
| goog.testing.ContinuationTestCase.superClass_.finalize.call(this); |
| }; |
| |
| |
| /** @override */ |
| goog.testing.ContinuationTestCase.prototype.cycleTests = function() { |
| // Get the next test in the queue. |
| if (!this.currentTest_) { |
| this.currentTest_ = this.createNextTest_(); |
| } |
| |
| // Run the next step of the current test, or exit if all tests are complete. |
| if (this.currentTest_) { |
| this.runNextStep_(); |
| } else { |
| this.finalize(); |
| } |
| }; |
| |
| |
| /** |
| * Creates the next test in the queue. |
| * @return {goog.testing.ContinuationTestCase.Test} The next test to execute, or |
| * null if no pending tests remain. |
| * @private |
| */ |
| goog.testing.ContinuationTestCase.prototype.createNextTest_ = function() { |
| var test = this.next(); |
| if (!test) { |
| return null; |
| } |
| |
| |
| var name = test.name; |
| goog.testing.TestCase.currentTestName = name; |
| this.result_.runCount++; |
| this.log('Running test: ' + name); |
| |
| return new goog.testing.ContinuationTestCase.Test( |
| new goog.testing.TestCase.Test(name, this.setUp, this), |
| test, |
| new goog.testing.TestCase.Test(name, this.tearDown, this)); |
| }; |
| |
| |
| /** |
| * Cleans up a finished test and cycles to the next test. |
| * @private |
| */ |
| goog.testing.ContinuationTestCase.prototype.finishTest_ = function() { |
| var err = this.currentTest_.getError(); |
| if (err) { |
| this.doError(this.currentTest_, err); |
| } else { |
| this.doSuccess(this.currentTest_); |
| } |
| |
| goog.testing.TestCase.currentTestName = null; |
| this.currentTest_ = null; |
| this.locked_ = false; |
| this.handler_.removeAll(); |
| |
| this.timeout(goog.bind(this.cycleTests, this), 0); |
| }; |
| |
| |
| /** |
| * Executes the next step in the current phase, advancing through each phase as |
| * all steps are completed. |
| * @private |
| */ |
| goog.testing.ContinuationTestCase.prototype.runNextStep_ = function() { |
| if (this.locked_) { |
| // Attempting to run a step before the previous step has finished. Try again |
| // after that step has released the lock. |
| return; |
| } |
| |
| var phase = this.currentTest_.getCurrentPhase(); |
| |
| if (!phase || !phase.length) { |
| // No more steps for this test. |
| this.finishTest_(); |
| return; |
| } |
| |
| // Find the next step that is not in a wait state. |
| var stepIndex = goog.array.findIndex(phase, function(step) { |
| return !step.waiting; |
| }); |
| |
| if (stepIndex < 0) { |
| // All active steps are currently waiting. Return until one wakes up. |
| return; |
| } |
| |
| this.locked_ = true; |
| var step = phase[stepIndex]; |
| |
| try { |
| step.execute(); |
| // Remove the successfully completed step. If an error is thrown, all steps |
| // will be removed for this phase. |
| goog.array.removeAt(phase, stepIndex); |
| |
| } catch (e) { |
| this.currentTest_.setError(e); |
| |
| // An assertion has failed, or an exception was raised. Clear the current |
| // phase, whether it is setUp, test, or tearDown. |
| this.currentTest_.cancelCurrentPhase(); |
| |
| // Cancel the setUp and test phase no matter where the error occurred. The |
| // tearDown phase will still run if it has pending steps. |
| this.currentTest_.cancelTestPhase(); |
| } |
| |
| this.locked_ = false; |
| this.runNextStep_(); |
| }; |
| |
| |
| /** |
| * Creates a new test step that will run after a user-specified |
| * timeout. No guarantee is made on the execution order of the |
| * continuation, except for those provided by each browser's |
| * window.setTimeout. In particular, if two continuations are |
| * registered at the same time with very small delta for their |
| * durations, this class can not guarantee that the continuation with |
| * the smaller duration will be executed first. |
| * @param {Function} continuation The test function to invoke after the timeout. |
| * @param {number=} opt_duration The length of the timeout in milliseconds. |
| */ |
| goog.testing.ContinuationTestCase.prototype.waitForTimeout = |
| function(continuation, opt_duration) { |
| var step = this.addStep_(continuation); |
| step.setTimeout(goog.bind(this.handleComplete_, this, step), |
| opt_duration || 0); |
| }; |
| |
| |
| /** |
| * Creates a new test step that will run after an event has fired. If the event |
| * does not fire within a reasonable timeout, the test will fail. |
| * @param {goog.events.EventTarget|EventTarget} eventTarget The target that will |
| * fire the event. |
| * @param {string} eventType The type of event to listen for. |
| * @param {Function} continuation The test function to invoke after the event |
| * fires. |
| */ |
| goog.testing.ContinuationTestCase.prototype.waitForEvent = function( |
| eventTarget, |
| eventType, |
| continuation) { |
| |
| var step = this.addStep_(continuation); |
| |
| var duration = goog.testing.ContinuationTestCase.MAX_TIMEOUT; |
| step.setTimeout(goog.bind(this.handleTimeout_, this, step, duration), |
| duration); |
| |
| this.handler_.listenOnce(eventTarget, |
| eventType, |
| goog.bind(this.handleComplete_, this, step)); |
| }; |
| |
| |
| /** |
| * Creates a new test step which will run once a condition becomes true. The |
| * condition will be polled at a user-specified interval until it becomes true, |
| * or until a maximum timeout is reached. |
| * @param {Function} condition The condition to poll. |
| * @param {Function} continuation The test code to evaluate once the condition |
| * becomes true. |
| * @param {number=} opt_interval The polling interval in milliseconds. |
| * @param {number=} opt_maxTimeout The maximum amount of time to wait for the |
| * condition in milliseconds (defaults to 1000). |
| */ |
| goog.testing.ContinuationTestCase.prototype.waitForCondition = function( |
| condition, |
| continuation, |
| opt_interval, |
| opt_maxTimeout) { |
| |
| var interval = opt_interval || 100; |
| var timeout = opt_maxTimeout || goog.testing.ContinuationTestCase.MAX_TIMEOUT; |
| |
| var step = this.addStep_(continuation); |
| this.testCondition_(step, condition, goog.now(), interval, timeout); |
| }; |
| |
| |
| /** |
| * Creates a new asynchronous test step which will be added to the current test |
| * phase. |
| * @param {Function} func The test function that will be executed for this step. |
| * @return {!goog.testing.ContinuationTestCase.Step} A new test step. |
| * @private |
| */ |
| goog.testing.ContinuationTestCase.prototype.addStep_ = function(func) { |
| if (!this.currentTest_) { |
| throw Error('Cannot add test steps outside of a running test.'); |
| } |
| |
| var step = new goog.testing.ContinuationTestCase.Step( |
| this.currentTest_.name, |
| func, |
| this.currentTest_.scope); |
| this.currentTest_.addStep(step); |
| return step; |
| }; |
| |
| |
| /** |
| * Handles completion of a step's wait condition. Advances the test, allowing |
| * the step's test method to run. |
| * @param {goog.testing.ContinuationTestCase.Step} step The step that has |
| * finished waiting. |
| * @private |
| */ |
| goog.testing.ContinuationTestCase.prototype.handleComplete_ = function(step) { |
| step.clearTimeout(); |
| step.waiting = false; |
| this.runNextStep_(); |
| }; |
| |
| |
| /** |
| * Handles the timeout event for a step that has exceeded the maximum time. This |
| * causes the current test to fail. |
| * @param {goog.testing.ContinuationTestCase.Step} step The timed-out step. |
| * @param {number} duration The length of the timeout in milliseconds. |
| * @private |
| */ |
| goog.testing.ContinuationTestCase.prototype.handleTimeout_ = |
| function(step, duration) { |
| step.ref = function() { |
| fail('Continuation timed out after ' + duration + 'ms.'); |
| }; |
| |
| // Since the test is failing, cancel any other pending event listeners. |
| this.handler_.removeAll(); |
| this.handleComplete_(step); |
| }; |
| |
| |
| /** |
| * Tests a wait condition and executes the associated test step once the |
| * condition is true. |
| * |
| * If the condition does not become true before the maximum duration, the |
| * interval will stop and the test step will fail in the kill timer. |
| * |
| * @param {goog.testing.ContinuationTestCase.Step} step The waiting test step. |
| * @param {Function} condition The test condition. |
| * @param {number} startTime Time when the test step began waiting. |
| * @param {number} interval The duration in milliseconds to wait between tests. |
| * @param {number} timeout The maximum amount of time to wait for the condition |
| * to become true. Measured from the startTime in milliseconds. |
| * @private |
| */ |
| goog.testing.ContinuationTestCase.prototype.testCondition_ = function( |
| step, |
| condition, |
| startTime, |
| interval, |
| timeout) { |
| |
| var duration = goog.now() - startTime; |
| |
| if (condition()) { |
| this.handleComplete_(step); |
| } else if (duration < timeout) { |
| step.setTimeout(goog.bind(this.testCondition_, |
| this, |
| step, |
| condition, |
| startTime, |
| interval, |
| timeout), |
| interval); |
| } else { |
| this.handleTimeout_(step, duration); |
| } |
| }; |
| |
| |
| |
| /** |
| * Creates a continuation test case, which consists of multiple test steps that |
| * occur in several phases. |
| * |
| * The steps are distributed between setUp, test, and tearDown phases. During |
| * the execution of each step, 0 or more steps may be added to the current |
| * phase. Once all steps in a phase have completed, the next phase will be |
| * executed. |
| * |
| * If any errors occur (such as an assertion failure), the setUp and Test phases |
| * will be cancelled immediately. The tearDown phase will always start, but may |
| * be cancelled as well if it raises an error. |
| * |
| * @param {goog.testing.TestCase.Test} setUp A setUp test method to run before |
| * the main test phase. |
| * @param {goog.testing.TestCase.Test} test A test method to run. |
| * @param {goog.testing.TestCase.Test} tearDown A tearDown test method to run |
| * after the test method completes or fails. |
| * @constructor |
| * @extends {goog.testing.TestCase.Test} |
| * @final |
| */ |
| goog.testing.ContinuationTestCase.Test = function(setUp, test, tearDown) { |
| // This test container has a name, but no evaluation function or scope. |
| goog.testing.TestCase.Test.call(this, test.name, null, null); |
| |
| /** |
| * The list of test steps to run during setUp. |
| * @type {Array<goog.testing.TestCase.Test>} |
| * @private |
| */ |
| this.setUp_ = [setUp]; |
| |
| /** |
| * The list of test steps to run for the actual test. |
| * @type {Array<goog.testing.TestCase.Test>} |
| * @private |
| */ |
| this.test_ = [test]; |
| |
| /** |
| * The list of test steps to run during the tearDown phase. |
| * @type {Array<goog.testing.TestCase.Test>} |
| * @private |
| */ |
| this.tearDown_ = [tearDown]; |
| }; |
| goog.inherits(goog.testing.ContinuationTestCase.Test, |
| goog.testing.TestCase.Test); |
| |
| |
| /** |
| * The first error encountered during the test run, if any. |
| * @type {Error} |
| * @private |
| */ |
| goog.testing.ContinuationTestCase.Test.prototype.error_ = null; |
| |
| |
| /** |
| * @return {Error} The first error to be raised during the test run or null if |
| * no errors occurred. |
| */ |
| goog.testing.ContinuationTestCase.Test.prototype.getError = function() { |
| return this.error_; |
| }; |
| |
| |
| /** |
| * Sets an error for the test so it can be reported. Only the first error set |
| * during a test will be reported. Additional errors that occur in later test |
| * phases will be discarded. |
| * @param {Error} e An error. |
| */ |
| goog.testing.ContinuationTestCase.Test.prototype.setError = function(e) { |
| this.error_ = this.error_ || e; |
| }; |
| |
| |
| /** |
| * @return {Array<goog.testing.TestCase.Test>} The current phase of steps |
| * being processed. Returns null if all steps have been completed. |
| */ |
| goog.testing.ContinuationTestCase.Test.prototype.getCurrentPhase = function() { |
| if (this.setUp_.length) { |
| return this.setUp_; |
| } |
| |
| if (this.test_.length) { |
| return this.test_; |
| } |
| |
| if (this.tearDown_.length) { |
| return this.tearDown_; |
| } |
| |
| return null; |
| }; |
| |
| |
| /** |
| * Adds a new test step to the end of the current phase. The new step will wait |
| * for a condition to be met before running, or will fail after a timeout. |
| * @param {goog.testing.ContinuationTestCase.Step} step The test step to add. |
| */ |
| goog.testing.ContinuationTestCase.Test.prototype.addStep = function(step) { |
| var phase = this.getCurrentPhase(); |
| if (phase) { |
| phase.push(step); |
| } else { |
| throw Error('Attempted to add a step to a completed test.'); |
| } |
| }; |
| |
| |
| /** |
| * Cancels all remaining steps in the current phase. Called after an error in |
| * any phase occurs. |
| */ |
| goog.testing.ContinuationTestCase.Test.prototype.cancelCurrentPhase = |
| function() { |
| this.cancelPhase_(this.getCurrentPhase()); |
| }; |
| |
| |
| /** |
| * Skips the rest of the setUp and test phases, but leaves the tearDown phase to |
| * clean up. |
| */ |
| goog.testing.ContinuationTestCase.Test.prototype.cancelTestPhase = function() { |
| this.cancelPhase_(this.setUp_); |
| this.cancelPhase_(this.test_); |
| }; |
| |
| |
| /** |
| * Clears a test phase and cancels any pending steps found. |
| * @param {Array<goog.testing.TestCase.Test>} phase A list of test steps. |
| * @private |
| */ |
| goog.testing.ContinuationTestCase.Test.prototype.cancelPhase_ = |
| function(phase) { |
| while (phase && phase.length) { |
| var step = phase.pop(); |
| if (step instanceof goog.testing.ContinuationTestCase.Step) { |
| step.clearTimeout(); |
| } |
| } |
| }; |
| |
| |
| |
| /** |
| * Constructs a single step in a larger continuation test. Each step is similar |
| * to a typical TestCase test, except it may wait for an event or timeout to |
| * occur before running the test function. |
| * |
| * @param {string} name The test name. |
| * @param {Function} ref The test function to run. |
| * @param {Object=} opt_scope The object context to run the test in. |
| * @constructor |
| * @extends {goog.testing.TestCase.Test} |
| * @final |
| */ |
| goog.testing.ContinuationTestCase.Step = function(name, ref, opt_scope) { |
| goog.testing.TestCase.Test.call(this, name, ref, opt_scope); |
| }; |
| goog.inherits(goog.testing.ContinuationTestCase.Step, |
| goog.testing.TestCase.Test); |
| |
| |
| /** |
| * Whether the step is currently waiting for a condition to continue. All new |
| * steps begin in wait state. |
| * @type {boolean} |
| */ |
| goog.testing.ContinuationTestCase.Step.prototype.waiting = true; |
| |
| |
| /** |
| * A saved reference to window.clearTimeout so that MockClock or other overrides |
| * don't affect continuation timeouts. |
| * @type {Function} |
| * @private |
| */ |
| goog.testing.ContinuationTestCase.Step.protectedClearTimeout_ = |
| window.clearTimeout; |
| |
| |
| /** |
| * A saved reference to window.setTimeout so that MockClock or other overrides |
| * don't affect continuation timeouts. |
| * @type {Function} |
| * @private |
| */ |
| goog.testing.ContinuationTestCase.Step.protectedSetTimeout_ = window.setTimeout; |
| |
| |
| /** |
| * Key to this step's timeout. If the step is waiting for an event, the timeout |
| * will be used as a kill timer. If the step is waiting |
| * @type {number} |
| * @private |
| */ |
| goog.testing.ContinuationTestCase.Step.prototype.timeout_; |
| |
| |
| /** |
| * Starts a timeout for this step. Each step may have only one timeout active at |
| * a time. |
| * @param {Function} func The function to call after the timeout. |
| * @param {number} duration The number of milliseconds to wait before invoking |
| * the function. |
| */ |
| goog.testing.ContinuationTestCase.Step.prototype.setTimeout = |
| function(func, duration) { |
| |
| this.clearTimeout(); |
| |
| var setTimeout = goog.testing.ContinuationTestCase.Step.protectedSetTimeout_; |
| this.timeout_ = setTimeout(func, duration); |
| }; |
| |
| |
| /** |
| * Clears the current timeout if it is active. |
| */ |
| goog.testing.ContinuationTestCase.Step.prototype.clearTimeout = function() { |
| if (this.timeout_) { |
| var clear = goog.testing.ContinuationTestCase.Step.protectedClearTimeout_; |
| |
| clear(this.timeout_); |
| delete this.timeout_; |
| } |
| }; |