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