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