// 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: * *
* 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); * }); * } ** * @author brenneman@google.com (Shawn Brenneman) */ goog.setTestOnly('goog.testing.ContinuationTestCase'); goog.provide('goog.testing.ContinuationTestCase'); goog.provide('goog.testing.ContinuationTestCase.ContinuationTest'); goog.provide('goog.testing.ContinuationTestCase.Step'); 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} * @deprecated ContinuationTestCase is deprecated. Prefer returning Promises * for tests that assert Asynchronous behavior. * @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} * @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.ContinuationTest} * @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.ContinuationTest} 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.ContinuationTest( 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.ContinuationTest = 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