blob: 707617dfd32e3a3685b692ed12a443f42ada3cdf [file] [log] [blame]
// 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 testUnhandledRejectionAfterThenAlways() {
mockClock.install();
var resolver = goog.Promise.withResolver();
resolver.promise.thenAlways(function() {});
resolver.reject(sentinel);
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);
}
function testLinksBetweenParentsAndChildrenAreCutOnResolve() {
mockClock.install();
var parentResolver = goog.Promise.withResolver();
var parent = parentResolver.promise;
var child = parent.then(function() {});
assertNotNull(child.parent_);
assertEquals(1, parent.callbackEntries_.length);
parentResolver.resolve();
mockClock.tick();
assertNull(child.parent_);
assertEquals(null, parent.callbackEntries_);
}
function testLinksBetweenParentsAndChildrenAreCutWithUnresolvedChild() {
mockClock.install();
var parentResolver = goog.Promise.withResolver();
var parent = parentResolver.promise;
var child = parent.then(function() {
// Will never resolve.
return new goog.Promise(function() {});
});
assertNotNull(child.parent_);
assertEquals(1, parent.callbackEntries_.length);
parentResolver.resolve();
mockClock.tick();
assertNull(child.parent_);
assertEquals(null, parent.callbackEntries_);
}
function testLinksBetweenParentsAndChildrenAreCutOnCancel() {
mockClock.install();
var parent = new goog.Promise(function() {});
var child = parent.then(function() {});
var grandChild = child.then(function() {});
assertEquals(1, child.callbackEntries_.length);
assertNotNull(child.parent_);
assertEquals(1, parent.callbackEntries_.length);
parent.cancel();
mockClock.tick();
assertNull(child.parent_);
assertNull(grandChild.parent_);
assertEquals(null, parent.callbackEntries_);
assertEquals(null, child.callbackEntries_);
}