// 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.

goog.require('goog.testing.MockClock');
goog.require('goog.testing.jsunit');
goog.require('goog.userAgent');
goog.require('webdriver.promise');
goog.require('webdriver.stacktrace');
goog.require('webdriver.test.testutil');


// Aliases for readability.
var assertIsPromise = webdriver.test.testutil.assertIsPromise,
    assertNotPromise = webdriver.test.testutil.assertNotPromise,
    callbackHelper = webdriver.test.testutil.callbackHelper,
    callbackPair = webdriver.test.testutil.callbackPair,
    assertIsStubError = webdriver.test.testutil.assertIsStubError,
    throwStubError = webdriver.test.testutil.throwStubError,
    StubError = webdriver.test.testutil.StubError;

var app, clock, uncaughtExceptions;

function shouldRunTests() {
  return !goog.userAgent.IE || goog.userAgent.isVersionOrHigher(10);
}


function setUp() {
  webdriver.promise.LONG_STACK_TRACES = false;
  clock = new goog.testing.MockClock(true);
  uncaughtExceptions = [];

  app = webdriver.promise.controlFlow();
  app.on(webdriver.promise.ControlFlow.EventType.UNCAUGHT_EXCEPTION,
         goog.bind(uncaughtExceptions.push, uncaughtExceptions));
}


function tearDown() {
  clock.tick(Infinity);
  clock.dispose();
  app.reset();
  webdriver.promise.setDefaultFlow(new webdriver.promise.ControlFlow);
  assertArrayEquals(
      'Did not expect any uncaught exceptions', [], uncaughtExceptions);
  webdriver.promise.LONG_STACK_TRACES = false;
}


function createRejectedPromise(reason) {
  var p = webdriver.promise.rejected(reason);
  p.thenCatch(goog.nullFunction);
  return p;
}


function testCanDetectPromiseLikeObjects() {
  assertIsPromise(new webdriver.promise.Promise(function(fulfill) {
    fulfill();
  }));
  assertIsPromise(new webdriver.promise.Deferred());
  assertIsPromise(new webdriver.promise.Deferred().promise);
  assertIsPromise({then:function() {}});

  assertNotPromise(undefined);
  assertNotPromise(null);
  assertNotPromise('');
  assertNotPromise(true);
  assertNotPromise(false);
  assertNotPromise(1);
  assertNotPromise({});
  assertNotPromise({then:1});
  assertNotPromise({then:true});
  assertNotPromise({then:''});
}


function testSimpleResolveScenario() {
  var callback = callbackHelper(function(value) {
    assertEquals(123, value);
  });

  var deferred = webdriver.promise.defer();
  deferred.promise.then(callback);

  callback.assertNotCalled();
  deferred.fulfill(123);
  clock.tick();
  callback.assertCalled();
}


function testRegisteringACallbackPostResolution() {
  var callback, deferred = new webdriver.promise.Deferred();

  deferred.then((callback = callbackHelper(function(value) {
    assertEquals(123, value);
  })));
  deferred.fulfill(123);
  clock.tick();
  callback.assertCalled();

  deferred.then((callback = callbackHelper(function(value) {
    assertEquals(123, value);
  })));
  callback.assertNotCalled();
  clock.tick();
  callback.assertCalled();
}


function testRegisterACallbackViaDeferredPromise() {
  var callback, deferred = new webdriver.promise.Deferred();

  deferred.promise.then((callback = callbackHelper(function(value) {
    assertEquals(123, value);
  })));
  deferred.fulfill(123);
  clock.tick();
  callback.assertCalled();

  deferred.promise.then((callback = callbackHelper(function(value) {
    assertEquals(123, value);
  })));
  clock.tick();
  callback.assertCalled();
}


function testTwoStepResolvedChain() {
  var callback, start = new webdriver.promise.Deferred();

  var next = start.then((callback = callbackHelper(function(value) {
    assertEquals(123, value);
    return value + 1;
  })));

  assertIsPromise(next);

  callback.assertNotCalled();
  start.fulfill(123);
  clock.tick();
  callback.assertCalled();

  next.then((callback = callbackHelper(function(value) {
    assertEquals(124, value);
  })));
  clock.tick();
  callback.assertCalled();
}


function testCanResolveOnlyOnce_resolved() {
  var deferred = new webdriver.promise.Deferred();
  deferred.fulfill(1);
  deferred.fulfill(2);
  deferred.reject(3);

  var callback;
  deferred.then(callback = callbackHelper(function(value) {
    assertEquals(1, value);
  }));
  clock.tick();
  callback.assertCalled();
}


function testCanResolveOnlyOnce_rejected() {
  var deferred = new webdriver.promise.Deferred();
  deferred.reject(new StubError);
  deferred.fulfill(1);
  deferred.reject(2);

  var callback;
  deferred.then(null, callback = callbackHelper(assertIsStubError));
  clock.tick();
  callback.assertCalled();
}


function testIfFulfilledWithOtherPromiseCannotChangeValueWhileWaiting() {
  var deferred = webdriver.promise.defer();
  var other = webdriver.promise.defer();

  deferred.fulfill(other.promise);
  deferred.fulfill('different value');

  var callback = callbackHelper(function(value) {
    assertEquals(123, value);
  });

  deferred.then(callback);
  callback.assertNotCalled();

  other.fulfill(123);
  clock.tick();
  callback.assertCalled();
}


function testOnlyGoesDownListenerPath_resolved() {
  var callback = callbackHelper();
  var errback = callbackHelper();

  webdriver.promise.fulfilled().then(callback, errback);
  clock.tick();
  callback.assertCalled();
  errback.assertNotCalled();
}


function testOnlyGoesDownListenerPath_rejected() {
  var callback = callbackHelper();
  var errback = callbackHelper();

  webdriver.promise.rejected().then(callback, errback);
  clock.tick();
  callback.assertNotCalled();
  errback.assertCalled();
}


function testCatchingAndSuppressingRejectionErrors() {
  var errback = callbackHelper(assertIsStubError);
  var callback = callbackHelper(function() {
    assertUndefined(arguments[0]);
  });

  webdriver.promise.rejected(new StubError).
      thenCatch(errback).
      then(callback);
  clock.tick();
  errback.assertCalled();
  callback.assertCalled();
}


function testThrowingNewRejectionErrors() {
  var errback1 = callbackHelper(assertIsStubError);
  var error2 = Error('hi');
  var errback2 = callbackHelper(function(error) {
    assertEquals(error2, error);
  });

  webdriver.promise.rejected(new StubError).
      thenCatch(function(error) {
        errback1(error);
        throw error2;
      }).
      thenCatch(errback2);
  clock.tick();
  errback1.assertCalled();
  errback2.assertCalled();
}


