123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684 |
- // 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.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<!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.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<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.ContinuationTest,
- goog.testing.TestCase.Test);
- /**
- * The first error encountered during the test run, if any.
- * @type {Error}
- * @private
- */
- goog.testing.ContinuationTestCase.ContinuationTest.prototype.error_ = null;
- /**
- * @return {Error} The first error to be raised during the test run or null if
- * no errors occurred.
- */
- goog.testing.ContinuationTestCase.ContinuationTest.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.ContinuationTest.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.ContinuationTest.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.ContinuationTest.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.ContinuationTest.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.ContinuationTest.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.ContinuationTest.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_;
- }
- };
|