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();