function testThenFinally_nonFailingCallbackDoesNotSuppressOriginalError() {
  var done = callbackHelper(assertIsStubError);
  webdriver.promise.rejected(new StubError).
      thenFinally(goog.nullFunction).
      thenCatch(done);
  clock.tick();
  done.assertCalled();
}


function testThenFinally_failingCallbackSuppressesOriginalError() {
  var done = callbackHelper(assertIsStubError);
  webdriver.promise.rejected(new Error('original')).
      thenFinally(throwStubError).
      thenCatch(done);
  clock.tick();
  done.assertCalled();
}


function testThenFinally_callbackThrowsAfterFulfilledPromise() {
  var done = callbackHelper(assertIsStubError);
  webdriver.promise.fulfilled().
      thenFinally(throwStubError).
      thenCatch(done);
  clock.tick();
  done.assertCalled();
}


function testThenFinally_callbackReturnsRejectedPromise() {
  var done = callbackHelper(assertIsStubError);
  webdriver.promise.fulfilled().
      thenFinally(function() {
        return webdriver.promise.rejected(new StubError);
      }).
      thenCatch(done);
  clock.tick();
  done.assertCalled();
}


function testChainingThen_AllResolved() {
  var callbacks = [
    callbackHelper(function(value) {
      assertEquals(128, value);
      return value * 2;
    }),
    callbackHelper(function(value) {
      assertEquals(256, value);
      return value * 2;
    }),
    callbackHelper(function(value) {
      assertEquals(512, value);
    })
  ];

  var deferred = new webdriver.promise.Deferred();
  deferred.
      then(callbacks[0]).
      then(callbacks[1]).
      then(callbacks[2]);

  callbacks[0].assertNotCalled();
  callbacks[1].assertNotCalled();
  callbacks[2].assertNotCalled();

  deferred.fulfill(128);

  clock.tick();
  callbacks[0].assertCalled();
  callbacks[1].assertCalled();
  callbacks[2].assertCalled();
}


function testWhen_ReturnsAResolvedPromiseIfGivenANonPromiseValue() {
  var ret = webdriver.promise.when('abc');
  assertIsPromise(ret);

  var callback;
  ret.then(callback = callbackHelper(function (value) {
    assertEquals('abc', value);
  }));
  clock.tick();
  callback.assertCalled();
}


function testWhen_PassesRawErrorsToCallbacks() {
  var error = new Error('boo!'), callback;
  webdriver.promise.when(error, callback = callbackHelper(function(value) {
    assertEquals(error, value);
  }));
  clock.tick();
  callback.assertCalled();
}


function testWhen_WaitsForValueToBeResolvedBeforeInvokingCallback() {
  var d = new webdriver.promise.Deferred(), callback;
  webdriver.promise.when(d, callback = callbackHelper(function(value) {
    assertEquals('hi', value);
  }));
  callback.assertNotCalled();
  d.fulfill('hi');
  clock.tick();
  callback.assertCalled();
}


function testWhen_canCancelReturnedPromise() {
  var callbacks = callbackPair(null, function(e) {
    assertTrue(e instanceof webdriver.promise.CancellationError);
    assertEquals('just because', e.message);
  });

  var promiseLike = {
    then: function(cb, eb) {
      this.callback = cb;
      this.errback = eb;
    }
  };

  var promise = webdriver.promise.when(promiseLike,
      callbacks.callback, callbacks.errback);

  assertTrue(promise.isPending());
  promise.cancel('just because');
  clock.tick();
  callbacks.assertErrback();

  // The following should have no effect.
  promiseLike.callback();
  promiseLike.errback();
}


function testFiresUncaughtExceptionEventIfRejectionNeverHandled() {
  webdriver.promise.rejected(new StubError);
  var handler = callbackHelper(assertIsStubError);

  // so tearDown() doesn't throw
  app.removeAllListeners();
  app.on(webdriver.promise.ControlFlow.EventType.UNCAUGHT_EXCEPTION, handler);
  clock.tick();
  handler.assertCalled();
}


function testWaitsIfCallbackReturnsAPromiseObject() {
  var callback1, callback2;
  var callback1Return = new webdriver.promise.Deferred();

  webdriver.promise.fulfilled('hi').
      then(callback1 = callbackHelper(function(value) {
        assertEquals('hi', value);
        return callback1Return;
      })).
      then(callback2 = callbackHelper(function(value) {
        assertEquals('bye', value);
      }));

  clock.tick();
  callback1.assertCalled();
  callback2.assertNotCalled();
  callback1Return.fulfill('bye');
  clock.tick();
  callback2.assertCalled();
}


function testWaitsIfCallbackReturnsAPromiseLikeObject() {
  var callback1, callback2;
  var callback1Return = {
    then: function(callback) {
      this.callback = callback;
    },
    fulfill: function(value) {
      this.callback(value);
    }
  };

  webdriver.promise.fulfilled('hi').
      then(callback1 = callbackHelper(function(value) {
        assertEquals('hi', value);
        return callback1Return;
      })).
      then(callback2 = callbackHelper(function(value) {
        assertEquals('bye', value);
      }));

  clock.tick();
  callback1.assertCalled();
  callback2.assertNotCalled();
  callback1Return.fulfill('bye');
  clock.tick();
  callback2.assertCalled();
}


function testResolvingAPromiseWithAnotherPromiseCreatesAChain_ourPromise() {
  var d1 = new webdriver.promise.Deferred();
  var d2 = new webdriver.promise.Deferred();
  var callback1, callback2;

  d1.then(callback1 = callbackHelper(function(value) {
    assertEquals(4, value);
  }));

  var d2promise = d2.then(callback2 = callbackHelper(function(value) {
    assertEquals(2, value);
    return value * 2;
  }));

  callback1.assertNotCalled();
  callback2.assertNotCalled();

  d2.fulfill(2);
  clock.tick();
  callback1.assertNotCalled();
  callback2.assertCalled();

  d1.fulfill(d2promise);
  clock.tick();
  callback1.assertCalled();
  callback2.assertCalled();
}


function testResolvingAPromiseWithAnotherPromiseCreatesAChain_otherPromise() {
  var d = new webdriver.promise.Deferred(), callback;
  d.then(callback = callbackHelper(function(value) {
    assertEquals(4, value);
  }));

  var otherPromise = {
    then: function(callback) {
      this.callback = callback;
    },
    fulfill: function(value) {
      this.callback(value);
    }
  };

  callback.assertNotCalled();
  d.fulfill(otherPromise);
  otherPromise.fulfill(4);
  clock.tick();
  callback.assertCalled();
}


