// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The SFC licenses this file
// to you 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.

'use strict';

const assert = require('assert');
const fail = assert.fail;
const sinon = require('sinon');

const testutil = require('./testutil');
const {TimeoutError} = require('../../lib/error');
const promise = require('../../lib/promise');
const {enablePromiseManager} = require('../../lib/test/promise');

const NativePromise = Promise;

// Aliases for readability.
const StubError = testutil.StubError;
const assertIsStubError = testutil.assertIsStubError;
const callbackPair = testutil.callbackPair;
const throwStubError = testutil.throwStubError;

describe('promise control flow', function() {
  enablePromiseManager(() => {
    let flow, flowHistory, messages, uncaughtExceptions;

    beforeEach(function setUp() {
      promise.LONG_STACK_TRACES = false;
      flow = new promise.ControlFlow();
      promise.setDefaultFlow(flow);
      messages = [];
      flowHistory = [];

      uncaughtExceptions = [];
      flow.on(promise.ControlFlow.EventType.UNCAUGHT_EXCEPTION,
              onUncaughtException);
    });

    afterEach(function tearDown() {
      flow.removeAllListeners(
          promise.ControlFlow.EventType.UNCAUGHT_EXCEPTION);
      assert.deepEqual([], uncaughtExceptions,
          'There were uncaught exceptions');
      flow.reset();
      promise.LONG_STACK_TRACES = false;
    });

    function onUncaughtException(e) {
      uncaughtExceptions.push(e);
    }

    function defer() {
      let d = {};
      let promise = new Promise((resolve, reject) => {
        Object.assign(d, {resolve, reject});
      });
      d.promise = promise;
      return d;
    }

    function waitForAbort(opt_flow) {
      var theFlow = opt_flow || flow;
      theFlow.removeAllListeners(
          promise.ControlFlow.EventType.UNCAUGHT_EXCEPTION);
      return new NativePromise(function(fulfill, reject) {
        theFlow.once(promise.ControlFlow.EventType.IDLE, function() {
          reject(Error('expected flow to report an unhandled error'));
        });
        theFlow.once(
            promise.ControlFlow.EventType.UNCAUGHT_EXCEPTION,
            fulfill);
      });
    }

    function waitForIdle(opt_flow) {
      var theFlow = opt_flow || flow;
      return new NativePromise(function(fulfill, reject) {
        theFlow.once(promise.ControlFlow.EventType.IDLE, fulfill);
        theFlow.once(
            promise.ControlFlow.EventType.UNCAUGHT_EXCEPTION, reject);
      });
    }

    function timeout(ms) {
      return new NativePromise(function(fulfill) {
        setTimeout(fulfill, ms);
      });
    }


    function schedule(msg, opt_return) {
      return scheduleAction(msg, function() {
        return opt_return;
      });
    }

    /**
     * @param {string} value The value to push.
     * @param {promise.Promise=} opt_taskPromise Promise to return from
     *     the task.
     * @return {!promise.Promise} The result.
     */
    function schedulePush(value, opt_taskPromise) {
      return scheduleAction(value, function() {
        messages.push(value);
        return opt_taskPromise;
      });
    }

    /**
     * @param {string} msg Debug message.
     * @param {!Function} actionFn The function.
     * @return {!promise.Promise} The function result.
     */
    function scheduleAction(msg, actionFn) {
      return promise.controlFlow().execute(function() {
        flowHistory.push(msg);
        return actionFn();
      }, msg);
    }

    /**
     * @param {!Function} condition The condition function.
     * @param {number=} opt_timeout The timeout.
     * @param {string=} opt_message Optional message.
     * @return {!promise.Promise} The wait result.
     */
    function scheduleWait(condition, opt_timeout, opt_message) {
      var msg = opt_message || '';
      // It's not possible to hook into when the wait itself is scheduled, so
      // we record each iteration of the wait loop.
      var count = 0;
      return promise.controlFlow().wait(function() {
        flowHistory.push((count++) + ': ' + msg);
        return condition();
      }, opt_timeout, msg);
    }

    function asyncRun(fn, opt_self) {
      NativePromise.resolve().then(() => fn.call(opt_self));
    }

    function assertFlowHistory(var_args) {
      var expected = Array.prototype.slice.call(arguments, 0);
      assert.deepEqual(expected, flowHistory);
    }

    function assertMessages(var_args) {
      var expected = Array.prototype.slice.call(arguments, 0);
      assert.deepEqual(expected, messages);
    }

    function assertingMessages(var_args) {
      var args = Array.prototype.slice.call(arguments, 0);
      return () => assertMessages.apply(null, args);
    }

    function assertFlowIs(flow) {
      assert.equal(flow, promise.controlFlow());
    }

    describe('testScheduling', function() {
      it('aSimpleFunction', function() {
        schedule('go');
        return waitForIdle().then(function() {
          assertFlowHistory('go');
        });
      });

      it('aSimpleFunctionWithANonPromiseReturnValue', function() {
        schedule('go', 123).then(function(value) {
          assert.equal(123, value);
        });
        return waitForIdle().then(function() {
          assertFlowHistory('go');
        });
      });

      it('aSimpleSequence', function() {
        schedule('a');
        schedule('b');
        schedule('c');
        return waitForIdle().then(function() {
          assertFlowHistory('a', 'b', 'c');
        });
      });

      it('invokesCallbacksWhenTaskIsDone', function() {
        var d = new promise.Deferred();
        var called = false;
        var done = schedule('a', d.promise).then(function(value) {
          called = true;
          assert.equal(123, value);
        });
        return timeout(5).then(function() {
          assert.ok(!called);
          d.fulfill(123);
          return done;
        }).
        then(function() {
          assertFlowHistory('a');
        });
      });

      it('blocksUntilPromiseReturnedByTaskIsResolved', function() {
        var done = promise.defer();
        schedulePush('a', done.promise);
        schedulePush('b');
        setTimeout(function() {
          done.fulfill();
          messages.push('c');
        }, 25);
        return waitForIdle().then(assertingMessages('a', 'c', 'b'));
      });

      it('waitsForReturnedPromisesToResolve', function() {
        var d1 = new promise.Deferred();
        var d2 = new promise.Deferred();

        var callback = sinon.spy();
        schedule('a', d1.promise).then(callback);

        return timeout(5).then(function() {
          assert(!callback.called);
          d1.fulfill(d2.promise);
          return timeout(5);
        }).then(function() {
          assert(!callback.called);
          d2.fulfill('fluffy bunny');
          return waitForIdle();
        }).then(function() {
          assert(callback.called);
          assert.equal('fluffy bunny', callback.getCall(0).args[0]);
          assertFlowHistory('a');
        });
      });

      it('executesTasksInAFutureTurnAfterTheyAreScheduled', function() {
        var count = 0;
        function incr() { count++; }

        scheduleAction('', incr);
        assert.equal(0, count);
        return waitForIdle().then(function() {
          assert.equal(1, count);
        });
      });

      it('executesOneTaskPerTurnOfTheEventLoop', function() {
        var order = [];
        function go() {
          order.push(order.length / 2);
          asyncRun(function() {
            order.push('-');
          });
        }

        scheduleAction('', go);
        scheduleAction('', go);
        return waitForIdle().then(function() {
          assert.deepEqual([0, '-', 1, '-'], order);
        })
      });

      it('firstScheduledTaskIsWithinACallback', function() {
        promise.fulfilled().then(function() {
          schedule('a');
          schedule('b');
          schedule('c');
        }).then(function() {
          assertFlowHistory('a', 'b', 'c');
        });
        return waitForIdle();
      });

      it('newTasksAddedWhileWaitingOnTaskReturnedPromise1', function() {
        scheduleAction('a', function() {
          var d = promise.defer();
          setTimeout(function() {
            schedule('c');
            d.fulfill();
          }, 10);
          return d.promise;
        });
        schedule('b');
        return waitForIdle().then(function() {
          assertFlowHistory('a', 'b', 'c');
        });
      });

      it('newTasksAddedWhileWaitingOnTaskReturnedPromise2', function() {
        scheduleAction('a', function() {
          var d = promise.defer();
          setTimeout(function() {
            schedule('c');
            asyncRun(d.fulfill);
          }, 10);
          return d.promise;
        });
        schedule('b');
        return waitForIdle().then(function() {
          assertFlowHistory('a', 'c', 'b');
        });
      });
    });

    describe('testFraming', function() {
      it('callbacksRunInANewFrame', function() {
        schedule('a').then(function() {
          schedule('c');
        });
        schedule('b');
        return waitForIdle().then(function() {
          assertFlowHistory('a', 'c', 'b');
        });
      });

      it('lotsOfNesting', function() {
        schedule('a').then(function() {
          schedule('c').then(function() {
            schedule('e').then(function() {
              schedule('g');
            });
            schedule('f');
          });
          schedule('d');
        });
        schedule('b');

        return waitForIdle().then(function() {
          assertFlowHistory('a', 'c', 'e', 'g', 'f', 'd', 'b');
        });
      });

      it('callbackReturnsPromiseThatDependsOnATask_1', function() {
        schedule('a').then(function() {
          schedule('b');
          return promise.delayed(5).then(function() {
            return schedule('c');
          });
        });
        schedule('d');

        return waitForIdle().then(function() {
          assertFlowHistory('a', 'b', 'c', 'd');
        });
      });

      it('callbackReturnsPromiseThatDependsOnATask_2', function() {
        schedule('a').then(function() {
          schedule('b');
          return promise.delayed(5).
              then(function() { return promise.delayed(5) }).
              then(function() { return promise.delayed(5) }).
              then(function() { return promise.delayed(5) }).
              then(function() { return schedule('c'); });
        });
        schedule('d');

        return waitForIdle().then(function() {
          assertFlowHistory('a', 'b', 'c', 'd');
        });
      });

      it('eachCallbackWaitsForAllScheduledTasksToComplete', function() {
        schedule('a').
            then(function() {
              schedule('b');
              schedule('c');
            }).
            then(function() {
              schedule('d');
            });
        schedule('e');

        return waitForIdle().then(function() {
          assertFlowHistory('a', 'b', 'c', 'd', 'e');
        });
      });

      it('eachCallbackWaitsForReturnTasksToComplete', function() {
        schedule('a').
            then(function() {
              schedule('b');
              return schedule('c');
            }).
            then(function() {
              schedule('d');
            });
        schedule('e');

        return waitForIdle().then(function() {
          assertFlowHistory('a', 'b', 'c', 'd', 'e');
        });
      });

      it('callbacksOnAResolvedPromiseInsertIntoTheCurrentFlow', function() {
        promise.fulfilled().then(function() {
          schedule('b');
        });
        schedule('a');

        return waitForIdle().then(function() {
          assertFlowHistory('b', 'a');
        });
      });

      it('callbacksInterruptTheFlowWhenPromiseIsResolved', function() {
        schedule('a').then(function() {
          schedule('c');
        });
        schedule('b');

        return waitForIdle().then(function() {
          assertFlowHistory('a', 'c', 'b');
        });
      });

      it('allCallbacksInAFrameAreScheduledWhenPromiseIsResolved', function() {
        var a = schedule('a');
        a.then(function() { schedule('b'); });
        schedule('c');
        a.then(function() { schedule('d'); });
        schedule('e');

        return waitForIdle().then(function() {
          assertFlowHistory('a', 'b', 'c', 'd', 'e');
        });
      });

      it('tasksScheduledInInActiveFrameDoNotGetPrecedence', function() {
        var d = promise.fulfilled();
        schedule('a');
        schedule('b');
        d.then(function() { schedule('c'); });

        return waitForIdle().then(function() {
          assertFlowHistory('a', 'b', 'c');
        });
      });

      it('tasksScheduledInAFrameGetPrecedence_1', function() {
        var a = schedule('a');
        schedule('b').then(function() {
          a.then(function() {
            schedule('c');
            schedule('d');
          });
          var e = schedule('e');
          a.then(function() {
            schedule('f');
            e.then(function() {
              schedule('g');
            });
            schedule('h');
          });
          schedule('i');
        });
        schedule('j');

        return waitForIdle().then(function() {
          assertFlowHistory('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j');
        });
      });
    });

    describe('testErrorHandling', function() {
      it('thrownErrorsArePassedToTaskErrback', function() {
        scheduleAction('function that throws', throwStubError).
            then(fail, assertIsStubError);
        return waitForIdle();
      });

      it('thrownErrorsPropagateThroughPromiseChain', function() {
        scheduleAction('function that throws', throwStubError).
            then(fail).
            then(fail, assertIsStubError);
        return waitForIdle();
      });

      it('catchesErrorsFromFailedTasksInAFrame', function() {
        schedule('a').then(function() {
          schedule('b');
          scheduleAction('function that throws', throwStubError);
        }).
        then(fail, assertIsStubError);
        return waitForIdle();
      });

      it('abortsIfOnlyTaskReturnsAnUnhandledRejection', function() {
        scheduleAction('function that returns rejected promise', function() {
          return promise.rejected(new StubError);
        });
        return waitForAbort().then(assertIsStubError);
      });

      it('abortsIfThereIsAnUnhandledRejection', function() {
        promise.rejected(new StubError);
        schedule('this should not run');
        return waitForAbort().
            then(assertIsStubError).
            then(function() {
              assertFlowHistory(/* none */);
            });
      });

      it('abortsSequenceIfATaskFails', function() {
        schedule('a');
        schedule('b');
        scheduleAction('c', throwStubError);
        schedule('d');  // Should never execute.

        return waitForAbort().
            then(assertIsStubError).
            then(function() {
              assertFlowHistory('a', 'b', 'c');
            });
      });

      it('abortsFromUnhandledFramedTaskFailures_1', function() {
        schedule('outer task').then(function() {
          scheduleAction('inner task', throwStubError);
        });
        schedule('this should not run');
        return waitForAbort().
            then(assertIsStubError).
            then(function() {
              assertFlowHistory('outer task', 'inner task');
            });
      });

      it('abortsFromUnhandledFramedTaskFailures_2', function() {
        schedule('a').then(function() {
          schedule('b').then(function() {
            scheduleAction('c', throwStubError);
            // This should not execute.
            schedule('d');
          });
        });

        return waitForAbort().
            then(assertIsStubError).
            then(function() {
              assertFlowHistory('a', 'b', 'c');
            });
      });

      it('abortsWhenErrorBubblesUpFromFullyResolvingAnObject', function() {
        var callback = sinon.spy();

        scheduleAction('', function() {
          var obj = {'foo': promise.rejected(new StubError)};
          return promise.fullyResolved(obj).then(callback);
        });

        return waitForAbort().
            then(assertIsStubError).
            then(() => assert(!callback.called));
      });

      it('abortsWhenErrorBubblesUpFromFullyResolvingAnObject_withCallback', function() {
        var callback1 = sinon.spy();
        var callback2 = sinon.spy();

        scheduleAction('', function() {
          var obj = {'foo': promise.rejected(new StubError)};
          return promise.fullyResolved(obj).then(callback1);
        }).then(callback2);

        return waitForAbort().
            then(assertIsStubError).
            then(() => assert(!callback1.called)).
            then(() => assert(!callback2.called));
      });

      it('canCatchErrorsFromNestedTasks', function() {
        var errback = sinon.spy();
        schedule('a').
            then(function() {
              return scheduleAction('b', throwStubError);
            }).
            catch(errback);
        return waitForIdle().then(function() {
          assert(errback.called);
          assertIsStubError(errback.getCall(0).args[0]);
        });
      });

      it('nestedCommandFailuresCanBeCaughtAndSuppressed', function() {
        var errback = sinon.spy();
        schedule('a').then(function() {
          return schedule('b').then(function() {
            return schedule('c').then(function() {
              throw new StubError;
            });
          });
        }).catch(errback);
        schedule('d');
        return waitForIdle().
            then(function() {
              assert(errback.called);
              assertIsStubError(errback.getCall(0).args[0]);
              assertFlowHistory('a', 'b', 'c', 'd');
            });
      });

      it('aTaskWithAnUnhandledPromiseRejection', function() {
        schedule('a');
        scheduleAction('sub-tasks', function() {
          promise.rejected(new StubError);
        });
        schedule('should never run');

        return waitForAbort().
            then(assertIsStubError).
            then(function() {
              assertFlowHistory('a', 'sub-tasks');
            });
      });

      it('aTaskThatReutrnsARejectedPromise', function() {
        schedule('a');
        scheduleAction('sub-tasks', function() {
          return promise.rejected(new StubError);
        });
        schedule('should never run');

        return waitForAbort().
            then(assertIsStubError).
            then(function() {
              assertFlowHistory('a', 'sub-tasks');
            });
      });

      it('discardsSubtasksIfTaskThrows', function() {
        var pair = callbackPair(null, assertIsStubError);
        scheduleAction('a', function() {
          schedule('b');
          schedule('c');
          throwStubError();
        }).then(pair.callback, pair.errback);
        schedule('d');

        return waitForIdle().
            then(pair.assertErrback).
            then(function() {
              assertFlowHistory('a', 'd');
            });
      });

      it('discardsRemainingSubtasksIfASubtaskFails', function() {
        var pair = callbackPair(null, assertIsStubError);
        scheduleAction('a', function() {
          schedule('b');
          scheduleAction('c', throwStubError);
          schedule('d');
        }).then(pair.callback, pair.errback);
        schedule('e');

        return waitForIdle().
            then(pair.assertErrback).
            then(function() {
              assertFlowHistory('a', 'b', 'c', 'e');
            });
      });
    });

    describe('testTryModelingFinally', function() {
      it('happyPath', function() {
        /* Model:
           try {
             doFoo();
             doBar();
           } finally {
             doBaz();
           }
         */
        schedulePush('foo').
            then(() => schedulePush('bar')).
            finally(() => schedulePush('baz'));
        return waitForIdle().then(assertingMessages('foo', 'bar', 'baz'));
      });

      it('firstTryFails', function() {
        /* Model:
           try {
             doFoo();
             doBar();
           } finally {
             doBaz();
           }
         */

        scheduleAction('doFoo and throw', function() {
          messages.push('foo');
          throw new StubError;
        }).
        then(function() { schedulePush('bar'); }).
        finally(function() { schedulePush('baz'); });

        return waitForAbort().
            then(assertIsStubError).
            then(assertingMessages('foo', 'baz'));
      });

      it('secondTryFails', function() {
        /* Model:
           try {
             doFoo();
             doBar();
           } finally {
             doBaz();
           }
         */

        schedulePush('foo').
            then(function() {
              return scheduleAction('doBar and throw', function() {
                messages.push('bar');
                throw new StubError;
              });
            }).
            finally(function() {
              return schedulePush('baz');
            });
        return waitForAbort().
            then(assertIsStubError).
            then(assertingMessages('foo', 'bar', 'baz'));
      });
    });

    describe('testTaskCallbacksInterruptFlow', function() {
      it('(base case)', function() {
        schedule('a').then(function() {
          schedule('b');
        });
        schedule('c');
        return waitForIdle().then(function() {
          assertFlowHistory('a', 'b', 'c');
        });
      });

      it('taskDependsOnImmediatelyFulfilledPromise', function() {
        scheduleAction('a', function() {
          return promise.fulfilled();
        }).then(function() {
          schedule('b');
        });
        schedule('c');
        return waitForIdle().then(function() {
          assertFlowHistory('a', 'b', 'c');
        });
      });

      it('taskDependsOnPreviouslyFulfilledPromise', function() {
        var aPromise = promise.fulfilled(123);
        scheduleAction('a', function() {
          return aPromise;
        }).then(function(value) {
          assert.equal(123, value);
          schedule('b');
        });
        schedule('c');
        return waitForIdle().then(function() {
          assertFlowHistory('a', 'b', 'c');
        });
      });

      it('taskDependsOnAsyncPromise', function() {
        scheduleAction('a', function() {
          return promise.delayed(25);
        }).then(function() {
          schedule('b');
        });
        schedule('c');
        return waitForIdle().then(function() {
          assertFlowHistory('a', 'b', 'c');
        });
      });

      it('promiseChainedToTaskInterruptFlow', function() {
        schedule('a').then(function() {
          return promise.fulfilled();
        }).then(function() {
          return promise.fulfilled();
        }).then(function() {
          schedule('b');
        });
        schedule('c');
        return waitForIdle().then(function() {
          assertFlowHistory('a', 'b', 'c');
        });
      });

      it('nestedTaskCallbacksInterruptFlowWhenResolved', function() {
        schedule('a').then(function() {
          schedule('b').then(function() {
            schedule('c');
          });
        });
        schedule('d');
        return waitForIdle().then(function() {
          assertFlowHistory('a', 'b', 'c', 'd');
        });
      });
    });

    describe('testDelayedNesting', function() {

      it('1', function() {
        var a = schedule('a');
        schedule('b').then(function() {
          a.then(function() { schedule('c'); });
          schedule('d');
        });
        schedule('e');

        return waitForIdle().then(function() {
          assertFlowHistory('a', 'b', 'c', 'd', 'e');
        });
      });

      it('2', function() {
        var a = schedule('a');
        schedule('b').then(function() {
          a.then(function() { schedule('c'); });
          schedule('d');
          a.then(function() { schedule('e'); });
        });
        schedule('f');

        return waitForIdle().then(function() {
          assertFlowHistory('a', 'b', 'c', 'd', 'e', 'f');
        });
      });

      it('3', function() {
        var a = schedule('a');
        schedule('b').then(function() {
          a.then(function() { schedule('c'); });
          a.then(function() { schedule('d'); });
        });
        schedule('e');

        return waitForIdle().then(function() {
          assertFlowHistory('a', 'b', 'c', 'd', 'e');
        });
      });

      it('4', function() {
        var a = schedule('a');
        schedule('b').then(function() {
          a.then(function() { schedule('c'); }).then(function() {
            schedule('d');
          });
          a.then(function() { schedule('e'); });
        });
        schedule('f');

        return waitForIdle().then(function() {
          assertFlowHistory('a', 'b', 'c', 'd', 'e', 'f');
        });
      });

      it('5', function() {
        var a = schedule('a');
        schedule('b').then(function() {
          var c;
          a.then(function() { c = schedule('c'); }).then(function() {
            schedule('d');
            a.then(function() { schedule('e'); });
            c.then(function() { schedule('f'); });
            schedule('g');
          });
          a.then(function() { schedule('h'); });
        });
        schedule('i');

        return waitForIdle().then(function() {
          assertFlowHistory('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i');
        });
      });
    });

    describe('testWaiting', function() {
      it('onAConditionThatIsAlwaysTrue', function() {
        scheduleWait(function() { return true;}, 0, 'waiting on true');
        return waitForIdle().then(function() {
          assertFlowHistory('0: waiting on true');
        });
      });

      it('aSimpleCountingCondition', function() {
        var count = 0;
        scheduleWait(function() {
          return ++count == 3;
        }, 100, 'counting to 3');

        return waitForIdle().then(function() {
          assert.equal(3, count);
        });
      });

      it('aConditionThatReturnsAPromise', function() {
        var d = new promise.Deferred();
        var count = 0;

        scheduleWait(function() {
          count += 1;
          return d.promise;
        }, 0, 'waiting for promise');

        return timeout(50).then(function() {
          assert.equal(1, count);
          d.fulfill(123);
          return waitForIdle();
        });
      });

      it('aConditionThatReturnsAPromise_2', function() {
        var count = 0;
        scheduleWait(function() {
          return promise.fulfilled(++count == 3);
        }, 100, 'waiting for promise');

        return waitForIdle().then(function() {
          assert.equal(3, count);
        });
      });

      it('aConditionThatReturnsATaskResult', function() {
        var count = 0;
        scheduleWait(function() {
          return scheduleAction('increment count', function() {
            return ++count == 3;
          });
        }, 100, 'counting to 3');
        schedule('post wait');

        return waitForIdle().then(function() {
          assert.equal(3, count);
          assertFlowHistory(
              '0: counting to 3', 'increment count',
              '1: counting to 3', 'increment count',
              '2: counting to 3', 'increment count',
              'post wait');
        });
      });

      it('conditionContainsASubtask', function() {
        var count = 0;
        scheduleWait(function() {
          schedule('sub task');
          return ++count == 3;
        }, 100, 'counting to 3');
        schedule('post wait');

        return waitForIdle().then(function() {
          assert.equal(3, count);
          assertFlowHistory(
              '0: counting to 3', 'sub task',
              '1: counting to 3', 'sub task',
              '2: counting to 3', 'sub task',
              'post wait');
        });
      });

      it('cancelsWaitIfScheduledTaskFails', function() {
        var pair = callbackPair(null, assertIsStubError);
        scheduleWait(function() {
          scheduleAction('boom', throwStubError);
          schedule('this should not run');
          return true;
        }, 100, 'waiting to go boom').then(pair.callback, pair.errback);
        schedule('post wait');

        return waitForIdle().
            then(pair.assertErrback).
            then(function() {
              assertFlowHistory(
                  '0: waiting to go boom', 'boom',
                  'post wait');
            });
      });

      it('failsIfConditionThrows', function() {
        var callbacks = callbackPair(null, assertIsStubError);
        scheduleWait(throwStubError, 0, 'goes boom').
            then(callbacks.callback, callbacks.errback);
        schedule('post wait');

        return waitForIdle().
            then(callbacks.assertErrback).
            then(function() {
              assertFlowHistory('0: goes boom', 'post wait');
            });
      });

      it('failsIfConditionReturnsARejectedPromise', function() {
        var callbacks = callbackPair(null, assertIsStubError);
        scheduleWait(function() {
          return promise.rejected(new StubError);
        }, 0, 'goes boom').then(callbacks.callback, callbacks.errback);
        schedule('post wait');

        return waitForIdle().
            then(callbacks.assertErrback).
            then(function() {
              assertFlowHistory('0: goes boom', 'post wait');
            });
      });

      it('failsIfConditionHasUnhandledRejection', function() {
        var callbacks = callbackPair(null, assertIsStubError);
        scheduleWait(function() {
          promise.controlFlow().execute(throwStubError);
        }, 0, 'goes boom').then(callbacks.callback, callbacks.errback);
        schedule('post wait');

        return waitForIdle().
            then(callbacks.assertErrback).
            then(function() {
              assertFlowHistory('0: goes boom', 'post wait');
            });
      });

      it('failsIfConditionHasAFailedSubtask', function() {
        var callbacks = callbackPair(null, assertIsStubError);
        var count = 0;
        scheduleWait(function() {
          scheduleAction('maybe throw', function() {
            if (++count == 2) {
              throw new StubError;
            }
          });
        }, 100, 'waiting').then(callbacks.callback, callbacks.errback);
        schedule('post wait');

        return waitForIdle().then(function() {
          assert.equal(2, count);
          assertFlowHistory(
              '0: waiting', 'maybe throw',
              '1: waiting', 'maybe throw',
              'post wait');
        });
      });

      it('pollingLoopWaitsForAllScheduledTasksInCondition', function() {
        var count = 0;
        scheduleWait(function() {
          scheduleAction('increment count', function() { ++count; });
          return count >= 3;
        }, 100, 'counting to 3');
        schedule('post wait');

        return waitForIdle().then(function() {
          assert.equal(4, count);
          assertFlowHistory(
              '0: counting to 3', 'increment count',
              '1: counting to 3', 'increment count',
              '2: counting to 3', 'increment count',
              '3: counting to 3', 'increment count',
              'post wait');
        });
      });

      it('waitsForeverOnAZeroTimeout', function() {
        var done = false;
        setTimeout(function() {
          done = true;
        }, 150);
        var waitResult = scheduleWait(function() {
          return done;
        }, 0);

        return timeout(75).then(function() {
          assert.ok(!done);
          return timeout(100);
        }).then(function() {
          assert.ok(done);
          return waitResult;
        });
      });

      it('waitsForeverIfTimeoutOmitted', function() {
        var done = false;
        setTimeout(function() {
          done = true;
        }, 150);
        var waitResult = scheduleWait(function() {
          return done;
        });

        return timeout(75).then(function() {
          assert.ok(!done);
          return timeout(100);
        }).then(function() {
          assert.ok(done);
          return waitResult;
        });
      });

      it('timesOut_nonZeroTimeout', function() {
        var count = 0;
        scheduleWait(function() {
          count += 1;
          var ms = count === 2 ? 65 : 5;
          return promise.delayed(ms).then(function() {
            return false;
          });
        }, 60, 'counting to 3');
        return waitForAbort().then(function(e) {
          switch (count) {
            case 1:
              assertFlowHistory('0: counting to 3');
              break;
            case 2:
              assertFlowHistory('0: counting to 3', '1: counting to 3');
              break;
            default:
              fail('unexpected polling count: ' + count);
          }
          assert.ok(e instanceof TimeoutError, 'Unexpected error: ' + e);
          assert.ok(
              /^counting to 3\nWait timed out after \d+ms$/.test(e.message));
        });
      });

      it('shouldFailIfConditionReturnsARejectedPromise', function() {
        scheduleWait(function() {
          return promise.rejected(new StubError);
        }, 100, 'returns rejected promise on first pass');
        return waitForAbort().then(assertIsStubError);
      });

      it('scheduleWithIntermittentWaits', function() {
        schedule('a');
        scheduleWait(function() { return true; }, 0, 'wait 1');
        schedule('b');
        scheduleWait(function() { return true; }, 0, 'wait 2');
        schedule('c');
        scheduleWait(function() { return true; }, 0, 'wait 3');

        return waitForIdle().then(function() {
          assertFlowHistory('a', '0: wait 1', 'b', '0: wait 2', 'c', '0: wait 3');
        });
      });

      it('scheduleWithIntermittentAndNestedWaits', function() {
        schedule('a');
        scheduleWait(function() { return true; }, 0, 'wait 1').
            then(function() {
              schedule('d');
              scheduleWait(function() { return true; }, 0, 'wait 2');
              schedule('e');
            });
        schedule('b');
        scheduleWait(function() { return true; }, 0, 'wait 3');
        schedule('c');
        scheduleWait(function() { return true; }, 0, 'wait 4');

        return waitForIdle().then(function() {
          assertFlowHistory(
              'a', '0: wait 1', 'd', '0: wait 2', 'e', 'b', '0: wait 3', 'c',
              '0: wait 4');
        });
      });

      it('requiresConditionToBeAPromiseOrFunction', function() {
        assert.throws(function() {
          flow.wait(1234, 0);
        });
        flow.wait(function() { return true;}, 0);
        flow.wait(promise.fulfilled(), 0);
        return waitForIdle();
      });

      it('promiseThatDoesNotResolveBeforeTimeout', function() {
        var d = promise.defer();
        flow.wait(d.promise, 5).then(fail, function(e) {
          assert.ok(e instanceof TimeoutError, 'Unexpected error: ' + e);
          assert.ok(
              /Timed out waiting for promise to resolve after \d+ms/
                  .test(e.message),
              'unexpected error message: ' + e.message);
        });
        return waitForIdle();
      });

      it('unboundedWaitOnPromiseResolution', function() {
        var messages = [];
        var d = promise.defer();
        var waitResult = flow.wait(d.promise).then(function(value) {
          messages.push('b');
          assert.equal(1234, value);
        });
        setTimeout(function() {
          messages.push('a');
        }, 5);

        timeout(10).then(function() {
          assert.deepEqual(['a'], messages);
          d.fulfill(1234);
          return waitResult;
        }).then(function() {
          assert.deepEqual(['a', 'b'], messages);
        });

        return waitForIdle();
      });
    });

    describe('testSubtasks', function() {
      it('(base case)', function() {
        schedule('a');
        scheduleAction('sub-tasks', function() {
          schedule('c');
          schedule('d');
        });
        schedule('b');

        return waitForIdle().then(function() {
          assertFlowHistory('a', 'sub-tasks', 'c', 'd', 'b');
        });
      });

      it('nesting', function() {
        schedule('a');
        scheduleAction('sub-tasks', function() {
          schedule('b');
          scheduleAction('sub-sub-tasks', function() {
            schedule('c');
            schedule('d');
          });
          schedule('e');
        });
        schedule('f');

        return waitForIdle().then(function() {
          assertFlowHistory(
              'a', 'sub-tasks', 'b', 'sub-sub-tasks', 'c', 'd', 'e', 'f');
        });
      });

      it('taskReturnsSubTaskResult_1', function() {
        schedule('a');
        scheduleAction('sub-tasks', function() {
          return schedule('c');
        });
        schedule('b');

        return waitForIdle().then(function() {
          assertFlowHistory('a', 'sub-tasks', 'c', 'b');
        });
      });

      it('taskReturnsSubTaskResult_2', function() {
        let pair = callbackPair((value) => assert.equal(123, value));
        schedule('a');
        schedule('sub-tasks', promise.fulfilled(123)).then(pair.callback);
        schedule('b');

        return waitForIdle().then(function() {
          assertFlowHistory('a', 'sub-tasks','b');
          pair.assertCallback();
        });
      });

      it('taskReturnsPromiseThatDependsOnSubtask_1', function() {
        scheduleAction('a', function() {
          return promise.delayed(10).then(function() {
            schedule('b');
          });
        });
        schedule('c');
        return waitForIdle().then(function() {
          assertFlowHistory('a', 'b', 'c');
        });
      });

      it('taskReturnsPromiseThatDependsOnSubtask_2', function() {
        scheduleAction('a', function() {
          return promise.fulfilled().then(function() {
            schedule('b');
          });
        });
        schedule('c');
        return waitForIdle().then(function() {
          assertFlowHistory('a', 'b', 'c');
        });
      });

      it('taskReturnsPromiseThatDependsOnSubtask_3', function() {
        scheduleAction('a', function() {
          return promise.delayed(10).then(function() {
            return schedule('b');
          });
        });
        schedule('c');
        return waitForIdle().then(function() {
          assertFlowHistory('a', 'b', 'c');
        });
      });

      it('taskReturnsPromiseThatDependsOnSubtask_4', function() {
        scheduleAction('a', function() {
          return promise.delayed(5).then(function() {
            return promise.delayed(5).then(function() {
              return schedule('b');
            });
          });
        });
        schedule('c');
        return waitForIdle().then(function() {
          assertFlowHistory('a', 'b', 'c');
        });
      });

      it('taskReturnsPromiseThatDependsOnSubtask_5', function() {
        scheduleAction('a', function() {
          return promise.delayed(5).then(function() {
            return promise.delayed(5).then(function() {
              return promise.delayed(5).then(function() {
                return promise.delayed(5).then(function() {
                  return schedule('b');
                });
              });
            });
          });
        });
        schedule('c');
        return waitForIdle().then(function() {
          assertFlowHistory('a', 'b', 'c');
        });
      });

      it('taskReturnsPromiseThatDependsOnSubtask_6', function() {
        scheduleAction('a', function() {
          return promise.delayed(5).
              then(function() { return promise.delayed(5) }).
              then(function() { return promise.delayed(5) }).
              then(function() { return promise.delayed(5) }).
              then(function() { return schedule('b'); });
        });
        schedule('c');
        return waitForIdle().then(function() {
          assertFlowHistory('a', 'b', 'c');
        });
      });

      it('subTaskFails_1', function() {
        schedule('a');
        scheduleAction('sub-tasks', function() {
          scheduleAction('sub-task that fails', throwStubError);
        });
        schedule('should never execute');

        return waitForAbort().
            then(assertIsStubError).
            then(function() {
              assertFlowHistory('a', 'sub-tasks', 'sub-task that fails');
            });
      });

      it('subTaskFails_2', function() {
        schedule('a');
        scheduleAction('sub-tasks', function() {
          return promise.rejected(new StubError);
        });
        schedule('should never execute');

        return waitForAbort().
            then(assertIsStubError).
            then(function() {
              assertFlowHistory('a', 'sub-tasks');
            });
      });

      it('subTaskFails_3', function() {
        var callbacks = callbackPair(null, assertIsStubError);

        schedule('a');
        scheduleAction('sub-tasks', function() {
          return promise.rejected(new StubError);
        }).then(callbacks.callback, callbacks.errback);
        schedule('b');

        return waitForIdle().
            then(function() {
              assertFlowHistory('a', 'sub-tasks', 'b');
              callbacks.assertErrback();
            });
      });
    });

    describe('testEventLoopWaitsOnPendingPromiseRejections', function() {
      it('oneRejection', function() {
        var d = new promise.Deferred;
        scheduleAction('one', function() {
          return d.promise;
        });
        scheduleAction('two', function() {});

        return timeout(50).then(function() {
          assertFlowHistory('one');
          d.reject(new StubError);
          return waitForAbort();
        }).
        then(assertIsStubError).
        then(function() {
          assertFlowHistory('one');
        });
      });

      it('multipleRejections', function() {
        var once = Error('once');
        var twice = Error('twice');

        scheduleAction('one', function() {
          promise.rejected(once);
          promise.rejected(twice);
        });
        var twoResult = scheduleAction('two', function() {});

        flow.removeAllListeners(
            promise.ControlFlow.EventType.UNCAUGHT_EXCEPTION);
        return new NativePromise(function(fulfill, reject) {
          setTimeout(function() {
            reject(Error('Should have reported the two errors by now'));
          }, 50);
          flow.on(
              promise.ControlFlow.EventType.UNCAUGHT_EXCEPTION,
              fulfill);
        }).then(function(e) {
          assert.ok(e instanceof promise.MultipleUnhandledRejectionError,
              'Not a MultipleUnhandledRejectionError');
          let errors = Array.from(e.errors);
          assert.deepEqual([once, twice], errors);
          assertFlowHistory('one');
        });
      });
    });

    describe('testCancelsPromiseReturnedByCallbackIfFrameFails', function() {
      it('promiseCallback', function() {
        var chainPair = callbackPair(null, assertIsStubError);
        var deferredPair = callbackPair(null, function(e) {
          assert.equal('CancellationError: StubError', e.toString(),
              'callback result should be cancelled');
        });

        var d = new promise.Deferred();
        d.promise.then(deferredPair.callback, deferredPair.errback);

        promise.fulfilled().
            then(function() {
              scheduleAction('boom', throwStubError);
              schedule('this should not run');
              return d.promise;
            }).
            then(chainPair.callback, chainPair.errback);

        return waitForIdle().then(function() {
          assertFlowHistory('boom');
          chainPair.assertErrback('chain errback not invoked');
          deferredPair.assertErrback('deferred errback not invoked');
        });
      });

      it('taskCallback', function() {
        var chainPair = callbackPair(null, assertIsStubError);
        var deferredPair = callbackPair(null, function(e) {
          assert.equal('CancellationError: StubError', e.toString(),
              'callback result should be cancelled');
        });

        var d = new promise.Deferred();
        d.promise.then(deferredPair.callback, deferredPair.errback);

        schedule('a').
            then(function() {
              scheduleAction('boom', throwStubError);
              schedule('this should not run');
              return d.promise;
            }).
            then(chainPair.callback, chainPair.errback);

        return waitForIdle().then(function() {
          assertFlowHistory('a', 'boom');
          chainPair.assertErrback('chain errback not invoked');
          deferredPair.assertErrback('deferred errback not invoked');
        });
      });
    });

    it('testMaintainsOrderInCallbacksWhenATaskReturnsAPromise', function() {
      schedule('__start__', promise.fulfilled()).
          then(function() {
            messages.push('a');
            schedulePush('b');
            messages.push('c');
          }).
          then(function() {
            messages.push('d');
          });
      schedulePush('e');

      return waitForIdle().then(function() {
        assertFlowHistory('__start__', 'b', 'e');
        assertMessages('a', 'c', 'b', 'd', 'e');
      });
    });

    it('testOwningFlowIsActivatedForExecutingTasks', function() {
      var defaultFlow = promise.controlFlow();
      var order = [];

      promise.createFlow(function(flow) {
        assertFlowIs(flow);
        order.push(0);

        defaultFlow.execute(function() {
          assertFlowIs(defaultFlow);
          order.push(1);
        });
      });

      return waitForIdle().then(function() {
        assertFlowIs(defaultFlow);
        assert.deepEqual([0, 1], order);
      });
    });

    it('testCreateFlowReturnsPromisePairedWithCreatedFlow', function() {
      return new NativePromise(function(fulfill, reject) {
        var newFlow;
        promise.createFlow(function(flow) {
          newFlow = flow;
          assertFlowIs(newFlow);
        }).then(function() {
          assertFlowIs(newFlow);
          waitForIdle(newFlow).then(fulfill, reject);
        });
      });
    });

    it('testDeferredFactoriesCreateForActiveFlow_defaultFlow', function() {
      var e = Error();
      var defaultFlow = promise.controlFlow();
      promise.fulfilled().then(function() {
        assertFlowIs(defaultFlow);
      });
      promise.rejected(e).then(null, function(err) {
        assert.equal(e, err);
        assertFlowIs(defaultFlow);
      });
      promise.defer().promise.then(function() {
        assertFlowIs(defaultFlow);
      });

      return waitForIdle();
    });

    it('testDeferredFactoriesCreateForActiveFlow_newFlow', function() {
      var e = Error();
      var newFlow = new promise.ControlFlow;
      newFlow.execute(function() {
        promise.fulfilled().then(function() {
          assertFlowIs(newFlow);
        });

        promise.rejected(e).then(null, function(err) {
          assert.equal(e, err);
          assertFlowIs(newFlow);
        });

        let d = promise.defer();
        d.promise.then(function() {
          assertFlowIs(newFlow);
        });
        d.fulfill();
      }).then(function() {
        assertFlowIs(newFlow);
      });

      return waitForIdle(newFlow);
    });

    it('testFlowsSynchronizeWithThemselvesNotEachOther', function() {
      var defaultFlow = promise.controlFlow();
      schedulePush('a', 'a');
      promise.controlFlow().timeout(500);
      schedulePush('b', 'b');

      promise.createFlow(function(flow2) {
        assertFlowIs(flow2);
        schedulePush('c', 'c');
        schedulePush('d', 'd');
      });

      return waitForIdle().then(function() {
        assertMessages('a', 'c', 'd', 'b');
      });
    });

    it('testUnhandledErrorsAreReportedToTheOwningFlow', function() {
      var error1 = Error('e1');
      var error2 = Error('e2');

      var defaultFlow = promise.controlFlow();
      defaultFlow.removeAllListeners('uncaughtException');

      var flow1Error = defer();
      flow1Error.promise.then(function(value) {
        assert.equal(error2, value);
      });

      var flow2Error = defer();
      flow2Error.promise.then(function(value) {
        assert.equal(error1, value);
      });

      promise.createFlow(function(flow) {
        flow.once('uncaughtException', flow2Error.resolve);
        promise.rejected(error1);

        defaultFlow.once('uncaughtException', flow1Error.resolve);
        defaultFlow.execute(function() {
          promise.rejected(error2);
        });
      });

      return NativePromise.all([flow1Error.promise, flow2Error.promise]);
    });

    it('testCanSynchronizeFlowsByReturningPromiseFromOneToAnother', function() {
      var flow1 = new promise.ControlFlow;
      var flow1Done = defer();
      flow1.once('idle', flow1Done.resolve);
      flow1.once('uncaughtException', flow1Done.reject);

      var flow2 = new promise.ControlFlow;
      var flow2Done = defer();
      flow2.once('idle', flow2Done.resolve);
      flow2.once('uncaughtException', flow2Done.reject);

      flow1.execute(function() {
        schedulePush('a', 'a');
        return promise.delayed(25);
      }, 'start flow 1');

      flow2.execute(function() {
        schedulePush('b', 'b');
        schedulePush('c', 'c');
        flow2.execute(function() {
          return flow1.execute(function() {
            schedulePush('d', 'd');
          }, 'flow 1 task');
        }, 'inject flow1 result into flow2');
        schedulePush('e', 'e');
      }, 'start flow 2');

      return NativePromise.all([flow1Done.promise, flow2Done.promise]).
          then(function() {
            assertMessages('a', 'b', 'c', 'd', 'e');
          });
    });

    it('testFramesWaitToCompleteForPendingRejections', function() {
      return new NativePromise(function(fulfill, reject) {

        promise.controlFlow().execute(function() {
          promise.rejected(new StubError);
        }).then(fulfill, reject);

      }).
      then(() => fail('expected to fail'), assertIsStubError);
    });

    it('testSynchronizeErrorsPropagateToOuterFlow', function() {
      var outerFlow = new promise.ControlFlow;
      var innerFlow = new promise.ControlFlow;

      var block = defer();
      innerFlow.execute(function() {
        return block.promise;
      }, 'block inner flow');

      outerFlow.execute(function() {
        block.resolve();
        return innerFlow.execute(function() {
          promise.rejected(new StubError);
        }, 'trigger unhandled rejection error');
      }, 'run test');

      return NativePromise.all([
        waitForIdle(innerFlow),
        waitForAbort(outerFlow).then(assertIsStubError)
      ]);
    });

    it('testFailsIfErrbackThrows', function() {
      promise.rejected('').then(null, throwStubError);
      return waitForAbort().then(assertIsStubError);
    });

    it('testFailsIfCallbackReturnsRejectedPromise', function() {
      promise.fulfilled().then(function() {
        return promise.rejected(new StubError);
      });
      return waitForAbort().then(assertIsStubError);
    });

    it('testAbortsFrameIfTaskFails', function() {
      promise.fulfilled().then(function() {
        promise.controlFlow().execute(throwStubError);
      });
      return waitForAbort().then(assertIsStubError);
    });

    it('testAbortsFramePromisedChainedFromTaskIsNotHandled', function() {
      promise.fulfilled().then(function() {
        promise.controlFlow().execute(function() {}).
            then(throwStubError);
      });
      return waitForAbort().then(assertIsStubError);
    });

    it('testTrapsChainedUnhandledRejectionsWithinAFrame', function() {
      var pair = callbackPair(null, assertIsStubError);
      promise.fulfilled().then(function() {
        promise.controlFlow().execute(function() {}).
            then(throwStubError);
      }).then(pair.callback, pair.errback);

      return waitForIdle().then(pair.assertErrback);
    });

    it('testCancelsRemainingTasksIfFrameThrowsDuringScheduling', function() {
      var task1, task2;
      var pair = callbackPair(null, assertIsStubError);
      var flow = promise.controlFlow();
      flow.execute(function() {
        task1 = flow.execute(function() {});
        task2 = flow.execute(function() {});
        throw new StubError;
      }).then(pair.callback, pair.errback);

      return waitForIdle().
          then(pair.assertErrback).
          then(function() {
            pair = callbackPair();
            return task1.then(pair.callback, pair.errback);
          }).
          then(function() {
            pair.assertErrback();
            pair = callbackPair();
            return task2.then(pair.callback, pair.errback);
          }).
          then(function() {
            pair.assertErrback();
          });
    });

    it('testCancelsRemainingTasksInFrameIfATaskFails', function() {
      var task;
      var pair = callbackPair(null, assertIsStubError);
      var flow = promise.controlFlow();
      flow.execute(function() {
        flow.execute(throwStubError);
        task = flow.execute(function() {});
      }).then(pair.callback, pair.errback);

      return waitForIdle().then(pair.assertErrback).then(function() {
        pair = callbackPair();
        task.then(pair.callback, pair.errback);
      }).then(function() {
        pair.assertErrback();
      });
    });

    it('testDoesNotModifyRejectionErrorIfPromiseNotInsideAFlow', function() {
      var error = Error('original message');
      var originalStack = error.stack;
      var originalStr = error.toString();

      var pair = callbackPair(null, function(e) {
        assert.equal(error, e);
        assert.equal('original message', e.message);
        assert.equal(originalStack, e.stack);
        assert.equal(originalStr, e.toString());
      });

      promise.rejected(error).then(pair.callback, pair.errback);
      return waitForIdle().then(pair.assertErrback);
    });

    /** See https://github.com/SeleniumHQ/selenium/issues/444 */
    it('testMaintainsOrderWithPromiseChainsCreatedWithinAForeach_1', function() {
      var messages = [];
      flow.execute(function() {
        return promise.fulfilled(['a', 'b', 'c', 'd']);
      }, 'start').then(function(steps) {
        steps.forEach(function(step) {
          promise.fulfilled(step)
          .then(function() {
            messages.push(step + '.1');
          }).then(function() {
            messages.push(step + '.2');
          });
        })
      });
      return waitForIdle().then(function() {
        assert.deepEqual(
            ['a.1', 'a.2', 'b.1', 'b.2', 'c.1', 'c.2', 'd.1', 'd.2'],
            messages);
      });
    });

    /** See https://github.com/SeleniumHQ/selenium/issues/444 */
    it('testMaintainsOrderWithPromiseChainsCreatedWithinAForeach_2', function() {
      var messages = [];
      flow.execute(function() {
        return promise.fulfilled(['a', 'b', 'c', 'd']);
      }, 'start').then(function(steps) {
        steps.forEach(function(step) {
          promise.fulfilled(step)
          .then(function() {
            messages.push(step + '.1');
          }).then(function() {
            flow.execute(function() {}, step + '.2').then(function() {
              messages.push(step + '.2');
            });
          });
        })
      });
      return waitForIdle().then(function() {
        assert.deepEqual(
            ['a.1', 'a.2', 'b.1', 'b.2', 'c.1', 'c.2', 'd.1', 'd.2'],
            messages);
      });
    });

    /** See https://github.com/SeleniumHQ/selenium/issues/444 */
    it('testMaintainsOrderWithPromiseChainsCreatedWithinAForeach_3', function() {
      var messages = [];
      flow.execute(function() {
        return promise.fulfilled(['a', 'b', 'c', 'd']);
      }, 'start').then(function(steps) {
        steps.forEach(function(step) {
          promise.fulfilled(step)
          .then(function(){})
          .then(function() {
            messages.push(step + '.1');
            return flow.execute(function() {}, step + '.1');
          }).then(function() {
            flow.execute(function() {}, step + '.2').then(function(text) {
              messages.push(step + '.2');
            });
          });
        })
      });
      return waitForIdle().then(function() {
        assert.deepEqual(
            ['a.1', 'a.2', 'b.1', 'b.2', 'c.1', 'c.2', 'd.1', 'd.2'],
            messages);
      });
    });

    /** See https://github.com/SeleniumHQ/selenium/issues/363 */
    it('testTasksScheduledInASeparateTurnOfTheEventLoopGetASeparateTaskQueue_2', function() {
      scheduleAction('a', () => promise.delayed(10));
      schedule('b');
      setTimeout(() => schedule('c'), 0);

      return waitForIdle().then(function() {
        assertFlowHistory('a', 'c', 'b');
      });
    });

    /** See https://github.com/SeleniumHQ/selenium/issues/363 */
    it('testTasksScheduledInASeparateTurnOfTheEventLoopGetASeparateTaskQueue_2', function() {
      scheduleAction('a', () => promise.delayed(10));
      schedule('b');
      schedule('c');
      setTimeout(function() {
        schedule('d');
        scheduleAction('e', () => promise.delayed(10));
        schedule('f');
      }, 0);

      return waitForIdle().then(function() {
        assertFlowHistory('a', 'd', 'e', 'b', 'c', 'f');
      });
    });

    /** See https://github.com/SeleniumHQ/selenium/issues/363 */
    it('testCanSynchronizeTasksFromAdjacentTaskQueues', function() {
      var task1 = scheduleAction('a', () => promise.delayed(10));
      schedule('b');
      setTimeout(function() {
        scheduleAction('c', () => task1);
        schedule('d');
      }, 0);

      return waitForIdle().then(function() {
        assertFlowHistory('a', 'c', 'd', 'b');
      });
    });

    describe('testCancellingAScheduledTask', function() {
      it('1', function() {
        var called = false;
        var task1 = scheduleAction('a', () => called = true);
        task1.cancel('no soup for you');

        return waitForIdle().then(function() {
          assert.ok(!called);
          assertFlowHistory();
          return task1.catch(function(e) {
            assert.ok(e instanceof promise.CancellationError);
            assert.equal('no soup for you', e.message);
          });
        });
      });

      it('2', function() {
        schedule('a');
        var called = false;
        var task2 = scheduleAction('b', () => called = true);
        schedule('c');

        task2.cancel('no soup for you');

        return waitForIdle().then(function() {
          assert.ok(!called);
          assertFlowHistory('a', 'c');
          return task2.catch(function(e) {
            assert.ok(e instanceof promise.CancellationError);
            assert.equal('no soup for you', e.message);
          });
        });
      });

      it('3', function() {
        var called = false;
        var task = scheduleAction('a', () => called = true);
        task.cancel(new StubError);

        return waitForIdle().then(function() {
          assert.ok(!called);
          assertFlowHistory();
          return task.catch(function(e) {
            assert.ok(e instanceof promise.CancellationError);
          });
        });
      });

      it('4', function() {
        var seen = [];
        var task = scheduleAction('a', () => seen.push(1))
            .then(() => seen.push(2))
            .then(() => seen.push(3))
            .then(() => seen.push(4))
            .then(() => seen.push(5));
        task.cancel(new StubError);

        return waitForIdle().then(function() {
          assert.deepEqual([], seen);
          assertFlowHistory();
          return task.catch(function(e) {
            assert.ok(e instanceof promise.CancellationError);
          });
        });
      });

      it('fromWithinAnExecutingTask', function() {
        var called = false;
        var task;
        scheduleAction('a', function() {
          task.cancel('no soup for you');
        });
        task = scheduleAction('b', () => called = true);
        schedule('c');

        return waitForIdle().then(function() {
          assert.ok(!called);
          assertFlowHistory('a', 'c');
          return task.catch(function(e) {
            assert.ok(e instanceof promise.CancellationError);
            assert.equal('no soup for you', e.message);
          });
        });
      });
    });

    it('testCancellingAPendingTask', function() {
      var order = [];
      var unresolved = promise.defer();

      var innerTask;
      var outerTask = scheduleAction('a', function() {
        order.push(1);

        // Schedule a task that will never finish.
        innerTask = scheduleAction('a.1', function() {
          return unresolved.promise;
        });

        // Since the outerTask is cancelled below, innerTask should be cancelled
        // with a DiscardedTaskError, which means its callbacks are silently
        // dropped - so this should never execute.
        innerTask.catch(function(e) {
          order.push(2);
        });
      });
      schedule('b');

      outerTask.catch(function(e) {
        order.push(3);
        assert.ok(e instanceof promise.CancellationError);
        assert.equal('no soup for you', e.message);
      });

      unresolved.promise.catch(function(e) {
        order.push(4);
        assert.ok(e instanceof promise.CancellationError);
      });

      return timeout(10).then(function() {
        assert.deepEqual([1], order);

        outerTask.cancel('no soup for you');
        return waitForIdle();
      }).then(function() {
        assertFlowHistory('a', 'a.1', 'b');
        assert.deepEqual([1, 3, 4], order);
      });
    });

    it('testCancellingAPendingPromiseCallback', function() {
      var called = false;

      var root = promise.fulfilled();
      root.then(function() {
        cb2.cancel('no soup for you');
      });

      var cb2 = root.then(fail, fail);  // These callbacks should never be called.
      cb2.then(fail, function(e) {
        called = true;
        assert.ok(e instanceof promise.CancellationError);
        assert.equal('no soup for you', e.message);
      });

      return waitForIdle().then(function() {
        assert.ok(called);
      });
    });

    describe('testResetFlow', function() {
      it('1', function() {
        var called = 0;
        var task = flow.execute(() => called++);
        task.finally(() => called++);

        return new Promise(function(fulfill) {
          flow.once('reset', fulfill);
          flow.reset();

        }).then(function() {
          assert.equal(0, called);
          return task;

        }).then(fail, function(e) {
          assert.ok(e instanceof promise.CancellationError);
          assert.equal('ControlFlow was reset', e.message);
        });
      });

      it('2', function() {
        var called = 0;
        var task1 = flow.execute(() => called++);
        task1.finally(() => called++);

        var task2 = flow.execute(() => called++);
        task2.finally(() => called++);

        var task3 = flow.execute(() => called++);
        task3.finally(() => called++);

        return new Promise(function(fulfill) {
          flow.once('reset', fulfill);
          flow.reset();

        }).then(function() {
          assert.equal(0, called);
        });
      });
    });

    describe('testPromiseFulfilledInsideTask', function() {
      it('1', function() {
        var order = [];

        flow.execute(function() {
          var d = promise.defer();

          d.promise.then(() => order.push('a'));
          d.promise.then(() => order.push('b'));
          d.promise.then(() => order.push('c'));
          d.fulfill();

          flow.execute(() => order.push('d'));

        }).then(() => order.push('fin'));

        return waitForIdle().then(function() {
          assert.deepEqual(['a', 'b', 'c', 'd', 'fin'], order);
        });
      });

      it('2', function() {
        var order = [];

        flow.execute(function() {
          flow.execute(() => order.push('a'));
          flow.execute(() => order.push('b'));

          var d = promise.defer();
          d.promise.then(() => order.push('c'));
          d.promise.then(() => order.push('d'));
          d.fulfill();

          flow.execute(() => order.push('e'));

        }).then(() => order.push('fin'));

        return waitForIdle().then(function() {
          assert.deepEqual(['a', 'b', 'c', 'd', 'e', 'fin'], order);
        });
      });

      it('3', function() {
        var order = [];
        var d = promise.defer();
        d.promise.then(() => order.push('c'));
        d.promise.then(() => order.push('d'));

        flow.execute(function() {
          flow.execute(() => order.push('a'));
          flow.execute(() => order.push('b'));

          d.promise.then(() => order.push('e'));
          d.fulfill();

          flow.execute(() => order.push('f'));

        }).then(() => order.push('fin'));

        return waitForIdle().then(function() {
          assert.deepEqual(['c', 'd', 'a', 'b', 'e', 'f', 'fin'], order);
        });
      });

      it('4', function() {
        var order = [];
        var d = promise.defer();
        d.promise.then(() => order.push('a'));
        d.promise.then(() => order.push('b'));

        flow.execute(function() {
          flow.execute(function() {
            order.push('c');
            flow.execute(() => order.push('d'));
            d.promise.then(() => order.push('e'));
          });
          flow.execute(() => order.push('f'));

          d.promise.then(() => order.push('g'));
          d.fulfill();

          flow.execute(() => order.push('h'));

        }).then(() => order.push('fin'));

        return waitForIdle().then(function() {
          assert.deepEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'fin'], order);
        });
      });
    });

    describe('testSettledPromiseCallbacksInsideATask', function() {
      it('1', function() {
        var order = [];
        var p = promise.fulfilled();

        flow.execute(function() {
          flow.execute(() => order.push('a'));
          p.then(() => order.push('b'));
          flow.execute(() => order.push('c'));
          p.then(() => order.push('d'));
        }).then(() => order.push('fin'));

        return waitForIdle().then(function() {
          assert.deepEqual(['a', 'b', 'c', 'd', 'fin'], order);
        });
      });

      it('2', function() {
        var order = [];

        flow.execute(function() {
          flow.execute(() => order.push('a'))
              .then(   () => order.push('c'));
          flow.execute(() => order.push('b'));
        }).then(() => order.push('fin'));

        return waitForIdle().then(function() {
          assert.deepEqual(['a', 'c', 'b', 'fin'], order);
        });
      });
    });

    it('testTasksDoNotWaitForNewlyCreatedPromises', function() {
      var order = [];

      flow.execute(function() {
        var d = promise.defer();

        // This is a normal promise, not a task, so the task for this callback is
        // considered volatile. Volatile tasks should be skipped when they reach
        // the front of the task queue.
        d.promise.then(() => order.push('a'));

        flow.execute(() => order.push('b'));
        flow.execute(function() {
          flow.execute(() => order.push('c'));
          d.promise.then(() => order.push('d'));
          d.fulfill();
        });
        flow.execute(() => order.push('e'));

      }).then(() => order.push('fin'));

      return waitForIdle().then(function() {
        assert.deepEqual(['b', 'a', 'c', 'd', 'e', 'fin'], order);
      });
    });

    it('testCallbackDependenciesDoNotDeadlock', function() {
      var order = [];
      var root = promise.defer();
      var dep = promise.fulfilled().then(function() {
        order.push('a');
        return root.promise.then(function() {
          order.push('b');
        });
      });
      // This callback depends on |dep|, which depends on another callback
      // attached to |root| via a chain.
      root.promise.then(function() {
        order.push('c');
        return dep.then(() => order.push('d'));
      }).then(() => order.push('fin'));

      setTimeout(() => root.fulfill(), 20);

      return waitForIdle().then(function() {
        assert.deepEqual(['a', 'b', 'c', 'd', 'fin'], order);
      });
    });
  });
});
