OLD | NEW |
| (Empty) |
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. | |
4 | |
5 'use strict'; | |
6 | |
7 /** @suppress {duplicate} */ | |
8 var base = base || {}; | |
9 | |
10 (function() { | |
11 /** | |
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 | |
16 * SpyPromise.resolve. | |
17 * | |
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. | |
21 * | |
22 * The normal way to use this class is within a call to | |
23 * SpyPromise.run, for example: | |
24 * | |
25 * base.SpyPromise.run(function() { | |
26 * myCodeThatUsesPromises(); | |
27 * }); | |
28 * base.SpyPromise.settleAll().then(function() { | |
29 * console.log('All promises have been settled!'); | |
30 * }); | |
31 * | |
32 * @constructor | |
33 * @extends {Promise} | |
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 | |
38 * reject function. | |
39 */ | |
40 base.SpyPromise = function(func) { | |
41 var unsettled = new RealPromise(func); | |
42 var unsettledId = remember(unsettled); | |
43 return unsettled.then(function(/** * */value) { | |
44 forget(unsettledId); | |
45 return value; | |
46 }, function(error) { | |
47 forget(unsettledId); | |
48 throw error; | |
49 }); | |
50 }; | |
51 | |
52 /** | |
53 * The real promise constructor. Needed because it is normally hidden | |
54 * by SpyPromise.activate or SpyPromise.run. | |
55 * @const | |
56 */ | |
57 var RealPromise = Promise; | |
58 | |
59 /** | |
60 * The real window.setTimeout method. Needed because some test | |
61 * frameworks like to replace this method with a fake implementation. | |
62 * @const | |
63 */ | |
64 var realSetTimeout = window.setTimeout.bind(window); | |
65 | |
66 /** | |
67 * The number of unsettled promises. | |
68 * @type {number} | |
69 */ | |
70 base.SpyPromise.unsettledCount; // initialized by reset() | |
71 | |
72 /** | |
73 * A collection of all unsettled promises. | |
74 * @type {!Object<number,!Promise>} | |
75 */ | |
76 var unsettled; // initialized by reset() | |
77 | |
78 /** | |
79 * A counter used to assign ID numbers to new SpyPromise objects. | |
80 * @type {number} | |
81 */ | |
82 var nextPromiseId; // initialized by reset() | |
83 | |
84 /** | |
85 * A promise returned by SpyPromise.settleAll. | |
86 * @type {Promise<null>} | |
87 */ | |
88 var settleAllPromise; // initialized by reset() | |
89 | |
90 /** | |
91 * Records an unsettled promise. | |
92 * | |
93 * @param {!Promise} unsettledPromise | |
94 * @return {number} The ID number to be passed to forget_. | |
95 */ | |
96 function remember(unsettledPromise) { | |
97 var id = nextPromiseId++; | |
98 if (unsettled[id] != null) { | |
99 throw Error('Duplicate ID: ' + id); | |
100 } | |
101 base.SpyPromise.unsettledCount++; | |
102 unsettled[id] = unsettledPromise; | |
103 return id; | |
104 }; | |
105 | |
106 /** | |
107 * Forgets a promise. Called after the promise has been settled. | |
108 * | |
109 * @param {number} id | |
110 * @private | |
111 */ | |
112 function forget(id) { | |
113 console.assert(unsettled[id] != null, 'No such Promise: ' + id + '.'); | |
114 base.SpyPromise.unsettledCount--; | |
115 delete unsettled[id]; | |
116 }; | |
117 | |
118 /** | |
119 * Forgets about all unsettled promises. | |
120 */ | |
121 base.SpyPromise.reset = function() { | |
122 base.SpyPromise.unsettledCount = 0; | |
123 unsettled = {}; | |
124 nextPromiseId = 0; | |
125 settleAllPromise = null; | |
126 }; | |
127 | |
128 // Initialize static variables. | |
129 base.SpyPromise.reset(); | |
130 | |
131 /** | |
132 * Tries to wait until all promises has been settled. | |
133 * | |
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. | |
139 */ | |
140 base.SpyPromise.settleAll = function(opt_maxTimeMs) { | |
141 if (settleAllPromise) { | |
142 return settleAllPromise; | |
143 } | |
144 | |
145 var maxDelay = opt_maxTimeMs == null ? 1000 : opt_maxTimeMs; | |
146 | |
147 /** | |
148 * @param {number} count | |
149 * @param {number} totalDelay | |
150 * @return {!Promise<null>} | |
151 */ | |
152 function loop(count, totalDelay) { | |
153 return new RealPromise(function(resolve, reject) { | |
154 if (base.SpyPromise.unsettledCount == 0) { | |
155 settleAllPromise = null; | |
156 resolve(null); | |
157 } else if (totalDelay > maxDelay) { | |
158 settleAllPromise = null; | |
159 base.SpyPromise.reset(); | |
160 reject(new Error('base.SpyPromise.settleAll timed out')); | |
161 } else { | |
162 // This implements quadratic backoff according to Euler's | |
163 // triangular number formula. | |
164 var delay = count; | |
165 | |
166 // Must jump through crazy hoops to get a real timer in a unit test. | |
167 realSetTimeout(function() { | |
168 resolve(loop( | |
169 count + 1, | |
170 delay + totalDelay)); | |
171 }, delay); | |
172 } | |
173 }); | |
174 }; | |
175 | |
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() { | |
180 return loop(0, 0); | |
181 }); | |
182 return settleAllPromise; | |
183 }; | |
184 | |
185 /** | |
186 * Only for testing this class. Do not use. | |
187 * @returns {boolean} True if settleAll is executing. | |
188 */ | |
189 base.SpyPromise.isSettleAllRunning = function() { | |
190 return settleAllPromise != null; | |
191 }; | |
192 | |
193 /** | |
194 * Wrapper for Promise.resolve. | |
195 * | |
196 * @param {*} value | |
197 * @return {!base.SpyPromise} | |
198 */ | |
199 base.SpyPromise.resolve = function(value) { | |
200 return new base.SpyPromise(function(resolve, reject) { | |
201 resolve(value); | |
202 }); | |
203 }; | |
204 | |
205 /** | |
206 * Wrapper for Promise.reject. | |
207 * | |
208 * @param {*} value | |
209 * @return {!base.SpyPromise} | |
210 */ | |
211 base.SpyPromise.reject = function(value) { | |
212 return new base.SpyPromise(function(resolve, reject) { | |
213 reject(value); | |
214 }); | |
215 }; | |
216 | |
217 /** | |
218 * Wrapper for Promise.all. | |
219 * | |
220 * @param {!Array<Promise>} promises | |
221 * @return {!base.SpyPromise} | |
222 */ | |
223 base.SpyPromise.all = function(promises) { | |
224 return base.SpyPromise.resolve(RealPromise.all(promises)); | |
225 }; | |
226 | |
227 /** | |
228 * Wrapper for Promise.race. | |
229 * | |
230 * @param {!Array<Promise>} promises | |
231 * @return {!base.SpyPromise} | |
232 */ | |
233 base.SpyPromise.race = function(promises) { | |
234 return base.SpyPromise.resolve(RealPromise.race(promises)); | |
235 }; | |
236 | |
237 /** | |
238 * Sets Promise = base.SpyPromise. Must not be called more than once | |
239 * without an intervening call to restore(). | |
240 */ | |
241 base.SpyPromise.activate = function() { | |
242 if (settleAllPromise) { | |
243 throw Error('called base.SpyPromise.activate while settleAll is running'); | |
244 } | |
245 if (Promise === base.SpyPromise) { | |
246 throw Error('base.SpyPromise is already active'); | |
247 } | |
248 Promise = /** @type {function(new:Promise)} */(base.SpyPromise); | |
249 }; | |
250 | |
251 /** | |
252 * Restores the original value of Promise. | |
253 */ | |
254 base.SpyPromise.restore = function() { | |
255 if (settleAllPromise) { | |
256 throw Error('called base.SpyPromise.restore while settleAll is running'); | |
257 } | |
258 if (Promise === base.SpyPromise) { | |
259 Promise = RealPromise; | |
260 } else if (Promise === RealPromise) { | |
261 throw new Error('base.SpyPromise is not active.'); | |
262 } else { | |
263 throw new Error('Something fishy is going on.'); | |
264 } | |
265 }; | |
266 | |
267 /** | |
268 * Calls func with Promise equal to base.SpyPromise. | |
269 * | |
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. | |
274 * (default: 1000 ms) | |
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. | |
279 */ | |
280 base.SpyPromise.run = function(func, opt_timeoutMs) { | |
281 base.SpyPromise.activate(); | |
282 try { | |
283 func(); | |
284 } finally { | |
285 return base.SpyPromise.settleAll(opt_timeoutMs).then(function() { | |
286 base.SpyPromise.restore(); | |
287 return null; | |
288 }, function(error) { | |
289 base.SpyPromise.restore(); | |
290 throw error; | |
291 }); | |
292 } | |
293 }; | |
294 })(); | |
OLD | NEW |