1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
7 /** @suppress {duplicate} */
12 * A wrapper around a Promise object that keeps track of all
13 * outstanding promises. This function is written to serve as a
14 * drop-in replacement for the native Promise constructor. To create
15 * a SpyPromise from an existing native Promise, use
18 * Note that this is a pseudo-constructor that actually returns a
19 * regular promise with appropriate handlers attached. This detail
20 * should be transparent when SpyPromise.activate has been called.
22 * The normal way to use this class is within a call to
23 * SpyPromise.run, for example:
25 * base.SpyPromise.run(function() {
26 * myCodeThatUsesPromises();
28 * base.SpyPromise.settleAll().then(function() {
29 * console.log('All promises have been settled!');
34 * @param {function(function(?):?, function(*):?):?} func A function
35 * of the same type used as an argument to the native Promise
36 * constructor, in other words, a function which is called
37 * immediately, and whose arguments are a resolve function and a
40 base.SpyPromise = function(func) {
41 var unsettled = new RealPromise(func);
42 var unsettledId = remember(unsettled);
43 return unsettled.then(function(/** * */value) {
53 * The real promise constructor. Needed because it is normally hidden
54 * by SpyPromise.activate or SpyPromise.run.
57 var RealPromise = Promise;
60 * The real window.setTimeout method. Needed because some test
61 * frameworks like to replace this method with a fake implementation.
64 var realSetTimeout = window.setTimeout.bind(window);
67 * The number of unsettled promises.
70 base.SpyPromise.unsettledCount; // initialized by reset()
73 * A collection of all unsettled promises.
74 * @type {!Object<number,!Promise>}
76 var unsettled; // initialized by reset()
79 * A counter used to assign ID numbers to new SpyPromise objects.
82 var nextPromiseId; // initialized by reset()
85 * A promise returned by SpyPromise.settleAll.
86 * @type {Promise<null>}
88 var settleAllPromise; // initialized by reset()
91 * Records an unsettled promise.
93 * @param {!Promise} unsettledPromise
94 * @return {number} The ID number to be passed to forget_.
96 function remember(unsettledPromise) {
97 var id = nextPromiseId++;
98 if (unsettled[id] != null) {
99 throw Error('Duplicate ID: ' + id);
101 base.SpyPromise.unsettledCount++;
102 unsettled[id] = unsettledPromise;
107 * Forgets a promise. Called after the promise has been settled.
112 function forget(id) {
113 base.debug.assert(unsettled[id] != null);
114 base.SpyPromise.unsettledCount--;
115 delete unsettled[id];
119 * Forgets about all unsettled promises.
121 base.SpyPromise.reset = function() {
122 base.SpyPromise.unsettledCount = 0;
125 settleAllPromise = null;
128 // Initialize static variables.
129 base.SpyPromise.reset();
132 * Tries to wait until all promises has been settled.
134 * @param {number=} opt_maxTimeMs The maximum number of milliseconds
135 * (approximately) to wait (default: 1000).
136 * @return {!Promise<null>} A real promise that is resolved when all
137 * SpyPromises have been settled, or rejected after opt_maxTimeMs
138 * milliseconds have elapsed.
140 base.SpyPromise.settleAll = function(opt_maxTimeMs) {
141 if (settleAllPromise) {
142 return settleAllPromise;
145 var maxDelay = opt_maxTimeMs == null ? 1000 : opt_maxTimeMs;
148 * @param {number} count
149 * @param {number} totalDelay
150 * @return {!Promise<null>}
152 function loop(count, totalDelay) {
153 return new RealPromise(function(resolve, reject) {
154 if (base.SpyPromise.unsettledCount == 0) {
155 settleAllPromise = null;
157 } else if (totalDelay > maxDelay) {
158 settleAllPromise = null;
159 base.SpyPromise.reset();
160 reject(new Error('base.SpyPromise.settleAll timed out'));
162 // This implements quadratic backoff according to Euler's
163 // triangular number formula.
166 // Must jump through crazy hoops to get a real timer in a unit test.
167 realSetTimeout(function() {
170 delay + totalDelay));
176 // An extra promise needed here to prevent the loop function from
177 // finishing before settleAllPromise is set. If that happens,
178 // settleAllPromise will never be reset to null.
179 settleAllPromise = RealPromise.resolve().then(function() {
182 return settleAllPromise;
186 * Only for testing this class. Do not use.
187 * @returns {boolean} True if settleAll is executing.
189 base.SpyPromise.isSettleAllRunning = function() {
190 return settleAllPromise != null;
194 * Wrapper for Promise.resolve.
197 * @return {!base.SpyPromise}
199 base.SpyPromise.resolve = function(value) {
200 return new base.SpyPromise(function(resolve, reject) {
206 * Wrapper for Promise.reject.
209 * @return {!base.SpyPromise}
211 base.SpyPromise.reject = function(value) {
212 return new base.SpyPromise(function(resolve, reject) {
218 * Wrapper for Promise.all.
220 * @param {!Array<Promise>} promises
221 * @return {!base.SpyPromise}
223 base.SpyPromise.all = function(promises) {
224 return base.SpyPromise.resolve(RealPromise.all(promises));
228 * Wrapper for Promise.race.
230 * @param {!Array<Promise>} promises
231 * @return {!base.SpyPromise}
233 base.SpyPromise.race = function(promises) {
234 return base.SpyPromise.resolve(RealPromise.race(promises));
238 * Sets Promise = base.SpyPromise. Must not be called more than once
239 * without an intervening call to restore().
241 base.SpyPromise.activate = function() {
242 if (settleAllPromise) {
243 throw Error('called base.SpyPromise.activate while settleAll is running');
245 if (Promise === base.SpyPromise) {
246 throw Error('base.SpyPromise is already active');
248 Promise = base.SpyPromise;
252 * Restores the original value of Promise.
254 base.SpyPromise.restore = function() {
255 if (settleAllPromise) {
256 throw Error('called base.SpyPromise.restore while settleAll is running');
258 if (Promise === base.SpyPromise) {
259 Promise = RealPromise;
260 } else if (Promise === RealPromise) {
261 throw new Error('base.SpyPromise is not active.');
263 throw new Error('Something fishy is going on.');
268 * Calls func with Promise equal to base.SpyPromise.
270 * @param {function():void} func A function which is expected to
271 * create one or more promises.
272 * @param {number=} opt_timeoutMs An optional timeout specifying how
273 * long to wait for promise chains started in func to be settled.
275 * @return {!Promise<null>} A promise that is resolved after every
276 * promise chain started in func is fully settled, or rejected
277 * after a opt_timeoutMs. In any case, the original value of the
278 * Promise constructor is restored before this promise is settled.
280 base.SpyPromise.run = function(func, opt_timeoutMs) {
281 base.SpyPromise.activate();
285 return base.SpyPromise.settleAll(opt_timeoutMs).then(function() {
286 base.SpyPromise.restore();
289 base.SpyPromise.restore();