/**
 * @license
 * Copyright The Closure Library Authors.
 * SPDX-License-Identifier: Apache-2.0
 */

goog.module('goog.PromiseTest');
goog.setTestOnly();

const GoogPromise = goog.require('goog.Promise');
const MockClock = goog.require('goog.testing.MockClock');
const PropertyReplacer = goog.require('goog.testing.PropertyReplacer');
const TestCase = goog.require('goog.testing.TestCase');
const Thenable = goog.require('goog.Thenable');
const Timer = goog.require('goog.Timer');
const functions = goog.require('goog.functions');
const recordFunction = goog.require('goog.testing.recordFunction');
const testSuite = goog.require('goog.testing.testSuite');

// TODO(brenneman):
// - Add tests for interoperability with native Promises where available.
// - Add tests for long stack traces.

/** @type {boolean} */
const SUPPORTS_STRICT_MODE = (function() {
                               return this;
                             })() === undefined;

/**
 * @type {boolean}
 * @suppress {strictMissingProperties}
 * */
const MICROTASKS_EXIST = Promise.toString().indexOf('[native code]') >= 0;

/** @type {!MockClock} */
const mockClock = new MockClock();

/** @type {!PropertyReplacer} */
const stubs = new PropertyReplacer();

let unhandledRejections;

// Simple shared objects used as test values.
const dummy = {
  toString: functions.constant('[object dummy]')
};
const sentinel = {
  toString: functions.constant('[object sentinel]')
};

/**
 * Dummy onfulfilled or onrejected function that should not be called.
 * @param {*} result The result passed into the callback.
 */
function shouldNotCall(result) {
  fail('This should not have been called (result: ' + String(result) + ')');
}

/** @typedef {function(new:IThenable<?>, function(function(?), function(?)))} */
const IThenableCtor = undefined;

const /** !IThenableCtor */ NativeOrGoogPromise = window.Promise || GoogPromise;

/** @implements {IThenable<?>} */
class CountingThenable {
  /** @param {function(function(?), function(?))} immediate */
  constructor(immediate) {
    /** @type {number} */
    this.thenCallCount = 0;
    /** @const @private {!GoogPromise<?>} */
    this.internalPromise_ = new GoogPromise(immediate);
  }

  /**
   * @param {?} value
   * @return {!CountingThenable}
   */
  static resolve(value) {
    return new CountingThenable((resolve) => resolve(value));
  }

  /**
   * @param {?(function(?): ?)=} onResolve
   * @param {?(function(?): ?)=} onReject
   * @param {*=} shouldCount
   * @return {?}
   */
  then(onResolve, onReject, shouldCount) {
    if (shouldCount !== CountingThenable) {
      this.thenCallCount++;
    }
    return CountingThenable.resolve(
        this.internalPromise_.then(onResolve, onReject));
  }
}

/** @implements {IThenable<?>} */
class ThrowingThenable {
  /** @param {?} value */
  constructor(value) {
    /** @const {?} */
    this.value = value;
  }

  /**
   * @param {?(function(?): ?)=} onResolve
   * @param {?(function(?): ?)=} onReject
   * @param {*=} ctx
   * @return {?}
   */
  then(onResolve, onReject, ctx) {
    throw this.value;
  }
}

/**
 * Return an `IThenable` for `(typeof ctor)` that resolves on the next tick.
 *
 * @template T
 * @param {function(new:T, ...?)} ctor
 * @param {?} value
 * @return {T}
 */
function fulfillSoon(ctor, value) {
  return new ctor((resolve, reject) => {
    window.setTimeout(() => resolve(value), 0);
  });
}

/**
 * Return an `IThenable` for `(typeof ctor)` that rejects on the next tick.
 *
 * @template T
 * @param {function(new:T, ...?)} ctor
 * @param {?} value
 * @return {T}
 */
function rejectSoon(ctor, value) {
  return new ctor((resolve, reject) => {
    window.setTimeout(() => reject(value), 0);
  });
}

/**
 * Return a new `IThenable` chained after `thenable` with a delay of one tick.
 *
 * @template T
 * @param {T} thenable
 * @param {function(...?): ?} callback
 * @return {T}
 */
function after(thenable, callback) {
  const always = () => {
    try {
      return fulfillSoon(thenable.constructor, callback());
    } catch (e) {
      return rejectSoon(thenable.constructor, e);
    }
  };

  return thenable.then(always, always, CountingThenable);
}

/**
 * Runs the test, passing it a function to call when its promise chain is
 * complete, and all test assertions are complete.
 * @param {function(function():undefined):undefined} testBody
 * @return {!Promise}
 */
function validatePromiseChain(testBody) {
  return new Promise((promiseChainComplete) => {
    testBody(promiseChainComplete);
  });
}

