1 | // Copyright 2007 The Closure Library Authors. All Rights Reserved. |
2 | // |
3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
4 | // you may not use this file except in compliance with the License. |
5 | // You may obtain a copy of the License at |
6 | // |
7 | // http://www.apache.org/licenses/LICENSE-2.0 |
8 | // |
9 | // Unless required by applicable law or agreed to in writing, software |
10 | // distributed under the License is distributed on an "AS-IS" BASIS, |
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
12 | // See the License for the specific language governing permissions and |
13 | // limitations under the License. |
14 | |
15 | /** |
16 | * @fileoverview Mock Clock implementation for working with setTimeout, |
17 | * setInterval, clearTimeout and clearInterval within unit tests. |
18 | * |
19 | * Derived from jsUnitMockTimeout.js, contributed to JsUnit by |
20 | * Pivotal Computer Systems, www.pivotalsf.com |
21 | * |
22 | */ |
23 | |
24 | goog.provide('goog.testing.MockClock'); |
25 | |
26 | goog.require('goog.Disposable'); |
27 | goog.require('goog.async.run'); |
28 | goog.require('goog.testing.PropertyReplacer'); |
29 | goog.require('goog.testing.events'); |
30 | goog.require('goog.testing.events.Event'); |
31 | goog.require('goog.testing.watchers'); |
32 | |
33 | |
34 | |
35 | /** |
36 | * Class for unit testing code that uses setTimeout and clearTimeout. |
37 | * |
38 | * NOTE: If you are using MockClock to test code that makes use of |
39 | * goog.fx.Animation, then you must either: |
40 | * |
41 | * 1. Install and dispose of the MockClock in setUpPage() and tearDownPage() |
42 | * respectively (rather than setUp()/tearDown()). |
43 | * |
44 | * or |
45 | * |
46 | * 2. Ensure that every test clears the animation queue by calling |
47 | * mockClock.tick(x) at the end of each test function (where `x` is large |
48 | * enough to complete all animations). |
49 | * |
50 | * Otherwise, if any animation is left pending at the time that |
51 | * MockClock.dispose() is called, that will permanently prevent any future |
52 | * animations from playing on the page. |
53 | * |
54 | * @param {boolean=} opt_autoInstall Install the MockClock at construction time. |
55 | * @constructor |
56 | * @extends {goog.Disposable} |
57 | * @final |
58 | */ |
59 | goog.testing.MockClock = function(opt_autoInstall) { |
60 | goog.Disposable.call(this); |
61 | |
62 | /** |
63 | * Reverse-order queue of timers to fire. |
64 | * |
65 | * The last item of the queue is popped off. Insertion happens from the |
66 | * right. For example, the expiration times for each element of the queue |
67 | * might be in the order 300, 200, 200. |
68 | * |
69 | * @type {Array.<Object>} |
70 | * @private |
71 | */ |
72 | this.queue_ = []; |
73 | |
74 | /** |
75 | * Set of timeouts that should be treated as cancelled. |
76 | * |
77 | * Rather than removing cancelled timers directly from the queue, this set |
78 | * simply marks them as deleted so that they can be ignored when their |
79 | * turn comes up. The keys are the timeout keys that are cancelled, each |
80 | * mapping to true. |
81 | * |
82 | * @type {Object} |
83 | * @private |
84 | */ |
85 | this.deletedKeys_ = {}; |
86 | |
87 | if (opt_autoInstall) { |
88 | this.install(); |
89 | } |
90 | }; |
91 | goog.inherits(goog.testing.MockClock, goog.Disposable); |
92 | |
93 | |
94 | /** |
95 | * Default wait timeout for mocking requestAnimationFrame (in milliseconds). |
96 | * |
97 | * @type {number} |
98 | * @const |
99 | */ |
100 | goog.testing.MockClock.REQUEST_ANIMATION_FRAME_TIMEOUT = 20; |
101 | |
102 | |
103 | /** |
104 | * Count of the number of timeouts made. |
105 | * @type {number} |
106 | * @private |
107 | */ |
108 | goog.testing.MockClock.prototype.timeoutsMade_ = 0; |
109 | |
110 | |
111 | /** |
112 | * PropertyReplacer instance which overwrites and resets setTimeout, |
113 | * setInterval, etc. or null if the MockClock is not installed. |
114 | * @type {goog.testing.PropertyReplacer} |
115 | * @private |
116 | */ |
117 | goog.testing.MockClock.prototype.replacer_ = null; |
118 | |
119 | |
120 | /** |
121 | * Map of deleted keys. These keys represents keys that were deleted in a |
122 | * clearInterval, timeoutid -> object. |
123 | * @type {Object} |
124 | * @private |
125 | */ |
126 | goog.testing.MockClock.prototype.deletedKeys_ = null; |
127 | |
128 | |
129 | /** |
130 | * The current simulated time in milliseconds. |
131 | * @type {number} |
132 | * @private |
133 | */ |
134 | goog.testing.MockClock.prototype.nowMillis_ = 0; |
135 | |
136 | |
137 | /** |
138 | * Additional delay between the time a timeout was set to fire, and the time |
139 | * it actually fires. Useful for testing workarounds for this Firefox 2 bug: |
140 | * https://bugzilla.mozilla.org/show_bug.cgi?id=291386 |
141 | * May be negative. |
142 | * @type {number} |
143 | * @private |
144 | */ |
145 | goog.testing.MockClock.prototype.timeoutDelay_ = 0; |
146 | |
147 | |
148 | /** |
149 | * Installs the MockClock by overriding the global object's implementation of |
150 | * setTimeout, setInterval, clearTimeout and clearInterval. |
151 | */ |
152 | goog.testing.MockClock.prototype.install = function() { |
153 | if (!this.replacer_) { |
154 | var r = this.replacer_ = new goog.testing.PropertyReplacer(); |
155 | r.set(goog.global, 'setTimeout', goog.bind(this.setTimeout_, this)); |
156 | r.set(goog.global, 'setInterval', goog.bind(this.setInterval_, this)); |
157 | r.set(goog.global, 'setImmediate', goog.bind(this.setImmediate_, this)); |
158 | r.set(goog.global, 'clearTimeout', goog.bind(this.clearTimeout_, this)); |
159 | r.set(goog.global, 'clearInterval', goog.bind(this.clearInterval_, this)); |
160 | // goog.Promise uses goog.async.run. In order to be able to test |
161 | // Promise-based code, we need to make sure that goog.async.run uses |
162 | // nextTick instead of native browser Promises. This means that it will |
163 | // default to setImmediate, which is replaced above. Note that we test for |
164 | // the presence of goog.async.run.forceNextTick to be resilient to the case |
165 | // where tests replace goog.async.run directly. |
166 | goog.async.run.forceNextTick && goog.async.run.forceNextTick(); |
167 | |
168 | // Replace the requestAnimationFrame functions. |
169 | this.replaceRequestAnimationFrame_(); |
170 | |
171 | // PropertyReplacer#set can't be called with renameable functions. |
172 | this.oldGoogNow_ = goog.now; |
173 | goog.now = goog.bind(this.getCurrentTime, this); |
174 | } |
175 | }; |
176 | |
177 | |
178 | /** |
179 | * Installs the mocks for requestAnimationFrame and cancelRequestAnimationFrame. |
180 | * @private |
181 | */ |
182 | goog.testing.MockClock.prototype.replaceRequestAnimationFrame_ = function() { |
183 | var r = this.replacer_; |
184 | var requestFuncs = ['requestAnimationFrame', |
185 | 'webkitRequestAnimationFrame', |
186 | 'mozRequestAnimationFrame', |
187 | 'oRequestAnimationFrame', |
188 | 'msRequestAnimationFrame']; |
189 | |
190 | var cancelFuncs = ['cancelRequestAnimationFrame', |
191 | 'webkitCancelRequestAnimationFrame', |
192 | 'mozCancelRequestAnimationFrame', |
193 | 'oCancelRequestAnimationFrame', |
194 | 'msCancelRequestAnimationFrame']; |
195 | |
196 | for (var i = 0; i < requestFuncs.length; ++i) { |
197 | if (goog.global && goog.global[requestFuncs[i]]) { |
198 | r.set(goog.global, requestFuncs[i], |
199 | goog.bind(this.requestAnimationFrame_, this)); |
200 | } |
201 | } |
202 | |
203 | for (var i = 0; i < cancelFuncs.length; ++i) { |
204 | if (goog.global && goog.global[cancelFuncs[i]]) { |
205 | r.set(goog.global, cancelFuncs[i], |
206 | goog.bind(this.cancelRequestAnimationFrame_, this)); |
207 | } |
208 | } |
209 | }; |
210 | |
211 | |
212 | /** |
213 | * Removes the MockClock's hooks into the global object's functions and revert |
214 | * to their original values. |
215 | */ |
216 | goog.testing.MockClock.prototype.uninstall = function() { |
217 | if (this.replacer_) { |
218 | this.replacer_.reset(); |
219 | this.replacer_ = null; |
220 | goog.now = this.oldGoogNow_; |
221 | } |
222 | |
223 | this.fireResetEvent(); |
224 | }; |
225 | |
226 | |
227 | /** @override */ |
228 | goog.testing.MockClock.prototype.disposeInternal = function() { |
229 | this.uninstall(); |
230 | this.queue_ = null; |
231 | this.deletedKeys_ = null; |
232 | goog.testing.MockClock.superClass_.disposeInternal.call(this); |
233 | }; |
234 | |
235 | |
236 | /** |
237 | * Resets the MockClock, removing all timeouts that are scheduled and resets |
238 | * the fake timer count. |
239 | */ |
240 | goog.testing.MockClock.prototype.reset = function() { |
241 | this.queue_ = []; |
242 | this.deletedKeys_ = {}; |
243 | this.nowMillis_ = 0; |
244 | this.timeoutsMade_ = 0; |
245 | this.timeoutDelay_ = 0; |
246 | |
247 | this.fireResetEvent(); |
248 | }; |
249 | |
250 | |
251 | /** |
252 | * Signals that the mock clock has been reset, allowing objects that |
253 | * maintain their own internal state to reset. |
254 | */ |
255 | goog.testing.MockClock.prototype.fireResetEvent = function() { |
256 | goog.testing.watchers.signalClockReset(); |
257 | }; |
258 | |
259 | |
260 | /** |
261 | * Sets the amount of time between when a timeout is scheduled to fire and when |
262 | * it actually fires. |
263 | * @param {number} delay The delay in milliseconds. May be negative. |
264 | */ |
265 | goog.testing.MockClock.prototype.setTimeoutDelay = function(delay) { |
266 | this.timeoutDelay_ = delay; |
267 | }; |
268 | |
269 | |
270 | /** |
271 | * @return {number} delay The amount of time between when a timeout is |
272 | * scheduled to fire and when it actually fires, in milliseconds. May |
273 | * be negative. |
274 | */ |
275 | goog.testing.MockClock.prototype.getTimeoutDelay = function() { |
276 | return this.timeoutDelay_; |
277 | }; |
278 | |
279 | |
280 | /** |
281 | * Increments the MockClock's time by a given number of milliseconds, running |
282 | * any functions that are now overdue. |
283 | * @param {number=} opt_millis Number of milliseconds to increment the counter. |
284 | * If not specified, clock ticks 1 millisecond. |
285 | * @return {number} Current mock time in milliseconds. |
286 | */ |
287 | goog.testing.MockClock.prototype.tick = function(opt_millis) { |
288 | if (typeof opt_millis != 'number') { |
289 | opt_millis = 1; |
290 | } |
291 | var endTime = this.nowMillis_ + opt_millis; |
292 | this.runFunctionsWithinRange_(endTime); |
293 | this.nowMillis_ = endTime; |
294 | return endTime; |
295 | }; |
296 | |
297 | |
298 | /** |
299 | * @return {number} The number of timeouts that have been scheduled. |
300 | */ |
301 | goog.testing.MockClock.prototype.getTimeoutsMade = function() { |
302 | return this.timeoutsMade_; |
303 | }; |
304 | |
305 | |
306 | /** |
307 | * @return {number} The MockClock's current time in milliseconds. |
308 | */ |
309 | goog.testing.MockClock.prototype.getCurrentTime = function() { |
310 | return this.nowMillis_; |
311 | }; |
312 | |
313 | |
314 | /** |
315 | * @param {number} timeoutKey The timeout key. |
316 | * @return {boolean} Whether the timer has been set and not cleared, |
317 | * independent of the timeout's expiration. In other words, the timeout |
318 | * could have passed or could be scheduled for the future. Either way, |
319 | * this function returns true or false depending only on whether the |
320 | * provided timeoutKey represents a timeout that has been set and not |
321 | * cleared. |
322 | */ |
323 | goog.testing.MockClock.prototype.isTimeoutSet = function(timeoutKey) { |
324 | return timeoutKey <= this.timeoutsMade_ && !this.deletedKeys_[timeoutKey]; |
325 | }; |
326 | |
327 | |
328 | /** |
329 | * Runs any function that is scheduled before a certain time. Timeouts can |
330 | * be made to fire early or late if timeoutDelay_ is non-0. |
331 | * @param {number} endTime The latest time in the range, in milliseconds. |
332 | * @private |
333 | */ |
334 | goog.testing.MockClock.prototype.runFunctionsWithinRange_ = function( |
335 | endTime) { |
336 | var adjustedEndTime = endTime - this.timeoutDelay_; |
337 | |
338 | // Repeatedly pop off the last item since the queue is always sorted. |
339 | while (this.queue_ && this.queue_.length && |
340 | this.queue_[this.queue_.length - 1].runAtMillis <= adjustedEndTime) { |
341 | var timeout = this.queue_.pop(); |
342 | |
343 | if (!(timeout.timeoutKey in this.deletedKeys_)) { |
344 | // Only move time forwards. |
345 | this.nowMillis_ = Math.max(this.nowMillis_, |
346 | timeout.runAtMillis + this.timeoutDelay_); |
347 | // Call timeout in global scope and pass the timeout key as the argument. |
348 | timeout.funcToCall.call(goog.global, timeout.timeoutKey); |
349 | // In case the interval was cleared in the funcToCall |
350 | if (timeout.recurring) { |
351 | this.scheduleFunction_( |
352 | timeout.timeoutKey, timeout.funcToCall, timeout.millis, true); |
353 | } |
354 | } |
355 | } |
356 | }; |
357 | |
358 | |
359 | /** |
360 | * Schedules a function to be run at a certain time. |
361 | * @param {number} timeoutKey The timeout key. |
362 | * @param {Function} funcToCall The function to call. |
363 | * @param {number} millis The number of milliseconds to call it in. |
364 | * @param {boolean} recurring Whether to function call should recur. |
365 | * @private |
366 | */ |
367 | goog.testing.MockClock.prototype.scheduleFunction_ = function( |
368 | timeoutKey, funcToCall, millis, recurring) { |
369 | if (!goog.isFunction(funcToCall)) { |
370 | // Early error for debuggability rather than dying in the next .tick() |
371 | throw new TypeError('The provided callback must be a function, not a ' + |
372 | typeof funcToCall); |
373 | } |
374 | |
375 | var timeout = { |
376 | runAtMillis: this.nowMillis_ + millis, |
377 | funcToCall: funcToCall, |
378 | recurring: recurring, |
379 | timeoutKey: timeoutKey, |
380 | millis: millis |
381 | }; |
382 | |
383 | goog.testing.MockClock.insert_(timeout, this.queue_); |
384 | }; |
385 | |
386 | |
387 | /** |
388 | * Inserts a timer descriptor into a descending-order queue. |
389 | * |
390 | * Later-inserted duplicates appear at lower indices. For example, the |
391 | * asterisk in (5,4,*,3,2,1) would be the insertion point for 3. |
392 | * |
393 | * @param {Object} timeout The timeout to insert, with numerical runAtMillis |
394 | * property. |
395 | * @param {Array.<Object>} queue The queue to insert into, with each element |
396 | * having a numerical runAtMillis property. |
397 | * @private |
398 | */ |
399 | goog.testing.MockClock.insert_ = function(timeout, queue) { |
400 | // Although insertion of N items is quadratic, requiring goog.structs.Heap |
401 | // from a unit test will make tests more prone to breakage. Since unit |
402 | // tests are normally small, scalability is not a primary issue. |
403 | |
404 | // Find an insertion point. Since the queue is in reverse order (so we |
405 | // can pop rather than unshift), and later timers with the same time stamp |
406 | // should be executed later, we look for the element strictly greater than |
407 | // the one we are inserting. |
408 | |
409 | for (var i = queue.length; i != 0; i--) { |
410 | if (queue[i - 1].runAtMillis > timeout.runAtMillis) { |
411 | break; |
412 | } |
413 | queue[i] = queue[i - 1]; |
414 | } |
415 | |
416 | queue[i] = timeout; |
417 | }; |
418 | |
419 | |
420 | /** |
421 | * Maximum 32-bit signed integer. |
422 | * |
423 | * Timeouts over this time return immediately in many browsers, due to integer |
424 | * overflow. Such known browsers include Firefox, Chrome, and Safari, but not |
425 | * IE. |
426 | * |
427 | * @type {number} |
428 | * @private |
429 | */ |
430 | goog.testing.MockClock.MAX_INT_ = 2147483647; |
431 | |
432 | |
433 | /** |
434 | * Schedules a function to be called after {@code millis} milliseconds. |
435 | * Mock implementation for setTimeout. |
436 | * @param {Function} funcToCall The function to call. |
437 | * @param {number} millis The number of milliseconds to call it after. |
438 | * @return {number} The number of timeouts created. |
439 | * @private |
440 | */ |
441 | goog.testing.MockClock.prototype.setTimeout_ = function(funcToCall, millis) { |
442 | if (millis > goog.testing.MockClock.MAX_INT_) { |
443 | throw Error( |
444 | 'Bad timeout value: ' + millis + '. Timeouts over MAX_INT ' + |
445 | '(24.8 days) cause timeouts to be fired ' + |
446 | 'immediately in most browsers, except for IE.'); |
447 | } |
448 | this.timeoutsMade_ = this.timeoutsMade_ + 1; |
449 | this.scheduleFunction_(this.timeoutsMade_, funcToCall, millis, false); |
450 | return this.timeoutsMade_; |
451 | }; |
452 | |
453 | |
454 | /** |
455 | * Schedules a function to be called every {@code millis} milliseconds. |
456 | * Mock implementation for setInterval. |
457 | * @param {Function} funcToCall The function to call. |
458 | * @param {number} millis The number of milliseconds between calls. |
459 | * @return {number} The number of timeouts created. |
460 | * @private |
461 | */ |
462 | goog.testing.MockClock.prototype.setInterval_ = function(funcToCall, millis) { |
463 | this.timeoutsMade_ = this.timeoutsMade_ + 1; |
464 | this.scheduleFunction_(this.timeoutsMade_, funcToCall, millis, true); |
465 | return this.timeoutsMade_; |
466 | }; |
467 | |
468 | |
469 | /** |
470 | * Schedules a function to be called when an animation frame is triggered. |
471 | * Mock implementation for requestAnimationFrame. |
472 | * @param {Function} funcToCall The function to call. |
473 | * @return {number} The number of timeouts created. |
474 | * @private |
475 | */ |
476 | goog.testing.MockClock.prototype.requestAnimationFrame_ = function(funcToCall) { |
477 | return this.setTimeout_(goog.bind(function() { |
478 | if (funcToCall) { |
479 | funcToCall(this.getCurrentTime()); |
480 | } else if (goog.global.mozRequestAnimationFrame) { |
481 | var event = new goog.testing.events.Event('MozBeforePaint', goog.global); |
482 | event['timeStamp'] = this.getCurrentTime(); |
483 | goog.testing.events.fireBrowserEvent(event); |
484 | } |
485 | }, this), goog.testing.MockClock.REQUEST_ANIMATION_FRAME_TIMEOUT); |
486 | }; |
487 | |
488 | |
489 | /** |
490 | * Schedules a function to be called immediately after the current JS |
491 | * execution. |
492 | * Mock implementation for setImmediate. |
493 | * @param {Function} funcToCall The function to call. |
494 | * @return {number} The number of timeouts created. |
495 | * @private |
496 | */ |
497 | goog.testing.MockClock.prototype.setImmediate_ = function(funcToCall) { |
498 | return this.setTimeout_(funcToCall, 0); |
499 | }; |
500 | |
501 | |
502 | /** |
503 | * Clears a timeout. |
504 | * Mock implementation for clearTimeout. |
505 | * @param {number} timeoutKey The timeout key to clear. |
506 | * @private |
507 | */ |
508 | goog.testing.MockClock.prototype.clearTimeout_ = function(timeoutKey) { |
509 | // Some common libraries register static state with timers. |
510 | // This is bad. It leads to all sorts of crazy test problems where |
511 | // 1) Test A sets up a new mock clock and a static timer. |
512 | // 2) Test B sets up a new mock clock, but re-uses the static timer |
513 | // from Test A. |
514 | // 3) A timeout key from test A gets cleared, breaking a timeout in |
515 | // Test B. |
516 | // |
517 | // For now, we just hackily fail silently if someone tries to clear a timeout |
518 | // key before we've allocated it. |
519 | // Ideally, we should throw an exception if we see this happening. |
520 | // |
521 | // TODO(user): We might also try allocating timeout ids from a global |
522 | // pool rather than a local pool. |
523 | if (this.isTimeoutSet(timeoutKey)) { |
524 | this.deletedKeys_[timeoutKey] = true; |
525 | } |
526 | }; |
527 | |
528 | |
529 | /** |
530 | * Clears an interval. |
531 | * Mock implementation for clearInterval. |
532 | * @param {number} timeoutKey The interval key to clear. |
533 | * @private |
534 | */ |
535 | goog.testing.MockClock.prototype.clearInterval_ = function(timeoutKey) { |
536 | this.clearTimeout_(timeoutKey); |
537 | }; |
538 | |
539 | |
540 | /** |
541 | * Clears a requestAnimationFrame. |
542 | * Mock implementation for cancelRequestAnimationFrame. |
543 | * @param {number} timeoutKey The requestAnimationFrame key to clear. |
544 | * @private |
545 | */ |
546 | goog.testing.MockClock.prototype.cancelRequestAnimationFrame_ = |
547 | function(timeoutKey) { |
548 | this.clearTimeout_(timeoutKey); |
549 | }; |