function testRejectForcesValueToAnError_errorInstance() {
  var d = webdriver.promise.defer();
  var callback = callbackHelper(assertIsStubError);

  d.thenCatch(callback);
  d.reject(new StubError);
  clock.tick();
  callback.assertCalled();
}


function testRejectForcesValueToAnError_errorSubTypeInstance() {
  var d = webdriver.promise.defer();
  var e = new TypeError('hi');
  var callback = callbackHelper(function(actual) {
    assertEquals(e, actual);
  });

  d.thenCatch(callback);
  d.reject(e);
  clock.tick();
  callback.assertCalled();
}


function testRejectForcesValueToAnError_customErrorInstance() {
  var d = webdriver.promise.defer();
  var e = new goog.debug.Error('hi there');
  var callback = callbackHelper(function(actual) {
    assertEquals(e, actual);
  });

  d.thenCatch(callback);
  d.reject(e);
  clock.tick();
  callback.assertCalled();
}


function testRejectForcesValueToAnError_errorLike() {
  var d = webdriver.promise.defer();
  var e = {message: 'yolo'};
  var callback = callbackHelper(function(actual) {
    assertEquals(e, actual);
  });

  d.thenCatch(callback);
  d.reject(e);
  clock.tick();
  callback.assertCalled();
}


function testRejectingAPromiseWithAnotherPromise_ourPromise() {
  var d1 = new webdriver.promise.Deferred();
  var d2 = new webdriver.promise.Deferred();
  var pair1 = callbackPair(assertIsStubError, null);
  var pair2 = callbackPair(function(value) {
    assertEquals(2, value);
    return new StubError;
  });

  d1.then(fail, function(e) {
    assertEquals(d2promise, e);
    return d2promise;
  }).then(pair1.callback, pair1.errback);

  var d2promise = d2.then(pair2.callback, pair2.errback);

  pair1.assertNeither();
  pair2.assertNeither();

  d2.fulfill(2);
  clock.tick();
  pair1.assertNeither();
  pair2.assertCallback();

  d1.reject(d2promise);
  clock.tick();
  pair1.assertCallback();
}


function testRejectingAPromiseWithAnotherPromise_otherPromise() {
  var otherPromise = {
    then: function(callback) {
      this.callback = callback;
    },
    fulfill: function(value) {
      this.callback(value);
    }
  };

  var pair = callbackPair(null, assertIsStubError);
  var d = new webdriver.promise.Deferred();
  d.promise.then(fail, function(e) {
    assertEquals(otherPromise, e);
    return otherPromise;
  }).then(pair.callback, pair.errback);

  d.reject(otherPromise);
  clock.tick();
  pair.assertNeither();

  otherPromise.fulfill(new StubError);
  clock.tick();
  pair.assertCallback();
}


function testResolvingADeferredWithAnotherCopiesTheResolvedValue() {
  var d1 = new webdriver.promise.Deferred();
  var d2 = new webdriver.promise.Deferred();
  var callback1, callback2;

  d1.then(callback1 = callbackHelper(function(value) {
    assertEquals(2, value);
  }));

  d2.then(callback2 = callbackHelper(function(value) {
    assertEquals(2, value);
    return 4;
  }));

  d1.fulfill(d2);
  clock.tick();
  callback1.assertNotCalled();
  callback2.assertNotCalled();

  d2.fulfill(2);
  clock.tick();
  callback1.assertCalled();
  callback2.assertCalled();
}


function testCannotResolveAPromiseWithItself_fulfill() {
  var pair = callbackPair(null, function(e) {
    assertTrue(e instanceof TypeError);
  });

  var f, p = new webdriver.promise.Promise(function(fulfill) {
    f = fulfill;
  });
  p.then(pair.callback, pair.errback);
  f(p);
  clock.tick();
  pair.assertErrback();
}


function testCannotResolveAPromiseWithItself_reject() {
  var pair = callbackPair(null, function(e) {
    assertTrue(e instanceof TypeError);
  });

  var r, p = new webdriver.promise.Promise(function(_, reject) {
    r = reject;
  });
  p.then(pair.callback, pair.errback);
  r(p);
  clock.tick();
  pair.assertErrback();
}


function testCannotResolveADeferredWithItself() {
  var deferred = new webdriver.promise.Deferred();
  assertThrows(goog.bind(deferred.fulfill, deferred, deferred));
  assertThrows(goog.bind(deferred.reject, deferred, deferred));
}


function testSkipsNullPointsInPromiseChain_callbacks() {
  var errback1, errback2, callback;
  webdriver.promise.fulfilled('hi').
      thenCatch(errback1 = callbackHelper()).
      thenCatch(errback2 = callbackHelper()).
      then(callback = callbackHelper(function(value) {
        assertEquals('hi', value);
      }));

  clock.tick();
  errback1.assertNotCalled();
  errback2.assertNotCalled();
  callback.assertCalled();
}


function testSkipsNullPointsInPromiseChain_errbacks() {
  var errback1, errback2, callback;
  webdriver.promise.fulfilled('hi').
      thenCatch(errback1 = callbackHelper()).
      thenCatch(errback2 = callbackHelper()).
      then(callback = callbackHelper(function(value) {
        assertEquals('hi', value);
      }));

  clock.tick();
  errback1.assertNotCalled();
  errback2.assertNotCalled();
  callback.assertCalled();
}


function testFullyResolved_primitives() {
  function runTest(value) {
    var callback, errback;
    webdriver.promise.fullyResolved(value).then(
        callback = callbackHelper(function(resolved) {
          assertEquals(value, resolved);
        }),
        errback = callbackHelper());

    clock.tick();
    errback.assertNotCalled(
        'Did not expect errback to be called for: ' + value);
    callback.assertCalled('Expected callback to be called for: ' + value);
  }

  runTest(true);
  runTest(goog.nullFunction);
  runTest(null);
  runTest(123);
  runTest('foo bar');
  runTest(undefined);
}


function testFullyResolved_arrayOfPrimitives() {
  var array = [true, goog.nullFunction, null, 123, '', undefined, 1];
  var callbacks = callbackPair(function(resolved) {
    assertEquals(array, resolved);
    assertArrayEquals([true, goog.nullFunction, null, 123, '', undefined, 1],
        resolved);
  });

  webdriver.promise.fullyResolved(array).then(
      callbacks.callback, callbacks.errback);

  clock.tick();
  callbacks.assertCallback();
}

function testFullyResolved_nestedArrayOfPrimitives() {
  var array = [true, [goog.nullFunction, null, 123], '', undefined];
  var callback, errback;
  webdriver.promise.fullyResolved(array).then(
      callback = callbackHelper(function(resolved) {
        assertEquals(array, resolved);
        assertArrayEquals([true, [goog.nullFunction, null, 123], '', undefined],
            resolved);
        assertArrayEquals([goog.nullFunction, null, 123], resolved[1]);
      }),
      errback = callbackHelper());

  clock.tick();
  errback.assertNotCalled();
  callback.assertCalled();
}


