lib/goog/testing/mockclock.js

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