promise_test.js

// Copyright 2013 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

goog.provide('goog.PromiseTest');

goog.require('goog.Promise');
goog.require('goog.Thenable');
goog.require('goog.functions');
goog.require('goog.testing.AsyncTestCase');
goog.require('goog.testing.MockClock');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.recordFunction');

goog.setTestOnly('goog.PromiseTest');


// TODO(brenneman):
// - Add tests for interoperability with native Promises where available.
// - Make most tests use the MockClock (though some tests should still verify
//   real asynchronous behavior.
// - Add tests for long stack traces.


var mockClock;
var asyncTestCase = goog.testing.AsyncTestCase.createAndInstall(document.title);
var stubs = new goog.testing.PropertyReplacer();
var unhandledRejections;


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


function setUpPage() {
  asyncTestCase.stepTimeout = 200;
  mockClock = new goog.testing.MockClock();
}


function setUp() {
  unhandledRejections = goog.testing.recordFunction();
  goog.Promise.setUnhandledRejectionHandler(unhandledRejections);
}


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


function tearDownPage() {
  goog.dispose(mockClock);
}


function continueTesting() {
  asyncTestCase.continueTesting();
}


/**
 * 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) + ')');
}


function fulfillSoon(value, delay) {
  return new goog.Promise(function(resolve, reject) {
    window.setTimeout(function() {
      resolve(value);
    }, delay);
  });
}


function rejectSoon(reason, delay) {
  return new goog.Promise(function(resolve, reject) {
    window.setTimeout(function() {
      reject(reason);
    }, delay);
  });
}


function testThenIsFulfilled() {
  asyncTestCase.waitForAsync();
  var timesCalled = 0;

  var p = new goog.Promise(function(resolve, reject) {
    resolve(sentinel);
  });
  p.then(function(value) {
    timesCalled++;
    assertEquals(sentinel, value);
    assertEquals('onFulfilled must be called exactly once.', 1, timesCalled);
  });
  p.thenAlways(continueTesting);

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


function testThenIsRejected() {
  asyncTestCase.waitForAsync();
  var timesCalled = 0;

  var p = new goog.Promise(function(resolve, reject) {
    reject(sentinel);
  });
  p.then(shouldNotCall, function(value) {
    timesCalled++;
    assertEquals(sentinel, value);
    assertEquals('onRejected must be called exactly once.', 1, timesCalled);
  });
  p.thenAlways(continueTesting);

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

function testThenAsserts() {
  var p = goog.Promise.resolve();

  var m = assertThrows(function() {
    p.then({});
  });
  assertContains('opt_onFulfilled should be a function.', m.message);

  m = assertThrows(function() {
    p.then(function() {}, {});
  });
  assertContains('opt_onRejected should be a function.', m.message);
}


function testOptionalOnFulfilled() {
  asyncTestCase.waitForAsync();

  goog.Promise.resolve(sentinel).
      then(null, null).
      then(null, shouldNotCall).
      then(function(value) {
        assertEquals(sentinel, value);
      }).
      thenAlways(continueTesting);
}


function testOptionalOnRejected() {
  asyncTestCase.waitForAsync();

  goog.Promise.reject(sentinel).
      then(null, null).
      then(shouldNotCall).
      then(null, function(reason) {
        assertEquals(sentinel, reason);
      }).
      thenAlways(continueTesting);
}


function testMultipleResolves() {
  asyncTestCase.waitForAsync();
  var timesCalled = 0;
  var resolvePromise;

  var p = new goog.Promise(function(resolve, reject) {
    resolvePromise = resolve;
    resolve('foo');
    resolve('bar');
  });

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

  // Add one more test for fulfilling after a delay.
  window.setTimeout(function() {
    resolvePromise('baz');
    assertEquals(1, timesCalled);
    continueTesting();
  }, 10);
}


function testMultipleRejects() {
  asyncTestCase.waitForAsync();
  var timesCalled = 0;
  var rejectPromise;

  var p = new goog.Promise(function(resolve, reject) {
    rejectPromise = reject;
    reject('foo');
    reject('bar');
  });

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

  // Add one more test for rejecting after a delay.
  window.setTimeout(function() {
    rejectPromise('baz');
    assertEquals(1, timesCalled);
    continueTesting();
  }, 10);
}


function testAsynchronousThenCalls() {
  asyncTestCase.waitForAsync();
  var timesCalled = [0, 0, 0, 0];
  var p = new goog.Promise(function(resolve, reject) {
    window.setTimeout(function() {
      resolve();
    }, 30);
  });

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

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

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

  window.setTimeout(function() {
    p.then(function() {
      timesCalled[3]++;
      assertArrayEquals([1, 1, 1, 1], timesCalled);
    });
    p.thenAlways(continueTesting);
  }, 40);
}


function testResolveWithPromise() {
  asyncTestCase.waitForAsync();
  var resolveBlocker;
  var hasFulfilled = false;
  var blocker = new goog.Promise(function(resolve, reject) {
    resolveBlocker = resolve;
  });

  var p = goog.Promise.resolve(blocker);
  p.then(function(value) {
    hasFulfilled = true;
    assertEquals(sentinel, value);
  }, shouldNotCall);
  p.thenAlways(function() {
    assertTrue(hasFulfilled);
    continueTesting();
  });

  assertFalse(hasFulfilled);
  resolveBlocker(sentinel);
}


function testResolveWithRejectedPromise() {
  asyncTestCase.waitForAsync();
  var rejectBlocker;
  var hasRejected = false;
  var blocker = new goog.Promise(function(resolve, reject) {
    rejectBlocker = reject;
  });

  var p = goog.Promise.resolve(blocker);
  p.then(shouldNotCall, function(reason) {
    hasRejected = true;
    assertEquals(sentinel, reason);
  });
  p.thenAlways(function() {
    assertTrue(hasRejected);
    continueTesting();
  });

  assertFalse(hasRejected);
  rejectBlocker(sentinel);
}


function testRejectWithPromise() {
  asyncTestCase.waitForAsync();
  var resolveBlocker;
  var hasFulfilled = false;
  var blocker = new goog.Promise(function(resolve, reject) {
    resolveBlocker = resolve;
  });

  var p = goog.Promise.reject(blocker);
  p.then(function(value) {
    hasFulfilled = true;
    assertEquals(sentinel, value);
  }, shouldNotCall);
  p.thenAlways(function() {
    assertTrue(hasFulfilled);
    continueTesting();
  });

  assertFalse(hasFulfilled);
  resolveBlocker(sentinel);
}


function testRejectWithRejectedPromise() {
  asyncTestCase.waitForAsync();
  var rejectBlocker;
  var hasRejected = false;
  var blocker = new goog.Promise(function(resolve, reject) {
    rejectBlocker = reject;
  });

  var p = goog.Promise.reject(blocker);
  p.then(shouldNotCall, function(reason) {
    hasRejected = true;
    assertEquals(sentinel, reason);
  });
  p.thenAlways(function() {
    assertTrue(hasRejected);
    continueTesting();
  });

  assertFalse(hasRejected);
  rejectBlocker(sentinel);
}


function testResolveAndReject() {
  asyncTestCase.waitForAsync();
  var onFulfilledCalled = false;
  var onRejectedCalled = false;
  var p = new goog.Promise(function(resolve, reject) {
    resolve();
    reject();
  });

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

  p.thenAlways(function() {
    assertTrue(onFulfilledCalled);
    assertFalse(onRejectedCalled);
    continueTesting();
  });
}


function testRejectAndResolve() {
  asyncTestCase.waitForAsync();
  var onFulfilledCalled = false;
  var onRejectedCalled = false;
  var p = new goog.Promise(function(resolve, reject) {
    reject();
    resolve();
  });

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

  p.thenAlways(function() {
    assertTrue(onRejectedCalled);
    assertFalse(onFulfilledCalled);
    continueTesting();
  });
}


function testThenReturnsBeforeCallbackWithFulfill() {
  asyncTestCase.waitForAsync();
  var thenHasReturned = false;
  var p = goog.Promise.resolve();

  p.then(function() {
    assertTrue(
        'Callback must be called only after then() has returned.',
        thenHasReturned);
  });
  p.thenAlways(continueTesting);
  thenHasReturned = true;
}


function testThenReturnsBeforeCallbackWithReject() {
  asyncTestCase.waitForAsync();
  var thenHasReturned = false;
  var p = goog.Promise.reject();

  p.then(null, function() {
    assertTrue(thenHasReturned);
  });
  p.thenAlways(continueTesting);
  thenHasReturned = true;
}


function testResolutionOrder() {
  asyncTestCase.waitForAsync();
  var callbacks = [];
  var p = goog.Promise.resolve();

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

  p.then(function() {
    assertArrayEquals([1, 2, 3], callbacks);
  });
  p.thenAlways(continueTesting);
}


function testResolutionOrderWithThrow() {
  asyncTestCase.waitForAsync();
  var callbacks = [];
  var p = goog.Promise.resolve();

  p.then(function() { callbacks.push(1); }, shouldNotCall);
  var child = p.then(function() {
    callbacks.push(2);
    throw Error();
  }, shouldNotCall);

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

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

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

  p.thenAlways(continueTesting);
}


function testResolutionOrderWithNestedThen() {
  asyncTestCase.waitForAsync();
  var callbacks = [];
  var p = goog.Promise.resolve();

  p.then(function() {
    callbacks.push(1);
    p.then(function() {
      callbacks.push(3);
    });
  });
  p.then(function() { callbacks.push(2); });

  window.setTimeout(function() {
    assertArrayEquals([1, 2, 3], callbacks);
    continueTesting();
  }, 100);
}


function testRejectionOrder() {
  asyncTestCase.waitForAsync();
  var callbacks = [];
  var p = goog.Promise.reject();

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

  p.then(shouldNotCall, function() {
    assertArrayEquals([1, 2, 3], callbacks);
  });
  p.thenAlways(continueTesting);
}


function testRejectionOrderWithThrow() {
  asyncTestCase.waitForAsync();
  var callbacks = [];
  var p = goog.Promise.reject();

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

  p.then(shouldNotCall, function() {
    assertArrayEquals([1, 2, 3], callbacks);
  });
  p.thenAlways(continueTesting);
}


function testRejectionOrderWithNestedThen() {
  asyncTestCase.waitForAsync();
  var callbacks = [];

  var p = goog.Promise.reject();

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

  window.setTimeout(function() {
    assertArrayEquals([1, 2, 3], callbacks);
    continueTesting();
  }, 0);
}


function testBranching() {
  asyncTestCase.waitForSignals(3);
  var p = goog.Promise.resolve(2);

  p.then(function(value) {
    assertEquals('then functions should see the same value', 2, value);
    return value / 2;
  }).then(function(value) {
    assertEquals('branch should receive the returned value', 1, value);
    asyncTestCase.signal();
  });

  p.then(function(value) {
    assertEquals('then functions should see the same value', 2, value);
    throw value + 1;
  }).then(shouldNotCall, function(reason) {
    assertEquals('branch should receive the thrown value', 3, reason);
    asyncTestCase.signal();
  });

  p.then(function(value) {
    assertEquals('then functions should see the same value', 2, value);
    return value * 2;
  }).then(function(value) {
    assertEquals('branch should receive the returned value', 4, value);
    asyncTestCase.signal();
  });
}


function testThenReturnsPromise() {
  var parent = goog.Promise.resolve();
  var child = parent.then();

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


function testBlockingPromise() {
  asyncTestCase.waitForAsync();
  var p = goog.Promise.resolve();
  var wasFulfilled = false;
  var wasRejected = false;

  var p2 = p.then(function() {
    return new goog.Promise(function(resolve, reject) {});
  });

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

  window.setTimeout(function() {
    assertFalse('p2 should be blocked on the returned Promise', wasFulfilled);
    assertFalse('p2 should be blocked on the returned Promise', wasRejected);
    continueTesting();
  }, 100);
}


function testBlockingPromiseFulfilled() {
  asyncTestCase.waitForAsync();
  var blockingPromise = new goog.Promise(function(resolve, reject) {
    window.setTimeout(function() {
      resolve(sentinel);
    }, 0);
  });

  var p = goog.Promise.resolve(dummy);
  var p2 = p.then(function(value) {
    return blockingPromise;
  });

  p2.then(function(value) {
    assertEquals(sentinel, value);
  }).thenAlways(continueTesting);
}


function testBlockingPromiseRejected() {
  asyncTestCase.waitForAsync();
  var blockingPromise = new goog.Promise(function(resolve, reject) {
    window.setTimeout(function() {
      reject(sentinel);
    }, 0);
  });

  var p = goog.Promise.resolve(blockingPromise);

  p.then(shouldNotCall, function(reason) {
    assertEquals(sentinel, reason);
  }).thenAlways(continueTesting);
}


function testBlockingThenableFulfilled() {
  asyncTestCase.waitForAsync();
  var thenable = {
    then: function(onFulfill, onReject) { onFulfill(sentinel); }
  };

  var p = goog.Promise.resolve(thenable).
      then(function(reason) {
        assertEquals(sentinel, reason);
      }, shouldNotCall).thenAlways(continueTesting);
}


function testBlockingThenableRejected() {
  asyncTestCase.waitForAsync();
  var thenable = {
    then: function(onFulfill, onReject) { onReject(sentinel); }
  };

  var p = goog.Promise.resolve(thenable).
      then(shouldNotCall, function(reason) {
        assertEquals(sentinel, reason);
      }).thenAlways(continueTesting);
}


function testBlockingThenableThrows() {
  asyncTestCase.waitForAsync();
  var thenable = {
    then: function(onFulfill, onReject) { throw sentinel; }
  };

  var p = goog.Promise.resolve(thenable).
      then(shouldNotCall, function(reason) {
        assertEquals(sentinel, reason);
      }).thenAlways(continueTesting);
}


function testBlockingThenableMisbehaves() {
  asyncTestCase.waitForAsync();
  var thenable = {
    then: function(onFulfill, onReject) {
      onFulfill(sentinel);
      onFulfill(dummy);
      onReject(dummy);
      throw dummy;
    }
  };

  var p = goog.Promise.resolve(thenable).
      then(function(value) {
        assertEquals(
            'Only the first resolution of the Thenable should have a result.',
            sentinel, value);
      }, shouldNotCall).thenAlways(continueTesting);
}


function testNestingThenables() {
  asyncTestCase.waitForAsync();
  var thenableA = {
    then: function(onFulfill, onReject) { onFulfill(sentinel); }
  };
  var thenableB = {
    then: function(onFulfill, onReject) { onFulfill(thenableA); }
  };
  var thenableC = {
    then: function(onFulfill, onReject) { onFulfill(thenableB); }
  };

  var p = goog.Promise.resolve(thenableC).
      then(function(value) {
        assertEquals(
            'Should resolve to the fulfillment value of thenableA',
            sentinel, value);
      }, shouldNotCall).thenAlways(continueTesting);
}


function testNestingThenablesRejected() {
  asyncTestCase.waitForAsync();
  var thenableA = {
    then: function(onFulfill, onReject) { onReject(sentinel); }
  };
  var thenableB = {
    then: function(onFulfill, onReject) { onReject(thenableA); }
  };
  var thenableC = {
    then: function(onFulfill, onReject) { onReject(thenableB); }
  };

  var p = goog.Promise.reject(thenableC).
      then(shouldNotCall, function(reason) {
        assertEquals(
            'Should resolve to rejection reason of thenableA',
            sentinel, reason);
      }).thenAlways(continueTesting);
}


function testThenCatch() {
  asyncTestCase.waitForAsync();
  var catchCalled = false;
  var p = goog.Promise.reject();

  var p2 = p.thenCatch(function(reason) {
    catchCalled = true;
    return sentinel;
  });

  p2.then(function(value) {
    assertTrue(catchCalled);
    assertEquals(sentinel, value);
  }, shouldNotCall);
  p2.thenAlways(continueTesting);
}


function testRaceWithEmptyList() {
  asyncTestCase.waitForAsync();
  goog.Promise.race([]).then(function(value) {
    assertUndefined(value);
  }).thenAlways(continueTesting);
}


function testRaceWithFulfill() {
  asyncTestCase.waitForAsync();

  var a = fulfillSoon('a', 40);
  var b = fulfillSoon('b', 30);
  var c = fulfillSoon('c', 10);
  var d = fulfillSoon('d', 20);

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


function testRaceWithReject() {
  asyncTestCase.waitForAsync();

  var a = rejectSoon('rejected-a', 40);
  var b = rejectSoon('rejected-b', 30);
  var c = rejectSoon('rejected-c', 10);
  var d = rejectSoon('rejected-d', 20);

  var p = goog.Promise.race([a, b, c, d]).
      then(shouldNotCall, function(value) {
        assertEquals('rejected-c', value);
        return a;
      }).
      then(shouldNotCall, function(reason) {
        assertEquals('The slowest promise should resolve eventually.',
                     'rejected-a', reason);
      }).thenAlways(continueTesting);
}


function testAllWithEmptyList() {
  asyncTestCase.waitForAsync();
  goog.Promise.all([]).then(function(value) {
    assertArrayEquals([], value);
  }).thenAlways(continueTesting);
}


function testAllWithFulfill() {
  asyncTestCase.waitForAsync();

  var a = fulfillSoon('a', 40);
  var b = fulfillSoon('b', 30);
  var c = fulfillSoon('c', 10);
  var d = fulfillSoon('d', 20);

  goog.Promise.all([a, b, c, d]).then(function(value) {
    assertArrayEquals(['a', 'b', 'c', 'd'], value);
  }).thenAlways(continueTesting);
}


function testAllWithReject() {
  asyncTestCase.waitForAsync();

  var a = fulfillSoon('a', 40);
  var b = rejectSoon('rejected-b', 30);
  var c = fulfillSoon('c', 10);
  var d = fulfillSoon('d', 20);

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


function testFirstFulfilledWithEmptyList() {
  asyncTestCase.waitForAsync();
  goog.Promise.firstFulfilled([]).then(function(value) {
    assertUndefined(value);
  }).thenAlways(continueTesting);
}


function testFirstFulfilledWithFulfill() {
  asyncTestCase.waitForAsync();

  var a = fulfillSoon('a', 40);
  var b = rejectSoon('rejected-b', 30);
  var c = rejectSoon('rejected-c', 10);
  var d = fulfillSoon('d', 20);

  goog.Promise.firstFulfilled([a, b, c, d]).
      then(function(value) {
        assertEquals('d', value);
        return c;
      }).
      then(shouldNotCall, function(reason) {
        assertEquals(
            'Promise "c" should have been rejected before the some() resolved.',
            'rejected-c', reason);
        return a;
      }).
      then(function(reason) {
        assertEquals(
            'Promise "a" should be fulfilled even after some() has resolved.',
            'a', value);
      }, shouldNotCall).thenAlways(continueTesting);
}


function testFirstFulfilledWithReject() {
  asyncTestCase.waitForAsync();

  var a = rejectSoon('rejected-a', 40);
  var b = rejectSoon('rejected-b', 30);
  var c = rejectSoon('rejected-c', 10);
  var d = rejectSoon('rejected-d', 20);

  var p = goog.Promise.firstFulfilled([a, b, c, d]).
      then(shouldNotCall, function(reason) {
        assertArrayEquals(
            ['rejected-a', 'rejected-b', 'rejected-c', 'rejected-d'], reason);
      }).thenAlways(continueTesting);
}


function testThenAlwaysWithFulfill() {
  asyncTestCase.waitForAsync();
  var p = goog.Promise.resolve().
      thenAlways(function() {
        assertEquals(0, arguments.length);
      }).
      then(continueTesting, shouldNotCall);
}


function testThenAlwaysWithReject() {
  asyncTestCase.waitForAsync();
  var p = goog.Promise.reject().
      thenAlways(function() {
        assertEquals(0, arguments.length);
      }).
      then(shouldNotCall, continueTesting);
}


function testThenAlwaysCalledMultipleTimes() {
  asyncTestCase.waitForAsync();
  var calls = [];

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


function testContextWithInit() {
  var initContext;
  var p = new goog.Promise(function(resolve, reject) {
    initContext = this;
  }, sentinel);
  assertEquals(sentinel, initContext);
}


function testContextWithInitDefault() {
  var initContext;
  var p = new goog.Promise(function(resolve, reject) {
    initContext = this;
  });
  assertEquals(
      'initFunc should default to being called in the global scope',
      goog.global, initContext);
}


function testContextWithFulfillment() {
  asyncTestCase.waitForAsync();
  var context = sentinel;
  var p = goog.Promise.resolve();

  p.then(function() {
    assertEquals(
        'Call should be made in the global scope if no context is specified.',
        goog.global, this);
  });
  p.then(function() {
    assertEquals(sentinel, this);
  }, shouldNotCall, sentinel);
  p.thenAlways(function() {
    assertEquals(sentinel, this);
    continueTesting();
  }, sentinel);
}


function testContextWithRejection() {
  asyncTestCase.waitForAsync();
  var context = sentinel;
  var p = goog.Promise.reject();

  p.then(shouldNotCall, function() {
    assertEquals(
        'Call should be made in the global scope if no context is specified.',
        goog.global, this);
  });
  p.then(shouldNotCall, function() {
    assertEquals(sentinel, this);
  }, sentinel);
  p.thenCatch(function() {
    assertEquals(sentinel, this);
  }, sentinel);
  p.thenAlways(function() {
    assertEquals(sentinel, this);
    continueTesting();
  }, sentinel);
}


function testCancel() {
  asyncTestCase.waitForAsync();
  var p = new goog.Promise(goog.nullFunction);
  p.then(shouldNotCall, function(reason) {
    assertTrue(reason instanceof goog.Promise.CancellationError);
    assertEquals('cancellation message', reason.message);
    continueTesting();
  });
  p.cancel('cancellation message');
}


function testCancelAfterResolve() {
  asyncTestCase.waitForAsync();
  var p = goog.Promise.resolve();
  p.cancel();
  p.then(null, shouldNotCall);
  p.thenAlways(continueTesting);
}


function testCancelAfterReject() {
  asyncTestCase.waitForAsync();
  var p = goog.Promise.reject(sentinel);
  p.cancel();
  p.then(shouldNotCall, function(reason) {
    assertEquals(sentinel, reason);
    continueTesting();
  });
}


function testCancelPropagation() {
  asyncTestCase.waitForSignals(2);
  var cancelError;
  var p = new goog.Promise(goog.nullFunction);

  var p2 = p.then(shouldNotCall, function(reason) {
    cancelError = reason;
    assertTrue(reason instanceof goog.Promise.CancellationError);
    assertEquals('parent cancel message', reason.message);
    return sentinel;
  });
  p2.then(function(value) {
    assertEquals(
        'Child promises should receive the returned value of the parent.',
        sentinel, value);
    asyncTestCase.signal();
  }, shouldNotCall);

  var p3 = p.then(shouldNotCall, function(reason) {
    assertEquals(
        'Every onRejected handler should receive the same cancel error.',
        cancelError, reason);
    assertEquals('parent cancel message', reason.message);
    asyncTestCase.signal();
  });

  p.cancel('parent cancel message');
}


function testCancelPropagationUpward() {
  asyncTestCase.waitForAsync();
  var cancelError;
  var cancelCalls = [];
  var parent = new goog.Promise(goog.nullFunction);

  var child = parent.then(shouldNotCall, function(reason) {
    assertTrue(reason instanceof goog.Promise.CancellationError);
    assertEquals('grandChild cancel message', reason.message);
    cancelError = reason;
    cancelCalls.push('parent');
  });

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

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

  grandChild.then(shouldNotCall, function(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);
  }).thenAlways(continueTesting);

  grandChild.cancel('grandChild cancel message');
}


function testCancelPropagationUpwardWithMultipleChildren() {
  asyncTestCase.waitForAsync();
  var cancelError;
  var cancelCalls = [];
  var parent = fulfillSoon(sentinel, 0);

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

  var child = parent.then(shouldNotCall, function(reason) {
    assertTrue(reason instanceof goog.Promise.CancellationError);
    assertEquals('grandChild cancel message', reason.message);
    cancelError = reason;
    cancelCalls.push('child');
  });

  var grandChild = child.then(shouldNotCall, function(reason) {
    assertEquals(reason, cancelError);
    cancelCalls.push('grandChild');
  });

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

  grandChild.cancel('grandChild cancel message');
}


function testCancelRecovery() {
  asyncTestCase.waitForSignals(2);
  var cancelError;
  var cancelCalls = [];

  var parent = fulfillSoon(sentinel, 100);

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

  var sibling2 = parent.then(shouldNotCall, function(reason) {
    assertTrue(reason instanceof goog.Promise.CancellationError);
    cancelError = reason;
    cancelCalls.push('sibling2');
    return sentinel;
  });

  parent.thenAlways(function() {
    asyncTestCase.signal();
  });

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

  grandChild.cancel();
}


function testCancellationError() {
  var err = new goog.Promise.CancellationError('cancel message');
  assertTrue(err instanceof Error);
  assertTrue(err instanceof goog.Promise.CancellationError);
  assertEquals('cancel', err.name);
  assertEquals('cancel message', err.message);
}


function testMockClock() {
  mockClock.install();

  var resolveA;
  var resolveB;
  var calls = [];

  var p = new goog.Promise(function(resolve, reject) {
    resolveA = resolve;
  });

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

  var fulfilledChild = p.then(function(value) {
    assertEquals(sentinel, value);
    return goog.Promise.resolve(1);
  }).then(function(value) {
    assertEquals(1, value);
    calls.push('fulfilledChild');

  });

  var rejectedChild = p.then(function(value) {
    assertEquals(sentinel, value);
    return goog.Promise.reject(2);
  }).then(shouldNotCall, function(reason) {
    assertEquals(2, reason);
    calls.push('rejectedChild');
  });

  var unresolvedChild = p.then(function(value) {
    assertEquals(sentinel, value);
    return new goog.Promise(function(r) {
      resolveB = r;
    });
  }).then(function(value) {
    assertEquals(3, value);
    calls.push('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);
}


function testHandledRejection() {
  mockClock.install();
  goog.Promise.reject(sentinel).then(shouldNotCall, function(reason) {});

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


function testUnhandledRejection() {
  mockClock.install();
  goog.Promise.reject(sentinel);

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


function testUnhandledRejection_asyncTestCase() {
  goog.Promise.reject(sentinel);

  goog.Promise.setUnhandledRejectionHandler(function(error) {
    assertEquals(sentinel, error);
    asyncTestCase.continueTesting();
  });
}


function testUnhandledThrow_asyncTestCase() {
  goog.Promise.resolve().then(function() {
    throw sentinel;
  });

  goog.Promise.setUnhandledRejectionHandler(function(error) {
    assertEquals(sentinel, error);
    asyncTestCase.continueTesting();
  });
}


function testUnhandledBlockingRejection() {
  mockClock.install();
  var blocker = goog.Promise.reject(sentinel);
  goog.Promise.resolve(blocker);

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


function testHandledBlockingRejection() {
  mockClock.install();
  var blocker = goog.Promise.reject(sentinel);
  goog.Promise.resolve(blocker).then(shouldNotCall, function(reason) {});

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


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

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

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


function testHandledRejectionWithTimeout() {
  mockClock.install();
  stubs.replace(goog.Promise, 'UNHANDLED_REJECTION_DELAY', 200);
  var p = goog.Promise.reject(sentinel);

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

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


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

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


function testThenableInterface() {
  var promise = new goog.Promise(function(resolve, reject) {});
  assertTrue(goog.Thenable.isImplementedBy(promise));

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

  function T() {}
  T.prototype.then = function(opt_a, opt_b, opt_c) {};
  goog.Thenable.addImplementation(T);
  assertTrue(goog.Thenable.isImplementedBy(new T));

  // Test COMPILED code path.
  try {
    COMPIlED = true;
    function C() {}
    C.prototype.then = function(opt_a, opt_b, opt_c) {};
    goog.Thenable.addImplementation(C);
    assertTrue(goog.Thenable.isImplementedBy(new C));
  } finally {
    COMPILED = false;
  }
}


function testCreateWithResolver_Resolved() {
  mockClock.install();
  var timesCalled = 0;

  var resolver = goog.Promise.withResolver();

  resolver.promise.then(function(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);
}


function testCreateWithResolver_Rejected() {
  mockClock.install();
  var timesCalled = 0;

  var resolver = goog.Promise.withResolver();

  resolver.promise.then(fail, function(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);
}