function testFullyResolved_arrayWithPromisedPrimitive() {
  var callback, errback;
  webdriver.promise.fullyResolved([webdriver.promise.fulfilled(123)]).then(
      callback = callbackHelper(function(resolved) {
        assertArrayEquals([123], resolved);
      }),
      errback = callbackHelper());

  clock.tick();
  errback.assertNotCalled();
  callback.assertCalled();
}


function testFullyResolved_promiseResolvesToPrimitive() {
  var promise = webdriver.promise.fulfilled(123);
  var callback, errback;
  webdriver.promise.fullyResolved(promise).then(
      callback = callbackHelper(function(resolved) {
        assertEquals(123, resolved);
      }),
      errback = callbackHelper());

  clock.tick();
  errback.assertNotCalled();
  callback.assertCalled();
}


function testFullyResolved_promiseResolvesToArray() {
  var array = [true, [goog.nullFunction, null, 123], '', undefined];
  var promise = webdriver.promise.fulfilled(array);
  var callback, errback;

  var result = webdriver.promise.fullyResolved(promise);
  result.then(
      callback = callbackHelper(function(resolved) {
        assertEquals(array, resolved);
        assertArrayEquals([true, [goog.nullFunction, null, 123], '', undefined],
            resolved);
        assertArrayEquals([goog.nullFunction, null, 123], resolved[1]);
      }),
      errback = callbackHelper());

  clock.tick();
  errback.assertNotCalled();
  callback.assertCalled();
}


function testFullyResolved_promiseResolvesToArrayWithPromises() {
  var nestedPromise = webdriver.promise.fulfilled(123);
  var promise = webdriver.promise.fulfilled([true, nestedPromise]);

  var callback, errback;
  webdriver.promise.fullyResolved(promise).then(
      callback = callbackHelper(function(resolved) {
        assertArrayEquals([true, 123], resolved);
      }),
      errback = callbackHelper());

  clock.tick();
  errback.assertNotCalled();
  callback.assertCalled();
}


function testFullyResolved_rejectsIfArrayPromiseRejects() {
  var nestedPromise = createRejectedPromise(new StubError);
  var promise = webdriver.promise.fulfilled([true, nestedPromise]);

  var pair = callbackPair(null, assertIsStubError);
  webdriver.promise.fullyResolved(promise).then(pair.callback, pair.errback);

  clock.tick();
  pair.assertErrback();
}


function testFullyResolved_rejectsOnFirstArrayRejection() {
  var e1 = new Error('foo');
  var e2 = new Error('bar');
  var promise = webdriver.promise.fulfilled([
    createRejectedPromise(e1),
    createRejectedPromise(e2)
  ]);

  var pair = callbackPair(null, function(error) {
    assertEquals(e1, error);
  });
  webdriver.promise.fullyResolved(promise).then(pair.callback, pair.errback);

  clock.tick();
  pair.assertErrback();
}


function testFullyResolved_rejectsIfNestedArrayPromiseRejects() {
  var promise = webdriver.promise.fulfilled([
    webdriver.promise.fulfilled([
      createRejectedPromise(new StubError)
    ])
  ]);

  var pair = callbackPair(null, assertIsStubError);
  webdriver.promise.fullyResolved(promise).then(pair.callback, pair.errback);

  clock.tick();
  pair.assertErrback();
}


function testFullyResolved_simpleHash() {
  var hash = {'a': 123};

  var callback, errback;
  webdriver.promise.fullyResolved(hash).then(
      callback = callbackHelper(function(resolved) {
        assertEquals(hash, resolved);
        webdriver.test.testutil.assertObjectEquals({'a': 123}, resolved);
      }),
      errback = callbackHelper());

  clock.tick();
  errback.assertNotCalled();
  callback.assertCalled();
}


function testFullyResolved_nestedHash() {
  var nestedHash = {'foo':'bar'};
  var hash = {'a': 123, 'b': nestedHash};

  var callback, errback;
  webdriver.promise.fullyResolved(hash).then(
      callback = callbackHelper(function(resolved) {
        assertEquals(hash, resolved);
        webdriver.test.testutil.assertObjectEquals(
            {'a': 123, 'b': {'foo': 'bar'}}, resolved);
        webdriver.test.testutil.assertObjectEquals(nestedHash, resolved['b']);
      }),
      errback = callbackHelper());

  clock.tick();
  errback.assertNotCalled();
  callback.assertCalled();
}


function testFullyResolved_promiseResolvesToSimpleHash() {
  var hash = {'a': 123};
  var promise = webdriver.promise.fulfilled(hash);

  var callback, errback;
  webdriver.promise.fullyResolved(promise).then(
      callback = callbackHelper(function(resolved) {
        webdriver.test.testutil.assertObjectEquals(hash, resolved);
      }),
      errback = callbackHelper());

  clock.tick();
  errback.assertNotCalled();
  callback.assertCalled();
}


function testFullyResolved_promiseResolvesToNestedHash() {
  var nestedHash = {'foo':'bar'};
  var hash = {'a': 123, 'b': nestedHash};
  var promise = webdriver.promise.fulfilled(hash);

  var callback, errback;
  webdriver.promise.fullyResolved(promise).then(
      callback = callbackHelper(function(resolved) {
        webdriver.test.testutil.assertObjectEquals(hash, resolved);
        webdriver.test.testutil.assertObjectEquals(nestedHash, resolved['b']);
      }),
      errback = callbackHelper());

  clock.tick();
  errback.assertNotCalled();
  callback.assertCalled();
}


function testFullyResolved_promiseResolvesToHashWithPromises() {
  var promise = webdriver.promise.fulfilled({
      'a': webdriver.promise.fulfilled(123)
  });

  var callback, errback;
  webdriver.promise.fullyResolved(promise).then(
      callback = callbackHelper(function(resolved) {
        webdriver.test.testutil.assertObjectEquals({'a': 123}, resolved);
      }),
      errback = callbackHelper());

  clock.tick();
  errback.assertNotCalled();
  callback.assertCalled();
}


function testFullyResolved_rejectsIfHashPromiseRejects() {
  var promise = webdriver.promise.fulfilled({
      'a': createRejectedPromise(new StubError)
  });

  var pair = callbackPair(null, assertIsStubError);
  webdriver.promise.fullyResolved(promise).then(
    pair.callback, pair.errback);

  clock.tick();
  pair.assertErrback();
}