testSuite({
  setUpPage() {
    TestCase.getActiveTestCase().promiseTimeout = 10000;  // 10s
  },

  setUp() {
    unhandledRejections = recordFunction();
    GoogPromise.setUnhandledRejectionHandler(unhandledRejections);
  },

  tearDown() {
    // The system should leave no pending unhandled rejections. Advance the mock
    // clock (if installed) to catch any rethrows waiting in the queue.
    mockClock.tick(Infinity);
    mockClock.uninstall();
    mockClock.reset();

    stubs.reset();
  },

  testThenIsFulfilled() {
    let timesCalled = 0;

    const p = new GoogPromise((resolve, reject) => {
      resolve(sentinel);
    });
    p.then((value) => {
      timesCalled++;
      assertEquals(sentinel, value);
    });

    assertEquals(
        'then() must return before callbacks are invoked.', 0, timesCalled);

    return p.then(() => {
      assertEquals('onFulfilled must be called exactly once.', 1, timesCalled);
    });
  },

  testThenVoidIsFulfilled() {
    let timesCalled = 0;

    const p = GoogPromise.resolve(sentinel);
    p.thenVoid((value) => {
      timesCalled++;
      assertEquals(sentinel, value);
    });

    assertEquals(
        'thenVoid() must return before callbacks are invoked.', 0, timesCalled);

    return p.then(() => {
      assertEquals('onFulfilled must be called exactly once.', 1, timesCalled);
    });
  },

  testThenIsRejected() {
    let timesCalled = 0;

    const p = GoogPromise.reject(sentinel);
    p.then(shouldNotCall, (value) => {
      timesCalled++;
      assertEquals(sentinel, value);
    });

    assertEquals(
        'then() must return before callbacks are invoked.', 0, timesCalled);

    return p.then(shouldNotCall, () => {
      assertEquals('onRejected must be called exactly once.', 1, timesCalled);
    });
  },

  testThenVoidIsRejected() {
    let timesCalled = 0;

    const p = GoogPromise.reject(sentinel);
    p.thenVoid(shouldNotCall, (value) => {
      timesCalled++;
      assertEquals(sentinel, value);
      assertEquals('onRejected must be called exactly once.', 1, timesCalled);
    });

    assertEquals(
        'thenVoid() must return before callbacks are invoked.', 0, timesCalled);

    return p.then(shouldNotCall, () => {
      assertEquals('onRejected must be called exactly once.', 1, timesCalled);
    });
  },

  testThenAsserts() {
    const p = GoogPromise.resolve();

    let m = assertThrows(() => {
      p.then(/** @type {?} */ ({}));
    });
    assertContains('opt_onFulfilled should be a function.', m.message);

    m = assertThrows(() => {
      p.then(() => {}, /** @type {?} */ ({}));
    });
    assertContains('opt_onRejected should be a function.', m.message);
  },

  testThenVoidAsserts() {
    const p = GoogPromise.resolve();

    let m = assertThrows(() => {
      p.thenVoid(/** @type {?} */ ({}));
    });
    assertContains('opt_onFulfilled should be a function.', m.message);

    m = assertThrows(() => {
      p.thenVoid(() => {}, /** @type {?} */ ({}));
    });
    assertContains('opt_onRejected should be a function.', m.message);
  },

  testOptionalOnFulfilled() {
    return GoogPromise.resolve(sentinel)
        .then(null, null)
        .then(null, shouldNotCall)
        .then((value) => {
          assertEquals(sentinel, value);
        });
  },

  testOptionalOnRejected() {
    return GoogPromise.reject(sentinel)
        .then(null, null)
        .then(shouldNotCall)
        .then(null, (reason) => {
          assertEquals(sentinel, reason);
        });
  },

  testMultipleResolves() {
    let timesCalled = 0;
    let resolvePromise;

    const p = new GoogPromise((resolve, reject) => {
      resolvePromise = resolve;
      resolve('foo');
      resolve('bar');
    });

    p.then((value) => {
      timesCalled++;
      assertEquals('onFulfilled must be called exactly once.', 1, timesCalled);
    });

    // Add one more test for fulfilling after a delay.
    return Timer.promise(10).then(() => {
      resolvePromise('baz');
      assertEquals(1, timesCalled);
    });
  },

  testMultipleRejects() {
    let timesCalled = 0;
    let rejectPromise;

    const p = new GoogPromise((resolve, reject) => {
      rejectPromise = reject;
      reject('foo');
      reject('bar');
    });

    p.then(shouldNotCall, (value) => {
      timesCalled++;
      assertEquals('onRejected must be called exactly once.', 1, timesCalled);
    });

    // Add one more test for rejecting after a delay.
    return Timer.promise(10).then(() => {
      rejectPromise('baz');
      assertEquals(1, timesCalled);
    });
  },

  testAsynchronousThenCalls() {
    const timesCalled = [0, 0, 0, 0];
    const p = new GoogPromise((resolve, reject) => {
      window.setTimeout(() => {
        resolve();
      }, 30);
    });

    p.then(() => {
      timesCalled[0]++;
      assertArrayEquals([1, 0, 0, 0], timesCalled);
    });

    window.setTimeout(() => {
      p.then(() => {
        timesCalled[1]++;
        assertArrayEquals([1, 1, 0, 0], timesCalled);
      });
    }, 10);

    window.setTimeout(() => {
      p.then(() => {
        timesCalled[2]++;
        assertArrayEquals([1, 1, 1, 0], timesCalled);
      });
    }, 20);

    return Timer.promise(40).then(() => p.then(() => {
      timesCalled[3]++;
      assertArrayEquals([1, 1, 1, 1], timesCalled);
    }));
  },

  testResolveWithPromise() {
    let resolveBlocker;
    let hasFulfilled = false;
    const blocker = new GoogPromise((resolve, reject) => {
      resolveBlocker = resolve;
    });

    const p = GoogPromise.resolve(blocker);
    p.then((value) => {
      hasFulfilled = true;
      assertEquals(sentinel, value);
    }, shouldNotCall);

    assertFalse(hasFulfilled);
    resolveBlocker(sentinel);

    return p.then(() => {
      assertTrue(hasFulfilled);
    });
  },

  testResolveWithRejectedPromise() {
    let rejectBlocker;
    let hasRejected = false;
    const blocker = new GoogPromise((resolve, reject) => {
      rejectBlocker = reject;
    });

    const p = GoogPromise.resolve(blocker);
    const child = p.then(shouldNotCall, (reason) => {
      hasRejected = true;
      assertEquals(sentinel, reason);
    });

    assertFalse(hasRejected);
    rejectBlocker(sentinel);

    return child.thenCatch(() => {
      assertTrue(hasRejected);
    });
  },

  testRejectWithPromise() {
    let resolveBlocker;
    let hasFulfilled = false;
    let hasRejected = false;
    const blocker = new GoogPromise((resolve, reject) => {
      resolveBlocker = resolve;
    });

    const p = GoogPromise.reject(blocker);
    const child = p.then((value) => {
      hasFulfilled = true;
      assertEquals(sentinel, value);
    }, shouldNotCall);

    assertFalse(hasFulfilled);
    resolveBlocker(sentinel);

    return child.thenCatch(() => {
      assertTrue(hasRejected);
    });
  },

  testRejectWithRejectedPromise() {
    let rejectBlocker;
    let hasRejected = false;
    const blocker = new GoogPromise((resolve, reject) => {
      rejectBlocker = reject;
    });

    const p = GoogPromise.reject(blocker);
    const child = p.then(shouldNotCall, (reason) => {
      hasRejected = true;
      assertEquals(sentinel, reason);
    });

    assertFalse(hasRejected);
    rejectBlocker(sentinel);

    return child.thenCatch(() => {
      assertTrue(hasRejected);
    });
  },

  testResolveAndReject() {
    let onFulfilledCalled = false;
    let onRejectedCalled = false;
    const p = new GoogPromise((resolve, reject) => {
      resolve();
      reject();
    });

    p.then(
        () => {
          onFulfilledCalled = true;
        },
        () => {
          onRejectedCalled = true;
        });

    return p.then(() => {
      assertTrue(onFulfilledCalled);
      assertFalse(onRejectedCalled);
    });
  },

  testResolveWithSelfRejects() {
    let r;
    const p = new GoogPromise((resolve) => {
      r = resolve;
    });
    r(p);
    return p.then(shouldNotCall, (e) => {
      assertEquals(
          /** @type {!Error} */ (e).message,
          'Promise cannot resolve to itself');
    });
  },

  testResolveWithObjectStringResolves() {
    return GoogPromise.resolve('[object Object]').then((v) => {
      assertEquals(v, '[object Object]');
    });
  },

  testRejectAndResolve() {
    return new GoogPromise((resolve, reject) => {
             reject();
             resolve();
           })
        .then(shouldNotCall, () => true);
  },

  testThenReturnsBeforeCallbackWithFulfill() {
    let thenHasReturned = false;
    const p = GoogPromise.resolve();

    const child = p.then(() => {
      assertTrue(
          'Callback must be called only after then() has returned.',
          thenHasReturned);
    });
    thenHasReturned = true;

    return child;
  },

  testThenReturnsBeforeCallbackWithReject() {
    let thenHasReturned = false;
    const p = GoogPromise.reject();

    const child = p.then(shouldNotCall, () => {
      assertTrue(
          'Callback must be called only after then() has returned.',
          thenHasReturned);
    });
    thenHasReturned = true;

    return child;
  },

  testResolutionOrder() {
    const callbacks = [];
    return GoogPromise.resolve()
        .then(
            () => {
              callbacks.push(1);
            },
            shouldNotCall)
        .then(
            () => {
              callbacks.push(2);
            },
            shouldNotCall)
        .then(
            () => {
              callbacks.push(3);
            },
            shouldNotCall)
        .then(() => {
          assertArrayEquals([1, 2, 3], callbacks);
        });
  },

  testResolutionOrderWithThrow() {
    const callbacks = [];
    const p = GoogPromise.resolve();

    p.then(() => {
      callbacks.push(1);
    }, shouldNotCall);
    const child = p.then(() => {
      callbacks.push(2);
      throw new Error();
    }, shouldNotCall);

    child.then(shouldNotCall, () => {
      // The parent callbacks should be evaluated before the child.
      callbacks.push(4);
    });

    p.then(() => {
      callbacks.push(3);
    }, shouldNotCall);

    return child.then(shouldNotCall, () => {
      callbacks.push(5);
      assertArrayEquals([1, 2, 3, 4, 5], callbacks);
    });
  },

  testResolutionOrderWithNestedThen() {
    const resolver = GoogPromise.withResolver();

    const callbacks = [];
    const p = GoogPromise.resolve();

    p.then(() => {
      callbacks.push(1);
      p.then(() => {
        callbacks.push(3);
        resolver.resolve();
      });
    });
    p.then(() => {
      callbacks.push(2);
    });

    return resolver.promise.then(() => {
      assertArrayEquals([1, 2, 3], callbacks);
    });
  },

  testRejectionOrder() {
    const callbacks = [];
    const p = GoogPromise.reject();

    p.then(shouldNotCall, () => {
      callbacks.push(1);
    });
    p.then(shouldNotCall, () => {
      callbacks.push(2);
    });
    p.then(shouldNotCall, () => {
      callbacks.push(3);
    });

    return p.then(shouldNotCall, () => {
      assertArrayEquals([1, 2, 3], callbacks);
    });
  },

  testRejectionOrderWithThrow() {
    const callbacks = [];
    const p = GoogPromise.reject();

    p.then(shouldNotCall, () => {
      callbacks.push(1);
    });
    p.then(shouldNotCall, () => {
      callbacks.push(2);
      throw new Error();
    });
    p.then(shouldNotCall, () => {
      callbacks.push(3);
    });

    return p.then(shouldNotCall, () => {
      assertArrayEquals([1, 2, 3], callbacks);
    });
  },

  testRejectionOrderWithNestedThen() {
    const resolver = GoogPromise.withResolver();

    const callbacks = [];
    const p = GoogPromise.reject();

    p.then(shouldNotCall, () => {
      callbacks.push(1);
      p.then(shouldNotCall, () => {
        callbacks.push(3);
        resolver.resolve();
      });
    });
    p.then(shouldNotCall, () => {
      callbacks.push(2);
    });

    return resolver.promise.then(() => {
      assertArrayEquals([1, 2, 3], callbacks);
    });
  },

  testBranching() {
    const p = GoogPromise.resolve(2);

    const branch1 =
        p.then((value) => {
           assertEquals('then functions should see the same value', 2, value);
           return value / 2;
         }).then((value) => {
          assertEquals('branch should receive the returned value', 1, value);
        });

    const branch2 =
        p.then((value) => {
           assertEquals('then functions should see the same value', 2, value);
           throw value + 1;
         }).then(shouldNotCall, (reason) => {
          assertEquals('branch should receive the thrown value', 3, reason);
        });

    const branch3 =
        p.then((value) => {
           assertEquals('then functions should see the same value', 2, value);
           return value * 2;
         }).then((value) => {
          assertEquals('branch should receive the returned value', 4, value);
        });

    return GoogPromise.all([branch1, branch2, branch3]);
  },

  testThenReturnsPromise() {
    const parent = GoogPromise.resolve();
    const child = parent.then();

    assertTrue(child instanceof GoogPromise);
    assertNotEquals(
        'The returned Promise must be different from the input.', parent,
        child);
  },

  testThenVoidReturnsUndefined() {
    const parent = GoogPromise.resolve();
    const child = parent.thenVoid();

    assertUndefined(child);
  },

  testBlockingPromise() {
    const p = GoogPromise.resolve();
    let wasFulfilled = false;
    let wasRejected = false;

    const p2 = p.then(() => new GoogPromise((resolve, reject) => {}));

    p2.then(
        () => {
          wasFulfilled = true;
        },
        () => {
          wasRejected = true;
        });

    return Timer.promise(10).then(() => {
      assertFalse('p2 should be blocked on the returned Promise', wasFulfilled);
      assertFalse('p2 should be blocked on the returned Promise', wasRejected);
    });
  },

  testBlockingPromiseFulfilled() {
    const blockingPromise = new GoogPromise((resolve, reject) => {
      window.setTimeout(() => {
        resolve(sentinel);
      }, 0);
    });

    const p = GoogPromise.resolve(dummy);
    const p2 = p.then((value) => blockingPromise);

    return p2.then((value) => {
      assertEquals(sentinel, value);
    });
  },

  testBlockingPromiseRejected() {
    const blockingPromise = new GoogPromise((resolve, reject) => {
      window.setTimeout(() => {
        reject(sentinel);
      }, 0);
    });

    const p = GoogPromise.resolve(blockingPromise);

    return p.then(shouldNotCall, (reason) => {
      assertEquals(sentinel, reason);
    });
  },

  testBlockingThenableFulfilled() {
    const thenable = {
      then: function(onFulfill, onReject) {
        onFulfill(sentinel);
      }
    };

    return GoogPromise.resolve(thenable).then((reason) => {
      assertEquals(sentinel, reason);
    });
  },

  testBlockingThenableRejected() {
    const thenable = {
      then: function(onFulfill, onReject) {
        onReject(sentinel);
      }
    };

    return GoogPromise.resolve(thenable).then(shouldNotCall, (reason) => {
      assertEquals(sentinel, reason);
    });
  },

  testBlockingThenableThrows() {
    const thenable = {
      then: function(onFulfill, onReject) {
        throw sentinel;
      }
    };

    return GoogPromise.resolve(thenable).then(shouldNotCall, (reason) => {
      assertEquals(sentinel, reason);
    });
  },

  testBlockingThenableMisbehaves() {
    const thenable = {
      then: function(onFulfill, onReject) {
        onFulfill(sentinel);
        onFulfill(dummy);
        onReject(dummy);
        throw dummy;
      },
    };

    return GoogPromise.resolve(thenable).then((value) => {
      assertEquals(
          'Only the first resolution of the Thenable should have a result.',
          sentinel, value);
    });
  },

  testNestingThenables() {
    const thenableA = {
      then: function(onFulfill, onReject) {
        onFulfill(sentinel);
      },
    };
    const thenableB = {
      then: function(onFulfill, onReject) {
        onFulfill(thenableA);
      },
    };
    const thenableC = {
      then: function(onFulfill, onReject) {
        onFulfill(thenableB);
      },
    };

    return GoogPromise.resolve(thenableC).then((value) => {
      assertEquals(
          'Should resolve to the fulfillment value of thenableA', sentinel,
          value);
    });
  },

  testNestingThenablesRejected() {
    const thenableA = {
      then: function(onFulfill, onReject) {
        onReject(sentinel);
      }
    };
    const thenableB = {
      then: function(onFulfill, onReject) {
        onReject(thenableA);
      },
    };
    const thenableC = {
      then: function(onFulfill, onReject) {
        onReject(thenableB);
      },
    };

    return GoogPromise.reject(thenableC).then(shouldNotCall, (reason) => {
      assertEquals(
          'Should resolve to rejection reason of thenableA', sentinel, reason);
    });
  },

  testThenCatch() {
    return validatePromiseChain((promiseChainComplete) => {
      let catchCalled = false;
      GoogPromise.reject()
          .thenCatch((reason) => {
            catchCalled = true;
            return sentinel;
          })
          .then((value) => {
            assertTrue(catchCalled);
            assertEquals(sentinel, value);
            promiseChainComplete();
          });
    });
  },

  testThenCatchWithSuccess() {
    return validatePromiseChain((promiseChainComplete) => {
      let catchCalled = false;
      GoogPromise.resolve(sentinel)
          .thenCatch((reason) => {
            catchCalled = true;
            return dummy;
          })
          .then((value) => {
            assertFalse(catchCalled);
            assertEquals(sentinel, value);
            promiseChainComplete();
          });
    });
  },

  testThenCatchThrows() {
    return validatePromiseChain((promiseChainComplete) => {
      GoogPromise.reject(dummy)
          .thenCatch((reason) => {
            assertEquals(dummy, reason);
            throw sentinel;
          })
          .then(
              () => {
                fail('Should have rejected.');
              },
              (value) => {
                assertEquals(sentinel, value);
                promiseChainComplete();
              });
    });
  },

  testRaceWithEmptyList() {
    return GoogPromise.race([]).then((value) => {
      assertUndefined(value);
    });
  },

  testRaceWithFulfill() {
    const c = fulfillSoon(NativeOrGoogPromise, 'c');
    const d = after(c, () => 'd');
    const b = after(d, () => 'b');
    const a = after(b, () => 'a');

    return GoogPromise.race([a, b, c, d])
        .then((value) => {
          assertEquals('c', value);
          // Return the slowest input promise to wait for it to complete.
          return a;
        })
        .then((value) => {
          assertEquals(
              'The slowest promise should resolve eventually.', 'a', value);
        });
  },

  testRaceWithThenables() {
    const c = fulfillSoon(CountingThenable, 'c');
    const d = after(c, () => 'd');
    const b = after(d, () => 'b');
    const a = after(b, () => 'a');

    return GoogPromise.race([a, b, c, d])
        .then((value) => {
          assertEquals('c', value);
          // Ensure that the `then` property was only accessed once by
          // `goog.Promise.race`.
          assertEquals(1, c.thenCallCount);
          // Return the slowest input thenable to wait for it to complete.
          return a;
        })
        .then((value) => {
          assertEquals(
              'The slowest thenable should resolve eventually.', 'a', value);
        });
  },

  testRaceWithBuiltIns() {
    const c = fulfillSoon(NativeOrGoogPromise, 'c');
    const d = after(c, () => 'd');
    const b = after(d, () => 'b');
    const a = after(d, () => 'a');

    return GoogPromise.race([a, b, c, d])
        .then((value) => {
          assertEquals('c', value);
          // Return the slowest input promise to wait for it to complete.
          return a;
        })
        .then((value) => {
          assertEquals(
              'The slowest promise should resolve eventually.', 'a', value);
        });
  },

  testRaceWithNonThenable() {
    const c = fulfillSoon(GoogPromise, 'c');
    const d = after(c, () => 'd');
    const b = 'b';
    const a = after(d, () => 'a');

    return GoogPromise.race([a, b, c, d])
        .then((value) => {
          assertEquals('b', value);
          // Return the slowest input promise to wait for it to complete.
          return a;
        })
        .then((value) => {
          assertEquals(
              'The slowest promise should resolve eventually.', 'a', value);
        });
  },

  testRaceWithFalseyNonThenable() {
    const c = fulfillSoon(GoogPromise, 'c');
    const d = after(c, () => 'd');
    const b = 0;
    const a = after(d, () => 'a');

    return GoogPromise.race([a, b, c, d])
        .then((value) => {
          assertEquals(0, value);
          // Return the slowest input promise to wait for it to complete.
          return a;
        })
        .then((value) => {
          assertEquals(
              'The slowest promise should resolve eventually.', 'a', value);
        });
  },

  testRaceWithFulfilledBeforeNonThenable() {
    const c = 'c';
    const d = fulfillSoon(GoogPromise, 'd');
    const b = GoogPromise.resolve('b');
    const a = after(d, () => 'a');

    return GoogPromise.race([a, b, c, d])
        .then((value) => {
          assertEquals('b', value);
          // Return the slowest input promise to wait for it to complete.
          return a;
        })
        .then((value) => {
          assertEquals(
              'The slowest promise should resolve eventually.', 'a', value);
        });
  },

  testRaceWithReject() {
    const c = rejectSoon(GoogPromise, 'rejected-c');
    const d = after(c, () => {
      throw 'rejected-d';
    });
    const b = after(d, () => {
      throw 'rejected-b';
    });
    const a = after(b, () => {
      throw 'rejected-a';
    });

    return GoogPromise.race([a, b, c, d])
        .then(
            shouldNotCall,
            (value) => {
              assertEquals('rejected-c', value);
              return a;
            })
        .then(shouldNotCall, (reason) => {
          assertEquals(
              'The slowest promise should resolve eventually.', 'rejected-a',
              reason);
        });
  },

  testRaceWithRejectThenable() {
    const c = rejectSoon(CountingThenable, 'rejected-c');
    const d = after(c, () => {
      throw 'rejected-d';
    });
    const b = after(d, () => {
      throw 'rejected-b';
    });
    const a = after(b, () => {
      throw 'rejected-a';
    });

    return GoogPromise.race([a, b, c, d])
        .then(
            shouldNotCall,
            (value) => {
              assertEquals('rejected-c', value);
              return a;
            })
        .then(shouldNotCall, (reason) => {
          assertEquals(
              'The slowest promise should resolve eventually.', 'rejected-a',
              reason);
        });
  },

  testRaceWithRejectBuiltIn() {
    const c = rejectSoon(NativeOrGoogPromise, 'rejected-c');
    const d = after(c, () => {
      throw 'rejected-d';
    });
    const b = after(d, () => {
      throw 'rejected-b';
    });
    const a = after(b, () => {
      throw 'rejected-a';
    });

    return GoogPromise.race([a, b, c, d])
        .then(
            shouldNotCall,
            (value) => {
              assertEquals('rejected-c', value);
              return a;
            })
        .then(shouldNotCall, (reason) => {
          assertEquals(
              'The slowest promise should resolve eventually.', 'rejected-a',
              reason);
        });
  },

  testRaceWithRejectAndThrowingThenable() {
    const c = rejectSoon(NativeOrGoogPromise, 'rejected-c');
    const d = new ThrowingThenable('rejected-d');
    const b = CountingThenable.resolve(after(c, () => {
      throw 'rejected-b';
    }));
    const a = GoogPromise.resolve(after(b, () => {
      throw 'rejected-a';
    }));

    return GoogPromise.race([a, b, c, d])
        .then(
            shouldNotCall,
            (value) => {
              assertEquals('rejected-d', value);
              return a;
            })
        .then(shouldNotCall, (reason) => {
          assertEquals(
              'The slowest promise should resolve eventually.', 'rejected-a',
              reason);
        });
  },

  testAllWithEmptyList() {
    return GoogPromise.all([]).then((value) => {
      assertArrayEquals([], value);
    });
  },

  testAllWithFulfill() {
    const a = fulfillSoon(GoogPromise, 'a');
    const b = fulfillSoon(GoogPromise, 'b');
    const c = fulfillSoon(GoogPromise, 'c');
    const d = fulfillSoon(GoogPromise, 'd');
    // Test a falsey value.
    const z = fulfillSoon(NativeOrGoogPromise, 0);

    return GoogPromise.all([a, b, c, d, z]).then((value) => {
      assertArrayEquals(['a', 'b', 'c', 'd', 0], value);
    });
  },

  testAllWithThenable() {
    const a = fulfillSoon(GoogPromise, 'a');
    const b = fulfillSoon(CountingThenable, 'b');
    const c = fulfillSoon(GoogPromise, 'c');
    const d = fulfillSoon(GoogPromise, 'd');

    return GoogPromise.all([a, b, c, d]).then((value) => {
      assertArrayEquals(['a', 'b', 'c', 'd'], value);
      // Ensure that the `then` property was only accessed once by
      // `goog.Promise.all`.
      assertEquals(1, b.thenCallCount);
    });
  },

  testAllWithBuiltIn() {
    const a = fulfillSoon(GoogPromise, 'a');
    const b = fulfillSoon(NativeOrGoogPromise, 'b');
    const c = fulfillSoon(GoogPromise, 'c');
    const d = fulfillSoon(GoogPromise, 'd');

    return GoogPromise.all([a, b, c, d]).then((value) => {
      assertArrayEquals(['a', 'b', 'c', 'd'], value);
    });
  },

  testAllWithNonThenable() {
    const a = fulfillSoon(GoogPromise, 'a');
    const b = 'b';
    const c = fulfillSoon(GoogPromise, 'c');
    const d = fulfillSoon(GoogPromise, 'd');

    // Test a falsey value.
    const z = 0;

    return GoogPromise.all([a, b, c, d, z]).then((value) => {
      assertArrayEquals(['a', 'b', 'c', 'd', 0], value);
    });
  },

  testAllWithReject() {
    const a = fulfillSoon(GoogPromise, 'a');
    const b = rejectSoon(GoogPromise, 'rejected-b');
    const c = fulfillSoon(GoogPromise, 'c');
    const d = fulfillSoon(GoogPromise, 'd');


    return GoogPromise.all([a, b, c, d])
        .then(
            shouldNotCall,
            (reason) => {
              assertEquals('rejected-b', reason);
              return a;
            })
        .then((value) => {
          assertEquals(
              'Promise "a" should be fulfilled even though the all()' +
                  'was rejected.',
              'a', value);
        });
  },

  testAllSettledWithEmptyList() {
    return GoogPromise.allSettled([]).then((results) => {
      assertArrayEquals([], results);
    });
  },

  testAllSettledWithFulfillAndReject() {
    const a = fulfillSoon(GoogPromise, 'a');
    const b = rejectSoon(GoogPromise, 'rejected-b');
    const c = 'c';
    const d = rejectSoon(NativeOrGoogPromise, 'rejected-d');
    const e = fulfillSoon(CountingThenable, 'e');
    const f = fulfillSoon(NativeOrGoogPromise, 'f');
    const g = rejectSoon(CountingThenable, 'rejected-g');
    const h = new ThrowingThenable('rejected-h');
    // Test a falsey value.
    const z = 0;

    return GoogPromise.allSettled([a, b, c, d, e, f, g, h, z])
        .then((results) => {
          assertArrayEquals(
              [
                {fulfilled: true, value: 'a'},
                {fulfilled: false, reason: 'rejected-b'},
                {fulfilled: true, value: 'c'},
                {fulfilled: false, reason: 'rejected-d'},
                {fulfilled: true, value: 'e'},
                {fulfilled: true, value: 'f'},
                {fulfilled: false, reason: 'rejected-g'},
                {fulfilled: false, reason: 'rejected-h'},
                {fulfilled: true, value: 0},
              ],
              results);
          // Ensure that the `then` property was only accessed once by
          // `goog.Promise.allSettled`.
          assertEquals(1, e.thenCallCount);
          assertEquals(1, g.thenCallCount);
        });
  },

  testFirstFulfilledWithEmptyList() {
    return GoogPromise.firstFulfilled([]).then((value) => {
      assertUndefined(value);
    });
  },

  testFirstFulfilledWithFulfill() {
    const c = rejectSoon(GoogPromise, 'rejected-c');
    const d = after(c, () => 'd');
    const b = after(d, () => {
      throw 'rejected-b';
    });
    const a = after(b, () => 'a');

    return GoogPromise.firstFulfilled([a, b, c, d])
        .then((value) => {
          assertEquals('d', value);
          return c;
        })
        .then(
            shouldNotCall,
            (reason) => {
              assertEquals(
                  'Promise "c" should be rejected before firstFulfilled() resolves.',
                  'rejected-c', reason);
              return a;
            })
        .then((value) => {
          assertEquals(
              'Promise "a" should be fulfilled after firstFulfilled() resolves.',
              'a', value);
        });
  },

  testFirstFulfilledWithThenables() {
    const c = rejectSoon(CountingThenable, 'rejected-c');
    const d = after(c, () => 'd');
    const b = after(d, () => {
      throw 'rejected-b';
    });
    const a = after(b, () => 'a');

    return GoogPromise.firstFulfilled([a, b, c, d])
        .then((value) => {
          assertEquals('d', value);
          // Ensure that the `then` property was only accessed once by
          // `goog.Promise.firstFulfilled`.
          assertEquals(1, d.thenCallCount);

          return c;
        })
        .then(
            shouldNotCall,
            (reason) => {
              assertEquals(
                  'Thenable "c" should be rejected before firstFulfilled() resolves.',
                  'rejected-c', reason);
              return a;
            })
        .then((value) => {
          assertEquals(
              'Thenable "a" should be fulfilled after firstFulfilled() resolves.',
              'a', value);
        });
  },

  testFirstFulfilledWithBuiltIns() {
    const c = rejectSoon(NativeOrGoogPromise, 'rejected-c');
    const d = after(c, () => 'd');
    const b = after(d, () => {
      throw 'rejected-b';
    });
    const a = after(b, () => 'a');

    return GoogPromise.firstFulfilled([a, b, c, d])
        .then((value) => {
          assertEquals('d', value);
          return c;
        })
        .then(
            shouldNotCall,
            (reason) => {
              assertEquals(
                  'Promise "c" should be rejected before firstFulfilled() resolves.',
                  'rejected-c', reason);
              return a;
            })
        .then((value) => {
          assertEquals(
              'Promise "a" should be fulfilled after firstFulfilled() resolves.',
              'a', value);
        });
  },

  testFirstFulfilledWithNonThenable() {
    const c = rejectSoon(GoogPromise, 'rejected-c');
    const d = 'd';
    const b = after(c, () => {
      throw 'rejected-b';
    });
    const a = after(b, () => 'a');

    return GoogPromise.firstFulfilled([a, b, c, d])
        .then((value) => {
          assertEquals('d', value);
          // Return the slowest input promise to wait for it to complete.
          return a;
        })
        .then((value) => {
          assertEquals(
              'The slowest promise should resolve eventually.', 'a', value);
        });
  },

  testFirstFulfilledWithFalseyNonThenable() {
    const c = rejectSoon(GoogPromise, 'rejected-c');
    const d = 0;
    const b = after(c, () => {
      throw 'rejected-b';
    });
    const a = after(b, () => 'a');

    return GoogPromise.firstFulfilled([a, b, c, d])
        .then((value) => {
          assertEquals(0, value);
          // Return the slowest input promise to wait for it to complete.
          return a;
        })
        .then((value) => {
          assertEquals(
              'The slowest promise should resolve eventually.', 'a', value);
        });
  },

  testFirstFulfilledWithFulfilledBeforeNonThenable() {
    const c = rejectSoon(GoogPromise, 'rejected-c');
    const d = 'd';
    const b = GoogPromise.resolve('b');
    const a = after(c, () => 'a');

    return GoogPromise.firstFulfilled([a, b, c, d])
        .then((value) => {
          assertEquals('b', value);
          // Return the slowest input promise to wait for it to complete.
          return a;
        })
        .then((value) => {
          assertEquals(
              'The slowest promise should resolve eventually.', 'a', value);
        });
  },

  testFirstFulfilledWithReject() {
    const c = rejectSoon(NativeOrGoogPromise, 'rejected-c');
    const d = new ThrowingThenable('rejected-d');
    const b = CountingThenable.resolve(after(c, () => {
      throw 'rejected-b';
    }));
    const a = GoogPromise.resolve(after(b, () => {
      throw 'rejected-a';
    }));

    return GoogPromise.firstFulfilled([a, b, c, d])
        .then(shouldNotCall, (reason) => {
          assertArrayEquals(
              ['rejected-a', 'rejected-b', 'rejected-c', 'rejected-d'], reason);
          // Ensure that the `then` property was only accessed once by
          // `goog.Promise.firstFulfilled`.
          assertEquals(1, b.thenCallCount);
        });
  },

  testThenAlwaysWithFulfill() {
    let thenAlwaysCalled = false;
    return GoogPromise.resolve(sentinel)
        .thenAlways(function() {
          assertEquals(
              'thenAlways should have no arguments', 0, arguments.length);
          thenAlwaysCalled = true;
        })
        .then((value) => {
          assertEquals(sentinel, value);
          assertTrue(thenAlwaysCalled);
        });
  },

  testThenAlwaysWithReject() {
    return GoogPromise.reject(sentinel)
        .thenAlways(function() {
          assertEquals(
              'thenAlways should have no arguments', 0, arguments.length);
        })
        .then(shouldNotCall, (err) => {
          assertEquals(sentinel, err);
          return null;
        });
  },

  testThenAlwaysCalledMultipleTimes() {
    const calls = [];

    const p = GoogPromise.resolve(sentinel);
    p.then((value) => {
      assertEquals(sentinel, value);
      calls.push(1);
      return value;
    });
    p.thenAlways(function() {
      assertEquals(0, arguments.length);
      calls.push(2);
      throw new Error('thenAlways throw');
    });
    p.then((value) => {
      assertEquals(
          'Promise result should not mutate after throw from thenAlways.',
          sentinel, value);
      calls.push(3);
    });
    p.thenAlways(() => {
      assertArrayEquals([1, 2, 3], calls);
    });
    p.thenAlways(() => {
      assertEquals(
          'Should be one unhandled exception from the "thenAlways throw".', 1,
          unhandledRejections.getCallCount());
      const rejectionCall = unhandledRejections.popLastCall();
      assertEquals(1, rejectionCall.getArguments().length);
      const err = rejectionCall.getArguments()[0];
      assertEquals('thenAlways throw', err.message);
      assertEquals(null, rejectionCall.getThis());
    });

    return p.thenAlways(() => {
      assertEquals(3, calls.length);
    });
  },

  testContextWithInit() {
    let initContext;
    const p = new GoogPromise(function(resolve, reject) {
      initContext = this;
    }, sentinel);
    assertNotNull(p);
    assertEquals(sentinel, initContext);
  },

  testContextWithInitDefault() {
    if (!SUPPORTS_STRICT_MODE) {
      return;
    }
    let initContext;
    const p = new GoogPromise(function(resolve, reject) {
      initContext = this;
    });
    assertNotNull(p);
    assertEquals(
        'initFunc should default to being called with undefined', undefined,
        initContext);
  },

  testContextWithFulfillment() {
    if (!SUPPORTS_STRICT_MODE) {
      return;
    }
    return GoogPromise.resolve()
        .then(function() {
          assertEquals(
              '"undefined" should be bound if no context is specified.',
              undefined, this);
        })
        .then(
            function() {
              assertEquals(sentinel, this);
            },
            shouldNotCall, sentinel)
        .thenAlways(function() {
          assertEquals(sentinel, this);
        }, sentinel);
  },

  testContextWithRejection() {
    if (!SUPPORTS_STRICT_MODE) {
      return;
    }
    return GoogPromise.reject()
        .then(
            shouldNotCall,
            function() {
              assertEquals(
                  'Call should with undefined when no context is set.',
                  undefined, this);
              throw new Error('Intentional rejection');
            })
        .then(
            shouldNotCall,
            function() {
              assertEquals(sentinel, this);
            },
            sentinel)
        .thenAlways(
            function() {
              assertEquals(sentinel, this);
            },
            sentinel)
        .thenCatch(function() {
          assertEquals(sentinel, this);
        }, sentinel);
  },

  testCancel() {
    const p = new GoogPromise(functions.UNDEFINED);
    const child = p.then(shouldNotCall, (reason) => {
      assertTrue(reason instanceof GoogPromise.CancellationError);
      assertEquals(
          'cancellation message',
          /** @type {!GoogPromise.CancellationError} */ (reason).message);

      // Return a non-Error to resolve the cancellation rejection.
      return null;
    });
    p.cancel('cancellation message');
    return child;
  },

  async testCancelThenCatchIncludesDetailedStack() {
    if (!new Error()['stack']) {
      // e.stack is missing for IE9, IE10, and IE11.
      return;
    }
    // Given.
    const p = new GoogPromise(functions.UNDEFINED);
    function recurse(depth) {
      if (depth == 0) {
        p.cancel('cancellation message');
      } else {
        // Increase the number of stack frames.
        recurse(depth - 1);
      }
    }
    recurse(20);

    // When.
    const error = await assertRejects(p);

    // Then.
    const stackLines = error['stack'].split('\n');
    // Note: If Error() is created in an asynchronous frame the length is ~5
    // frames (depending on browser). When Error() is created synchronously
    // The frame is much longer and includes the frames from this test.
    assertTrue(stackLines.length > 20);
  },

  testThenVoidCancel() {
    let thenVoidCalled = false;
    const p = new GoogPromise(functions.UNDEFINED);

    p.thenVoid(shouldNotCall, (reason) => {
      assertTrue(reason instanceof GoogPromise.CancellationError);
      assertEquals(
          'cancellation message',
          /** @type {!GoogPromise.CancellationError} */ (reason).message);
      thenVoidCalled = true;
    });

    p.cancel('cancellation message');
    assertFalse(thenVoidCalled);

    return p.thenCatch(() => {
      assertTrue(thenVoidCalled);

      // Return a non-Error to resolve the cancellation rejection.
      return null;
    });
  },

  testCancelAfterResolve() {
    const p = GoogPromise.resolve();
    p.cancel();
    return p.then(null, shouldNotCall);
  },

  testThenVoidCancelAfterResolve() {
    const p = GoogPromise.resolve();
    p.cancel();
    p.thenVoid(null, shouldNotCall);
    return p;
  },

  testCancelAfterReject() {
    const p = GoogPromise.reject(sentinel);
    p.cancel();
    return p.then(shouldNotCall, (reason) => {
      assertEquals(sentinel, reason);
    });
  },

  testThenVoidCancelAfterReject() {
    let thenVoidCalled = false;
    const p = GoogPromise.reject(sentinel);
    p.cancel();

    p.thenVoid(shouldNotCall, (reason) => {
      assertEquals(sentinel, reason);
      thenVoidCalled = true;
    });

    return p.thenCatch(() => {
      assertTrue(thenVoidCalled);
    });
  },

  testCancelPropagation() {
    let cancelError;
    const p = new GoogPromise(functions.UNDEFINED);

    const p2 =
        p.then(shouldNotCall, (reason) => {
           cancelError = reason;
           assertTrue(reason instanceof GoogPromise.CancellationError);
           assertEquals(
               'parent cancel message',
               /** @type {!GoogPromise.CancellationError} */ (reason).message);
           return sentinel;
         }).then((value) => {
          assertEquals(
              'Child promises should receive the returned value of the parent.',
              sentinel, value);
        }, shouldNotCall);

    const p3 = p.then(shouldNotCall, (reason) => {
      assertEquals(
          'Every onRejected handler should receive the same cancel error.',
          cancelError, reason);
      assertEquals(
          'parent cancel message',
          /** @type {!GoogPromise.CancellationError} */ (reason).message);

      // Return a non-Error to resolve the cancellation rejection.
      return null;
    });

    p.cancel('parent cancel message');
    return GoogPromise.all([p2, p3]);
  },

  testThenVoidCancelPropagation() {
    const resolver = GoogPromise.withResolver();
    let toResolveCount = 2;

    const partialResolve = () => {
      if (--toResolveCount == 0) {
        resolver.resolve();
      }
    };

    let cancelError;
    const p = new GoogPromise(functions.UNDEFINED);

    const p2 = p.then(shouldNotCall, (reason) => {
      cancelError = reason;
      assertTrue(reason instanceof GoogPromise.CancellationError);
      assertEquals(
          'parent cancel message',
          /** @type {!GoogPromise.CancellationError} */ (reason).message);
      return sentinel;
    });
    p2.thenVoid((value) => {
      assertEquals(
          'Child promises should receive the returned value of the parent.',
          sentinel, value);
      partialResolve();
    }, shouldNotCall);

    p.thenVoid(shouldNotCall, (reason) => {
      assertEquals(
          'Every onRejected handler should receive the same cancel error.',
          cancelError, reason);
      assertEquals(
          'parent cancel message',
          /** @type {!GoogPromise.CancellationError} */ (reason).message);
      partialResolve();
    });

    p.cancel('parent cancel message');
    return resolver.promise;
  },

  testCancelPropagationUpward() {
    let cancelError;
    const cancelCalls = [];
    const parent = new GoogPromise(functions.UNDEFINED);

    const child = parent.then(shouldNotCall, (reason) => {
      assertTrue(reason instanceof GoogPromise.CancellationError);
      assertEquals(
          'grandChild cancel message',
          /** @type {!GoogPromise.CancellationError} */ (reason).message);
      cancelError = reason;
      cancelCalls.push('parent');
    });

    const grandChild = child.then(shouldNotCall, (reason) => {
      assertEquals(
          'Child should receive the same cancel error.', cancelError, reason);
      cancelCalls.push('child');
    });

    const descendant = grandChild.then(shouldNotCall, (reason) => {
      assertEquals(
          'GrandChild should receive the same cancel error.', cancelError,
          reason);
      cancelCalls.push('grandChild');

      assertArrayEquals(
          'Each promise in the hierarchy has a single child, so canceling the ' +
              'grandChild should cancel each ancestor in order.',
          ['parent', 'child', 'grandChild'], cancelCalls);

      // Return a non-Error to resolve the cancellation rejection.
      return null;
    });

    grandChild.cancel('grandChild cancel message');
    return descendant;
  },

  testThenVoidCancelPropagationUpward() {
    let cancelError;
    const cancelCalls = [];
    const parent = new GoogPromise(functions.UNDEFINED);

    const child = parent.then(shouldNotCall, (reason) => {
      assertTrue(reason instanceof GoogPromise.CancellationError);
      assertEquals(
          'grandChild cancel message',
          /** @type {!GoogPromise.CancellationError} */ (reason).message);
      cancelError = reason;
      cancelCalls.push('parent');
    });

    const grandChild = child.then(shouldNotCall, (reason) => {
      assertEquals(
          'Child should receive the same cancel error.', cancelError, reason);
      cancelCalls.push('child');
    });

    grandChild.thenVoid(shouldNotCall, (reason) => {
      assertEquals(
          'GrandChild should receive the same cancel error.', cancelError,
          reason);
      cancelCalls.push('grandChild');
    });

    grandChild.cancel('grandChild cancel message');
    return grandChild.thenCatch((reason) => {
      assertEquals(cancelError, reason);
      assertArrayEquals(
          'Each promise in the hierarchy has a single child, so canceling the ' +
              'grandChild should cancel each ancestor in order.',
          ['parent', 'child', 'grandChild'], cancelCalls);

      // Return a non-Error to resolve the cancellation rejection.
      return null;
    });
  },

  testCancelPropagationUpwardWithMultipleChildren() {
    let cancelError;
    const cancelCalls = [];
    const parent = fulfillSoon(GoogPromise, sentinel);

    parent.then((value) => {
      assertEquals(
          'Non-canceled callbacks should be called after a sibling is canceled.',
          sentinel, value);
    });

    const child = parent.then(shouldNotCall, (reason) => {
      assertTrue(reason instanceof GoogPromise.CancellationError);
      assertEquals(
          'grandChild cancel message',
          /** @type {!GoogPromise.CancellationError} */ (reason).message);
      cancelError = reason;
      cancelCalls.push('child');
    });

    const grandChild = child.then(shouldNotCall, (reason) => {
      assertEquals(reason, cancelError);
      cancelCalls.push('grandChild');
    });
    grandChild.cancel('grandChild cancel message');

    return grandChild.then(shouldNotCall, (reason) => {
      assertEquals(reason, cancelError);
      assertArrayEquals(
          'The parent promise has multiple children, so only the child and ' +
              'grandChild should be canceled.',
          ['child', 'grandChild'], cancelCalls);

      // Return a non-Error to resolve the cancellation rejection.
      return null;
    });
  },

  testThenVoidCancelPropagationUpwardWithMultipleChildren() {
    let cancelError;
    const cancelCalls = [];
    const parent = fulfillSoon(GoogPromise, sentinel);

    parent.thenVoid((value) => {
      assertEquals(
          'Non-canceled callbacks should be called after a sibling is canceled.',
          sentinel, value);
    }, shouldNotCall);

    const child = parent.then(shouldNotCall, (reason) => {
      assertTrue(reason instanceof GoogPromise.CancellationError);
      assertEquals(
          'grandChild cancel message',
          /** @type {!GoogPromise.CancellationError} */ (reason).message);
      cancelError = reason;
      cancelCalls.push('child');
    });

    const grandChild = child.then(shouldNotCall, (reason) => {
      assertEquals(reason, cancelError);
      cancelCalls.push('grandChild');
    });
    grandChild.cancel('grandChild cancel message');

    grandChild.thenVoid(shouldNotCall, (reason) => {
      assertEquals(reason, cancelError);
      cancelCalls.push('void grandChild');
    });

    return grandChild.then(shouldNotCall, (reason) => {
      assertEquals(reason, cancelError);
      assertArrayEquals(
          'The parent promise has multiple children, so only the child and ' +
              'grandChildren should be canceled.',
          ['child', 'grandChild', 'void grandChild'], cancelCalls);

      // Return a non-Error to resolve the cancellation rejection.
      return null;
    });
  },

  testCancelRecovery() {
    const cancelCalls = [];

    const parent = fulfillSoon(GoogPromise, sentinel);

    const sibling1 = parent.then((value) => {
      assertEquals(
          'Non-canceled callbacks should be called after a sibling is canceled.',
          sentinel, value);
    });

    const sibling2 = parent.then(shouldNotCall, (reason) => {
      assertTrue(reason instanceof GoogPromise.CancellationError);
      cancelCalls.push('sibling2');
      return sentinel;
    });

    const grandChild = sibling2.then((value) => {
      cancelCalls.push('child');
      assertEquals(
          'Returning a non-cancel value should uncancel the grandChild.', value,
          sentinel);
      assertArrayEquals(['sibling2', 'child'], cancelCalls);
    }, shouldNotCall);

    grandChild.cancel();
    return GoogPromise.all([sibling1, grandChild]);
  },

  testCancellationError() {
    const err = new GoogPromise.CancellationError('cancel message');
    assertTrue(err instanceof Error);
    assertTrue(err instanceof GoogPromise.CancellationError);
    assertFalse(err.reportErrorToServer);
    assertEquals('cancel', err.name);
    assertEquals('cancel message', err.message);
  },

  testMockClock() {
    mockClock.install();

    let resolveA;
    let resolveB;
    const calls = [];

    const p = new GoogPromise((resolve, reject) => {
      resolveA = resolve;
    });

    p.then((value) => {
      assertEquals(sentinel, value);
      calls.push('then');
    });

    const fulfilledChild = p.then((value) => {
                              assertEquals(sentinel, value);
                              return GoogPromise.resolve(1);
                            }).then((value) => {
      assertEquals(1, value);
      calls.push('fulfilledChild');
    });
    assertNotNull(fulfilledChild);

    const rejectedChild = p.then((value) => {
                             assertEquals(sentinel, value);
                             return GoogPromise.reject(2);
                           }).then(shouldNotCall, (reason) => {
      assertEquals(2, reason);
      calls.push('rejectedChild');
    });
    assertNotNull(rejectedChild);

    const unresolvedChild = p.then((value) => {
                               assertEquals(sentinel, value);
                               return new GoogPromise((r) => {
                                 resolveB = r;
                               });
                             }).then((value) => {
      assertEquals(3, value);
      calls.push('unresolvedChild');
    });
    assertNotNull(unresolvedChild);

    resolveA(sentinel);
    assertArrayEquals(
        'Calls must not be resolved until the clock ticks.', [], calls);

    mockClock.tick();
    assertArrayEquals(
        'All resolved Promises should execute in the same timestep.',
        ['then', 'fulfilledChild', 'rejectedChild'], calls);

    resolveB(3);
    assertArrayEquals(
        'New calls must not resolve until the clock ticks.',
        ['then', 'fulfilledChild', 'rejectedChild'], calls);

    mockClock.tick();
    assertArrayEquals(
        'All callbacks should have executed.',
        ['then', 'fulfilledChild', 'rejectedChild', 'unresolvedChild'], calls);
  },

  testHandledRejection() {
    mockClock.install();
    GoogPromise.reject(sentinel).then(shouldNotCall, (reason) => {});

    mockClock.tick();
    assertEquals(0, unhandledRejections.getCallCount());
  },

  testThenVoidHandledRejection() {
    mockClock.install();
    GoogPromise.reject(sentinel).thenVoid(shouldNotCall, (reason) => {});

    mockClock.tick();
    assertEquals(0, unhandledRejections.getCallCount());
  },

  testUnhandledRejection1() {
    mockClock.install();
    GoogPromise.reject(sentinel);

    mockClock.tick();
    assertEquals(1, unhandledRejections.getCallCount());
    const rejectionCall = unhandledRejections.popLastCall();
    assertArrayEquals([sentinel], rejectionCall.getArguments());
    assertEquals(null, rejectionCall.getThis());
  },

  testUnhandledRejection2() {
    mockClock.install();
    GoogPromise.reject(sentinel).then(shouldNotCall);

    mockClock.tick();
    assertEquals(1, unhandledRejections.getCallCount());
    const rejectionCall = unhandledRejections.popLastCall();
    assertArrayEquals([sentinel], rejectionCall.getArguments());
    assertEquals(null, rejectionCall.getThis());
  },

  testThenVoidUnhandledRejection() {
    mockClock.install();
    GoogPromise.reject(sentinel).thenVoid(shouldNotCall);

    mockClock.tick();
    assertEquals(1, unhandledRejections.getCallCount());
    const rejectionCall = unhandledRejections.popLastCall();
    assertArrayEquals([sentinel], rejectionCall.getArguments());
    assertEquals(null, rejectionCall.getThis());
  },

  testUnhandledRejection() {
    const resolver = GoogPromise.withResolver();

    GoogPromise.setUnhandledRejectionHandler((err) => {
      assertEquals(sentinel, err);
      resolver.resolve();
    });
    GoogPromise.reject(sentinel);

    return resolver.promise;
  },

  testUnhandledThrow() {
    const resolver = GoogPromise.withResolver();

    GoogPromise.setUnhandledRejectionHandler((err) => {
      assertEquals(sentinel, err);
      resolver.resolve();
    });
    GoogPromise.resolve().then(() => {
      throw sentinel;
    });

    return resolver.promise;
  },

  testThenVoidUnhandledThrow() {
    const resolver = GoogPromise.withResolver();

    GoogPromise.setUnhandledRejectionHandler((error) => {
      assertEquals(sentinel, error);
      resolver.resolve();
    });

    GoogPromise.resolve().thenVoid(() => {
      throw sentinel;
    });

    return resolver.promise;
  },

  testUnhandledBlockingRejection() {
    mockClock.install();
    const blocker = GoogPromise.reject(sentinel);
    GoogPromise.resolve(blocker);

    mockClock.tick();
    assertEquals(1, unhandledRejections.getCallCount());
    const rejectionCall = unhandledRejections.popLastCall();
    assertArrayEquals([sentinel], rejectionCall.getArguments());
    assertEquals(null, rejectionCall.getThis());
  },

  testUnhandledRejectionAfterThenAlways() {
    mockClock.install();
    const resolver = GoogPromise.withResolver();
    resolver.promise.thenAlways(() => {});
    resolver.reject(sentinel);

    mockClock.tick();
    assertEquals(1, unhandledRejections.getCallCount());
    const rejectionCall = unhandledRejections.popLastCall();
    assertArrayEquals([sentinel], rejectionCall.getArguments());
    assertEquals(null, rejectionCall.getThis());
  },

  testHandledBlockingRejection() {
    mockClock.install();
    const blocker = GoogPromise.reject(sentinel);
    GoogPromise.resolve(blocker).then(shouldNotCall, (reason) => {});

    mockClock.tick();
    assertEquals(0, unhandledRejections.getCallCount());
  },

  testThenVoidHandledBlockingRejection() {
    const shouldCall = recordFunction();

    mockClock.install();
    const blocker = GoogPromise.reject(sentinel);
    GoogPromise.resolve(blocker).thenVoid(shouldNotCall, shouldCall);

    mockClock.tick();
    assertEquals(0, unhandledRejections.getCallCount());
    assertEquals(1, shouldCall.getCallCount());
  },

  testUnhandledRejectionWithTimeout() {
    mockClock.install();
    stubs.replace(GoogPromise, 'UNHANDLED_REJECTION_DELAY', 200);
    GoogPromise.reject(sentinel);

    mockClock.tick(199);
    assertEquals(0, unhandledRejections.getCallCount());

    mockClock.tick(1);
    assertEquals(1, unhandledRejections.getCallCount());
  },

  testHandledRejectionWithTimeout() {
    mockClock.install();
    stubs.replace(GoogPromise, 'UNHANDLED_REJECTION_DELAY', 200);
    const p = GoogPromise.reject(sentinel);

    mockClock.tick(199);
    p.then(shouldNotCall, (reason) => {});

    mockClock.tick(1);
    assertEquals(0, unhandledRejections.getCallCount());
  },

  testUnhandledRejectionDisabled() {
    mockClock.install();
    stubs.replace(GoogPromise, 'UNHANDLED_REJECTION_DELAY', -1);
    GoogPromise.reject(sentinel);

    mockClock.tick();
    assertEquals(0, unhandledRejections.getCallCount());
  },

  async testUnhandledRejectionNotFiredWhenAwaitingARejectedGoogPromise() {
    try {
      await GoogPromise.reject(sentinel);
    } catch (e) {
      // Expected
    }

    // TODO(user): Expect 0 unhandled rejections in all environemnts.
    assertEquals(MICROTASKS_EXIST ? 1 : 0, unhandledRejections.getCallCount());
  },

  testThenableInterface() {
    const promise = new GoogPromise((resolve, reject) => {});
    assertTrue(Thenable.isImplementedBy(promise));

    assertFalse(Thenable.isImplementedBy({}));
    assertFalse(Thenable.isImplementedBy('string'));
    assertFalse(Thenable.isImplementedBy(1));
    assertFalse(Thenable.isImplementedBy({then: function() {}}));

    /** @constructor */
    function T() {}
    T.prototype.then = (opt_a, opt_b, opt_c) => {};
    Thenable.addImplementation(T);
    assertTrue(Thenable.isImplementedBy(new T));

    // Test COMPILED code path.
    try {
      globalThis['COMPILED'] = true;
      /** @constructor */
      function C() {}
      C.prototype.then = (opt_a, opt_b, opt_c) => {};
      Thenable.addImplementation(C);
      assertTrue(Thenable.isImplementedBy(new C));
    } finally {
      globalThis['COMPILED'] = false;
    }
  },

  testCreateWithResolver_Resolved() {
    mockClock.install();
    let timesCalled = 0;

    const resolver = GoogPromise.withResolver();

    resolver.promise.then((value) => {
      timesCalled++;
      assertEquals(sentinel, value);
    }, fail);

    assertEquals(
        'then() must return before callbacks are invoked.', 0, timesCalled);

    mockClock.tick();

    assertEquals(
        'promise is not resolved until resolver is invoked.', 0, timesCalled);

    resolver.resolve(sentinel);

    assertEquals('resolution is delayed until the next tick', 0, timesCalled);

    mockClock.tick();

    assertEquals('onFulfilled must be called exactly once.', 1, timesCalled);
  },

  testCreateWithResolver_Rejected() {
    mockClock.install();
    let timesCalled = 0;

    const resolver = GoogPromise.withResolver();

    resolver.promise.then(fail, (reason) => {
      timesCalled++;
      assertEquals(sentinel, reason);
    });

    assertEquals(
        'then() must return before callbacks are invoked.', 0, timesCalled);

    mockClock.tick();

    assertEquals(
        'promise is not resolved until resolver is invoked.', 0, timesCalled);

    resolver.reject(sentinel);

    assertEquals('resolution is delayed until the next tick', 0, timesCalled);

    mockClock.tick();

    assertEquals('onFulfilled must be called exactly once.', 1, timesCalled);
  },

  /** @suppress {visibility} */
  testLinksBetweenParentsAndChildrenAreCutOnResolve() {
    mockClock.install();
    const parentResolver = GoogPromise.withResolver();
    const parent = parentResolver.promise;
    const child = parent.then(() => {});
    assertNotNull(child.parent_);
    assertEquals(null, parent.callbackEntries_.next);
    parentResolver.resolve();
    mockClock.tick();
    assertNull(child.parent_);
    assertEquals(null, parent.callbackEntries_);
  },

  /** @suppress {visibility} */
  testLinksBetweenParentsAndChildrenAreCutWithUnresolvedChild() {
    mockClock.install();
    const parentResolver = GoogPromise.withResolver();
    const parent = parentResolver.promise;
    const child = parent.then(() => {
      // Will never resolve.
      return new GoogPromise(() => {});
    });
    assertNotNull(child.parent_);
    assertNull(parent.callbackEntries_.next);
    parentResolver.resolve();
    mockClock.tick();
    assertNull(child.parent_);
    assertEquals(null, parent.callbackEntries_);
  },

  /** @suppress {visibility} */
  testLinksBetweenParentsAndChildrenAreCutOnCancel() {
    mockClock.install();
    const parent = new GoogPromise(() => {});
    const child = parent.then(() => {});
    const grandChild = child.then(() => {});
    assertEquals(null, child.callbackEntries_.next);
    assertNotNull(child.parent_);
    assertEquals(null, parent.callbackEntries_.next);
    parent.cancel();
    mockClock.tick();
    assertNull(child.parent_);
    assertNull(grandChild.parent_);
    assertEquals(null, parent.callbackEntries_);
    assertEquals(null, child.callbackEntries_);
  },
});
