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