function testFullyResolved_rejectsIfNestedHashPromiseRejects() {
  var promise = webdriver.promise.fulfilled({
      'a': {'b': createRejectedPromise(new StubError)}
  });

  var pair = callbackPair(null, assertIsStubError);
  webdriver.promise.fullyResolved(promise).then(pair.callback, pair.errback);

  clock.tick();
  pair.assertErrback();
}


function testFullyResolved_instantiatedObject() {
  function Foo() {
    this.bar = 'baz';
  }
  var foo = new Foo;

  var callback, errback;
  webdriver.promise.fullyResolved(foo).then(
    callback = callbackHelper(function(resolvedFoo) {
      assertEquals(foo, resolvedFoo);
      assertTrue(resolvedFoo instanceof Foo);
      webdriver.test.testutil.assertObjectEquals(new Foo, resolvedFoo);
    }),
    errback = callbackHelper());

  clock.tick();
  errback.assertNotCalled();
  callback.assertCalled();
}


function testFullyResolved_withEmptyArray() {
  var callback, errback;
  webdriver.promise.fullyResolved([]).then(
    callback = callbackHelper(function(resolved) {
      assertArrayEquals([], resolved);
    }),
    errback = callbackHelper());

  clock.tick();
  errback.assertNotCalled();
  callback.assertCalled();
}


function testFullyResolved_withEmptyHash() {
  var callback, errback;
  webdriver.promise.fullyResolved({}).then(
    callback = callbackHelper(function(resolved) {
      webdriver.test.testutil.assertObjectEquals({}, resolved);
    }),
    errback = callbackHelper());

  clock.tick();
  errback.assertNotCalled();
  callback.assertCalled();
}


function testFullyResolved_arrayWithPromisedHash() {
  var obj = {'foo': 'bar'};
  var promise = webdriver.promise.fulfilled(obj);
  var array = [promise];

  var callback, errback;
  webdriver.promise.fullyResolved(array).then(
      callback = callbackHelper(function(resolved) {
        webdriver.test.testutil.assertObjectEquals(resolved, [obj]);
      }),
      errback = callbackHelper());


  clock.tick();
  errback.assertNotCalled();
  callback.assertCalled();
}


function testFullyResolved_aDomElement() {
  var e = document.createElement('div');
  var callbacks = callbackPair(function(resolved) {
    assertEquals(e, resolved);
  });

  webdriver.promise.fullyResolved(e).
      then(callbacks.callback, callbacks.errback);

  clock.tick();
  callbacks.assertCallback();
}


function testCallbackChain_nonSplit() {
  var stage1 = callbackPair(),
      stage2 = callbackPair(),
      stage3 = callbackPair();

  webdriver.promise.rejected('foo').
      then(stage1.callback, stage1.errback).
      then(stage2.callback, stage2.errback).
      then(stage3.callback, stage3.errback);

  clock.tick();
  stage1.assertErrback('Wrong function for stage 1');
  stage2.assertCallback('Wrong function for stage 2');
  stage3.assertCallback('Wrong function for final stage');
}


function testCallbackChain_split() {
  var stage1 = callbackPair(),
      stage2 = callbackPair(),
      stage3 = callbackPair();

  webdriver.promise.rejected('foo').
      then(stage1.callback).
      thenCatch(stage1.errback).
      then(stage2.callback).
      thenCatch(stage2.errback).
      then(stage3.callback, stage3.errback);

  clock.tick();
  stage1.assertErrback('Wrong function for stage 1');
  stage2.assertCallback('Wrong function for stage 2');
  stage3.assertCallback('Wrong function for final stage');
}


function testCheckedNodeCall_functionThrows() {
  var error = new Error('boom');
  var pair = callbackPair(null, function(e) {
    assertEquals(error, e);
  });

  webdriver.promise.checkedNodeCall(function() {
    throw error;
  }).then(pair.callback, pair.errback);

  clock.tick();
  pair.assertErrback();
}


function testCheckedNodeCall_functionReturnsAnError() {
  var error = new Error('boom');
  var pair = callbackPair(null, function(e) {
    assertEquals(error, e);
  });
  webdriver.promise.checkedNodeCall(function(callback) {
    callback(error);
  }).then(pair.callback, pair.errback);
  clock.tick();
  pair.assertErrback();
}


function testCheckedNodeCall_functionReturnsSuccess() {
  var success = 'success!';
  var pair = callbackPair(function(value) {
    assertEquals(success, value);
  });
  webdriver.promise.checkedNodeCall(function(callback) {
    callback(null, success);
  }).then(pair.callback, pair.errback);
  clock.tick();
  pair.assertCallback();
}


function testCheckedNodeCall_functionReturnsAndThrows() {
  var error = new Error('boom');
  var error2 = new Error('boom again');
  var pair = callbackPair(null, function(e) {
    assertEquals(error, e);
  });
  webdriver.promise.checkedNodeCall(function(callback) {
    callback(error);
    throw error2;
  }).then(pair.callback, pair.errback);
  clock.tick();
  pair.assertErrback();
}


function testCheckedNodeCall_functionThrowsAndReturns() {
  var error = new Error('boom');
  var error2 = new Error('boom again');
  var pair = callbackPair(null, function(e) {
    assertEquals(error2, e);
  });
  webdriver.promise.checkedNodeCall(function(callback) {
    setTimeout(goog.partial(callback, error), 10);
    throw error2;
  }).then(pair.callback, pair.errback);
  clock.tick();
  pair.assertErrback();
  pair.reset();
  clock.tick(Infinity);
  pair.assertNeither();
}


function testCancel_passesTheCancellationReasonToReject() {
  var pair = callbackPair(null, function(e) {
    assertTrue(e instanceof webdriver.promise.CancellationError);
    assertEquals('because i said so', e.message);
  });
  var d = new webdriver.promise.Deferred();
  d.then(pair.callback, pair.errback);
  d.cancel('because i said so');
  clock.tick();
  pair.assertErrback();
}


function testCancel_canCancelADeferredFromAChainedPromise() {
  var pair1 = callbackPair(null, function(e) {
    assertTrue(e instanceof webdriver.promise.CancellationError);
    assertEquals('because i said so', e.message);
  });
  var pair2 = callbackPair();

  var d = new webdriver.promise.Deferred();
  var p = d.then(pair1.callback, pair1.errback);
  p.then(pair2.callback, pair2.errback);

  p.cancel('because i said so');
  clock.tick();
  pair1.assertErrback('The first errback should have fired.');
  pair2.assertCallback();
}


function testCancel_canCancelATimeout() {
  var pair = callbackPair(null, function(e) {
    assertTrue(e instanceof webdriver.promise.CancellationError);
  });
  var p = webdriver.promise.delayed(250).
      then(pair.callback, pair.errback);
  p.cancel();
  clock.tick();
  pair.assertErrback();
  clock.tick(250);  // Just to make sure nothing happens.
  pair.assertErrback();
}


function testCancel_cancelIsANoopOnceAPromiseHasBeenFulfilled() {
  var p = webdriver.promise.fulfilled(123);
  p.cancel();

  var pair = callbackPair(goog.partial(assertEquals, 123));
  p.then(pair.callback, pair.errback);
  clock.tick();
  pair.assertCallback();
}


function testCancel_cancelIsANoopOnceAPromiseHasBeenRejected() {
  var p = webdriver.promise.rejected(new StubError);
  p.cancel();

  var pair = callbackPair(null, assertIsStubError);
  p.then(pair.callback, pair.errback);
  clock.tick();
  pair.assertErrback();
}


function testCancel_noopCancelTriggeredOnCallbackOfResolvedPromise() {
  var d = webdriver.promise.defer();
  var p = d.promise.then();

  d.fulfill();
  p.cancel();  // This should not throw.
}


function testCallbackRegistersAnotherListener_callbacksConfiguredPreResolve() {
  var messages = [];
  var d = new webdriver.promise.Deferred();
  d.promise.then(function() {
    messages.push('a');
    d.promise.then(function() {
      messages.push('c');
    });
  });
  d.promise.then(function() {
    messages.push('b');
  });
  d.fulfill();
  clock.tick();
  assertArrayEquals(['a', 'c', 'b'], messages);
}


function testCallbackRegistersAnotherListener_callbacksConfiguredPostResolve() {
  var messages = [];
  var p = webdriver.promise.fulfilled();
  p.then(function() {
    messages.push('a');
    p.then(function() {
      messages.push('c');
    });
  });
  p.then(function() {
    messages.push('b');
  });
  clock.tick();
  assertArrayEquals(['a', 'c', 'b'], messages);
}


function testCallbackRegistersAnotherListener_recursive() {
  var order = [];
  var promise = webdriver.promise.fulfilled();

  promise.then(function() {
    push();
    promise.then(push);
  }).then(function() {
    push();
  });

  assertArrayEquals([], order);
  clock.tick();
  assertArrayEquals([0, 1, 2], order);

  function push() {
    order.push(order.length);
  }
}


function testCallbackRegistersAnotherListener_recursiveCallbacks_many() {
  var messages = [];
  var start = 97;  // 'a'

  var p = webdriver.promise.fulfilled();
  p.then(push).then(function() {
    messages.push('done');
  });

  function push() {
    messages.push(String.fromCharCode(start++));
    if (start != 101) {  // 'd'
      p.then(push);
    }
  }

  clock.tick();
  assertArrayEquals(['a', 'b', 'c', 'd', 'done'], messages);
}


function testThenReturnsOwnPromiseIfNoCallbacksWereGiven() {
  var deferred = new webdriver.promise.Deferred();
  assertEquals(deferred.promise, deferred.promise.then());
  assertEquals(deferred.promise, webdriver.promise.when(deferred.promise));
}


function testIsStillConsideredUnHandledIfNoCallbacksWereGivenOnCallsToThen() {
  webdriver.promise.rejected(new StubError).then();
  var handler = callbackHelper(assertIsStubError);

  // so tearDown() doesn't throw
  app.removeAllListeners();
  app.on(webdriver.promise.ControlFlow.EventType.UNCAUGHT_EXCEPTION, handler);
  clock.tick();
  handler.assertCalled();
}

function testResolvedReturnsInputValueIfItIsAPromise() {
  var input = webdriver.promise.fulfilled('a');
  var output = webdriver.promise.fulfilled(input);
  assertEquals(input, output);
}


function testADeferredsParentControlFlowIsActiveForCallbacks() {
  var defaultFlow = webdriver.promise.controlFlow();

  var flow1 = new webdriver.promise.ControlFlow();
  var d = new webdriver.promise.Deferred(flow1);
  d.fulfill();

  var flow2 = new webdriver.promise.ControlFlow();
  var d2 = new webdriver.promise.Deferred(flow2);
  d2.fulfill();

  assertIsFlow(defaultFlow);

  var callbacks = callbackPair();
  d.promise.then(assertIsFlow(flow1)).
      then(assertIsFlow(flow1)).
      then(function() {
        return d2.promise.then(assertIsFlow(flow2));
      }).
      then(assertIsFlow(flow1)).
      then(callbacks.callback, callbacks.errback);

  clock.tick();
  callbacks.assertCallback();
  assertIsFlow(defaultFlow);

  function assertIsFlow(flow) {
    return function() {
      assertEquals(flow, webdriver.promise.controlFlow());
    };
  }
}


function testPromiseAll_emptyArray() {
  var pair = callbackPair(function(value) {
    assertArrayEquals([], value);
  });

  webdriver.promise.all([]).then(pair.callback, pair.errback);
  clock.tick();
  pair.assertCallback();
}


function testPromiseAll() {
  var a = [
      0, 1,
      webdriver.promise.defer(),
      webdriver.promise.defer(),
      4, 5, 6
  ];
  delete a[5];

  var pair = callbackPair(function(value) {
    var expected = [0, 1, 2, 3, 4, 5, 6];
    delete expected[5];
    assertArrayEquals(expected, value);
  });

  webdriver.promise.all(a).then(pair.callback, pair.errback);
  pair.assertNeither();

  a[2].fulfill(2);
  pair.assertNeither();

  a[3].fulfill(3);
  clock.tick();
  pair.assertCallback();
}


function testPromiseAll_usesFirstRejection() {
  var a = [
    webdriver.promise.defer(),
    webdriver.promise.defer()
  ];

  var pair = callbackPair(null, assertIsStubError);

  webdriver.promise.all(a).then(pair.callback, pair.errback);
  pair.assertNeither();

  a[1].reject(new StubError);
  clock.tick();
  pair.assertErrback();

  a[0].reject(Error('ignored'));
  clock.tick(Infinity);
}


function testMappingAnArray() {
  var a = [1, 2, 3];
  var result = webdriver.promise.map(a, function(value, index, a2) {
    assertEquals(a, a2);
    assertEquals('not a number', 'number', typeof index);
    return value + 1;
  });

  var pair = callbackPair(function(value) {
    assertArrayEquals([2, 3, 4], value);
  });

  result.then(pair.callback, pair.errback);
  clock.tick();
  pair.assertCallback();
}


function testMappingAnArray_omitsDeleted() {
  var a = [0, 1, 2, 3, 4, 5, 6];
  delete a[1];
  delete a[3];
  delete a[4];
  delete a[6];

  var result = webdriver.promise.map(a, function(value) {
    return value * value;
  });

  var expected = [0, 1, 4, 9, 16, 25, 36];
  delete expected[1];
  delete expected[3];
  delete expected[4];
  delete expected[6];

  var pair = callbackPair(function(value) {
    assertArrayEquals(expected, value);
  });

  result.then(pair.callback, pair.errback);
  clock.tick();
  pair.assertCallback();
}


function testMappingAnArray_emptyArray() {
  var result = webdriver.promise.map([], function(value) {
    return value + 1;
  });

  var pair = callbackPair(function(value) {
    assertArrayEquals([], value);
  });

  result.then(pair.callback, pair.errback);
  clock.tick();
  pair.assertCallback();
}


function testMappingAnArray_inputIsPromise() {
  var input = webdriver.promise.defer();
  var result = webdriver.promise.map(input, function(value) {
    return value + 1;
  });

  var pair = callbackPair(function(value) {
    assertArrayEquals([2, 3, 4], value);
  });

  result.then(pair.callback, pair.errback);
  pair.assertNeither();
  input.fulfill([1, 2, 3]);
  clock.tick();
  pair.assertCallback();
}


function testMappingAnArray_waitsForFunctionResultToResolve() {
  var innerResults = [
    webdriver.promise.defer(),
    webdriver.promise.defer()
  ];

  var result = webdriver.promise.map([1, 2], function(value, index) {
    return innerResults[index].promise;
  });

  var pair = callbackPair(function(value) {
    assertArrayEquals(['a', 'b'], value);
  });

  result.then(pair.callback, pair.errback);
  pair.assertNeither();

  innerResults[0].fulfill('a');
  clock.tick();
  pair.assertNeither();

  innerResults[1].fulfill('b');
  clock.tick();
  pair.assertCallback();
}


function testMappingAnArray_rejectsPromiseIfFunctionThrows() {
  var result = webdriver.promise.map([1], throwStubError);
  var pair = callbackPair(null, assertIsStubError);
  result.then(pair.callback, pair.errback);
  clock.tick();
  pair.assertErrback();
}


function testMappingAnArray_rejectsPromiseIfFunctionReturnsRejectedPromise() {
  var result = webdriver.promise.map([1], function() {
    return webdriver.promise.rejected(new StubError);
  });

  var pair = callbackPair(null, assertIsStubError);
  result.then(pair.callback, pair.errback);
  clock.tick();
  pair.assertErrback();
}


function testMappingAnArray_stopsCallingFunctionIfPreviousIterationFailed() {
  var count = 0;
  var result = webdriver.promise.map([1, 2, 3, 4], function() {
    count++;
    if (count == 3) {
      throw new StubError;
    }
  });

  var pair = callbackPair(null, assertIsStubError);
  result.then(pair.callback, pair.errback);
  clock.tick();
  pair.assertErrback();
  assertEquals(3, count);
}


function testMappingAnArray_rejectsWithFirstRejectedPromise() {
  var innerResult = [
      webdriver.promise.fulfilled(),
      createRejectedPromise(new StubError),
      createRejectedPromise(Error('should be ignored'))
  ];
  var count = 0;
  var result = webdriver.promise.map([1, 2, 3, 4], function(value, index) {
    count += 1;
    return innerResult[index];
  });

  var pair = callbackPair(null, assertIsStubError);
  result.then(pair.callback, pair.errback);

  clock.tick();
  pair.assertErrback();
  assertEquals(2, count);
}


function testMappingAnArray_preservesOrderWhenMapReturnsPromise() {
  var deferreds = [
    webdriver.promise.defer(),
    webdriver.promise.defer(),
    webdriver.promise.defer(),
    webdriver.promise.defer()
  ];
  var result = webdriver.promise.map(deferreds, function(value) {
    return value.promise;
  });

  var pair = callbackPair(function(value) {
    assertArrayEquals([0, 1, 2, 3], value);
  });
  result.then(pair.callback, pair.errback);
  clock.tick();
  pair.assertNeither();

  goog.array.forEachRight(deferreds, function(d, i) {
    d.fulfill(i);
  });
  clock.tick();
  pair.assertCallback();
}


function testFilteringAnArray() {
  var a = [0, 1, 2, 3];
  var result = webdriver.promise.filter(a, function(val, index, a2) {
    assertEquals(a, a2);
    assertEquals('not a number', 'number', typeof index);
    return val > 1;
  });

  var pair = callbackPair(function(val) {
    assertArrayEquals([2, 3], val);
  });
  result.then(pair.callback, pair.errback);
  clock.tick();
  pair.assertCallback();
}


function testFilteringAnArray_omitsDeleted() {
  var a = [0, 1, 2, 3, 4, 5, 6];
  delete a[3];
  delete a[4];

  var result = webdriver.promise.filter(a, function(value) {
    return value > 1 && value < 6;
  });

  var pair = callbackPair(function(val) {
    assertArrayEquals([2, 5], val);
  });
  result.then(pair.callback, pair.errback);
  clock.tick();
  pair.assertCallback();
}


function testFilteringAnArray_preservesInputs() {
  var a = [0, 1, 2, 3];

  var result = webdriver.promise.filter(a, function(value, i, a2) {
    assertEquals(a, a2);
    // Even if a function modifies the input array, the original value
    // should be inserted into the new array.
    a2[i] = a2[i] - 1;
    return a2[i] >= 1;
  });

  var pair = callbackPair(function(val) {
    assertArrayEquals([2, 3], val);
  });
  result.then(pair.callback, pair.errback);
  clock.tick();
  pair.assertCallback();
}


function testFilteringAnArray_inputIsPromise() {
  var input = webdriver.promise.defer();
  var result = webdriver.promise.filter(input, function(value) {
    return value > 1 && value < 3;
  });

  var pair = callbackPair(function(value) {
    assertArrayEquals([2], value);
  });

  result.then(pair.callback, pair.errback);
  pair.assertNeither();
  input.fulfill([1, 2, 3]);
  clock.tick();
  pair.assertCallback();
}


function testFilteringAnArray_waitsForFunctionResultToResolve() {
  var innerResults = [
    webdriver.promise.defer(),
    webdriver.promise.defer()
  ];

  var result = webdriver.promise.filter([1, 2], function(value, index) {
    return innerResults[index].promise;
  });

  var pair = callbackPair(function(value) {
    assertArrayEquals([2], value);
  });

  result.then(pair.callback, pair.errback);
  pair.assertNeither();

  innerResults[0].fulfill(false);
  clock.tick();
  pair.assertNeither();

  innerResults[1].fulfill(true);
  clock.tick();
  pair.assertCallback();
}


function testFilteringAnArray_rejectsPromiseIfFunctionReturnsRejectedPromise() {
  var result = webdriver.promise.filter([1], function() {
    return webdriver.promise.rejected(new StubError);
  });

  var pair = callbackPair(null, assertIsStubError);
  result.then(pair.callback, pair.errback);
  clock.tick();
  pair.assertErrback();
}


function testFilteringAnArray_stopsCallingFunctionIfPreviousIterationFailed() {
  var count = 0;
  var result = webdriver.promise.filter([1, 2, 3, 4], function() {
    count++;
    if (count == 3) {
      throw new StubError;
    }
  });

  var pair = callbackPair(null, assertIsStubError);
  result.then(pair.callback, pair.errback);
  clock.tick();
  pair.assertErrback();
  assertEquals(3, count);
}


function testFilteringAnArray_rejectsWithFirstRejectedPromise() {
  var innerResult = [
    webdriver.promise.fulfilled(),
    createRejectedPromise(new StubError),
    createRejectedPromise(Error('should be ignored'))
  ];
  var result = webdriver.promise.filter([1, 2, 3, 4], function(value, index) {
    assertTrue(index < innerResult.length);
    return innerResult[index];
  });

  var pair = callbackPair(null, assertIsStubError);
  result.then(pair.callback, pair.errback);
  pair.assertNeither();

  clock.tick();
  pair.assertErrback();
}


function testFilteringAnArray_preservesOrderWhenFilterReturnsPromise() {
  var deferreds = [
    webdriver.promise.defer(),
    webdriver.promise.defer(),
    webdriver.promise.defer(),
    webdriver.promise.defer()
  ];
  var result = webdriver.promise.filter([0, 1, 2, 3], function(value, index) {
    return deferreds[index].promise;
  });

  var pair = callbackPair(function(value) {
    assertArrayEquals([1, 2], value);
  });
  result.then(pair.callback, pair.errback);
  clock.tick();
  pair.assertNeither();

  goog.array.forEachRight(deferreds, function(d, i) {
    d.fulfill(i > 0 && i < 3);
  });
  clock.tick();
  pair.assertCallback();
}


function testAddThenableImplementation() {
  function tmp() {}
  assertFalse(webdriver.promise.Thenable.isImplementation(new tmp()));
  webdriver.promise.Thenable.addImplementation(tmp);
  assertTrue(webdriver.promise.Thenable.isImplementation(new tmp()));
}


function testLongStackTraces_doesNotAppendStackIfFeatureDisabled() {
  webdriver.promise.LONG_STACK_TRACES = false;

  var error = Error('hello');
  var originalStack = error.stack;
  var pair = callbackPair(null, function(e) {
    assertEquals(error, e);
    assertEquals(originalStack, e.stack);
  });
  webdriver.promise.rejected(error).
      then(fail).
      then(fail).
      then(fail).
      then(pair.callback, pair.errback);
  clock.tick();
  pair.assertErrback();
}


function getStackMessages(error) {
  var stack = webdriver.stacktrace.getStack(error);
  return goog.array.filter(stack.split(/\n/), function(line) {
    return /^From: /.test(line);
  });
}


function testLongStackTraces_appendsInitialPromiseCreation_resolverThrows() {
  webdriver.promise.LONG_STACK_TRACES = true;

  var error = Error('hello');
  var originalStack = '(placeholder; will be overwritten later)';

  var pair = callbackPair(null, function(e) {
    assertEquals(error, e);
    if (!goog.isString(originalStack)) {
      return;
    }
    assertNotEquals(originalStack, e.stack);
    assertTrue('should start with original stack',
        goog.string.startsWith(e.stack, originalStack));
    assertArrayEquals(['From: Promise: new'], getStackMessages(e));
  });

  new webdriver.promise.Promise(function() {
    try {
      throw error;
    } catch (e) {
      originalStack = e.stack;
      throw e;
    }
  }).then(pair.callback, pair.errback);

  clock.tick();
  pair.assertErrback();
}


function testLongStackTraces_appendsInitialPromiseCreation_rejectCalled() {
  webdriver.promise.LONG_STACK_TRACES = true;

  var error = Error('hello');
  var originalStack = error.stack;

  var pair = callbackPair(null, function(e) {
    assertEquals(error, e);
    if (!goog.isString(originalStack)) {
      return;
    }
    assertNotEquals(originalStack, e.stack);
    assertTrue('should start with original stack',
        goog.string.startsWith(e.stack, originalStack));
    assertArrayEquals(['From: Promise: new'], getStackMessages(e));
  });

  new webdriver.promise.Promise(function(_, reject) {
    reject(error);
  }).then(pair.callback, pair.errback);

  clock.tick();
  pair.assertErrback();
}


function testLongStackTraces_appendsEachStepToRejectionError() {
  webdriver.promise.LONG_STACK_TRACES = true;

  var error = Error('hello');
  var originalStack = '(placeholder; will be overwritten later)';

  var pair = callbackPair(null, function(e) {
    assertEquals(error, e);
    if (!goog.isString(originalStack)) {
      return;
    }
    assertNotEquals(originalStack, e.stack);
    assertTrue('should start with original stack',
        goog.string.startsWith(e.stack, originalStack));
    assertArrayEquals([
      'From: Promise: new',
      'From: Promise: then',
      'From: Promise: catch',
      'From: Promise: then',
      'From: Promise: catch',
    ], getStackMessages(e));
  });

  new webdriver.promise.Promise(function() {
    try {
      throw error;
    } catch (e) {
      originalStack = e.stack;
      throw e;
    }
  }).
  then(fail).
  thenCatch(function(e) { throw e; }).
  then(fail).
  thenCatch(function(e) { throw e; }).
  then(pair.callback, pair.errback);

  clock.tick();
  pair.assertErrback();
}


function testLongStackTraces_errorOccursInCallbackChain() {
  webdriver.promise.LONG_STACK_TRACES = true;

  var error = Error('hello');
  var originalStack = '(placeholder; will be overwritten later)';

  var pair = callbackPair(null, function(e) {
    assertEquals(error, e);
    if (!goog.isString(originalStack)) {
      return;
    }
    assertNotEquals(originalStack, e.stack);
    assertTrue('should start with original stack',
        goog.string.startsWith(e.stack, originalStack));
    assertArrayEquals([
      'From: Promise: then',
      'From: Promise: catch',
    ], getStackMessages(e));
  });

  webdriver.promise.fulfilled().
      then(goog.nullFunction).
      then(goog.nullFunction).
      then(function() {
        try {
          throw error;
        } catch (e) {
          originalStack = e.stack;
          throw e;
        }
      }).
      thenCatch(function(e) { throw e; }).
      then(pair.callback, pair.errback);

  clock.tick();
  pair.assertErrback();
}
