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 * Count of the number of timeouts made.
105 * @type {number}
106 * @private
107 */
108goog.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 */
117goog.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 */
126goog.testing.MockClock.prototype.deletedKeys_ = null;
127
128
129/**
130 * The current simulated time in milliseconds.
131 * @type {number}
132 * @private
133 */
134goog.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 */
145goog.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 */
152goog.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 */
182goog.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 */
216goog.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 */
228goog.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 */
240goog.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 */
255goog.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 */
265goog.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 */
275goog.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 */
287goog.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 * Takes a promise and then ticks the mock clock. If the promise successfully
300 * resolves, returns the value produced by the promise. If the promise is
301 * rejected, it throws the rejection as an exception. If the promise is not
302 * resolved at all, throws an exception.
303 * Also ticks the general clock by the specified amount.
304 *
305 * @param {!goog.Thenable<T>} promise A promise that should be resolved after
306 * the mockClock is ticked for the given opt_millis.
307 * @param {number=} opt_millis Number of milliseconds to increment the counter.
308 * If not specified, clock ticks 1 millisecond.
309 * @return {T}
310 * @template T
311 */
312goog.testing.MockClock.prototype.tickPromise = function(promise, opt_millis) {
313 var value;
314 var error;
315 var resolved = false;
316 promise.then(function(v) {
317 value = v;
318 resolved = true;
319 }, function(e) {
320 error = e;
321 resolved = true;
322 });
323 this.tick(opt_millis);
324 if (!resolved) {
325 throw new Error(
326 'Promise was expected to be resolved after mock clock tick.');
327 }
328 if (error) {
329 throw error;
330 }
331 return value;
332};
333
334
335/**
336 * @return {number} The number of timeouts that have been scheduled.
337 */
338goog.testing.MockClock.prototype.getTimeoutsMade = function() {
339 return this.timeoutsMade_;
340};
341
342
343/**
344 * @return {number} The MockClock's current time in milliseconds.
345 */
346goog.testing.MockClock.prototype.getCurrentTime = function() {
347 return this.nowMillis_;
348};
349
350
351/**
352 * @param {number} timeoutKey The timeout key.
353 * @return {boolean} Whether the timer has been set and not cleared,
354 * independent of the timeout's expiration. In other words, the timeout
355 * could have passed or could be scheduled for the future. Either way,
356 * this function returns true or false depending only on whether the
357 * provided timeoutKey represents a timeout that has been set and not
358 * cleared.
359 */
360goog.testing.MockClock.prototype.isTimeoutSet = function(timeoutKey) {
361 return timeoutKey <= this.timeoutsMade_ && !this.deletedKeys_[timeoutKey];
362};
363
364
365/**
366 * Runs any function that is scheduled before a certain time. Timeouts can
367 * be made to fire early or late if timeoutDelay_ is non-0.
368 * @param {number} endTime The latest time in the range, in milliseconds.
369 * @private
370 */
371goog.testing.MockClock.prototype.runFunctionsWithinRange_ = function(
372 endTime) {
373 var adjustedEndTime = endTime - this.timeoutDelay_;
374
375 // Repeatedly pop off the last item since the queue is always sorted.
376 while (this.queue_ && this.queue_.length &&
377 this.queue_[this.queue_.length - 1].runAtMillis <= adjustedEndTime) {
378 var timeout = this.queue_.pop();
379
380 if (!(timeout.timeoutKey in this.deletedKeys_)) {
381 // Only move time forwards.
382 this.nowMillis_ = Math.max(this.nowMillis_,
383 timeout.runAtMillis + this.timeoutDelay_);
384 // Call timeout in global scope and pass the timeout key as the argument.
385 timeout.funcToCall.call(goog.global, timeout.timeoutKey);
386 // In case the interval was cleared in the funcToCall
387 if (timeout.recurring) {
388 this.scheduleFunction_(
389 timeout.timeoutKey, timeout.funcToCall, timeout.millis, true);
390 }
391 }
392 }
393};
394
395
396/**
397 * Schedules a function to be run at a certain time.
398 * @param {number} timeoutKey The timeout key.
399 * @param {Function} funcToCall The function to call.
400 * @param {number} millis The number of milliseconds to call it in.
401 * @param {boolean} recurring Whether to function call should recur.
402 * @private
403 */
404goog.testing.MockClock.prototype.scheduleFunction_ = function(
405 timeoutKey, funcToCall, millis, recurring) {
406 if (!goog.isFunction(funcToCall)) {
407 // Early error for debuggability rather than dying in the next .tick()
408 throw new TypeError('The provided callback must be a function, not a ' +
409 typeof funcToCall);
410 }
411
412 var timeout = {
413 runAtMillis: this.nowMillis_ + millis,
414 funcToCall: funcToCall,
415 recurring: recurring,
416 timeoutKey: timeoutKey,
417 millis: millis
418 };
419
420 goog.testing.MockClock.insert_(timeout, this.queue_);
421};
422
423
424/**
425 * Inserts a timer descriptor into a descending-order queue.
426 *
427 * Later-inserted duplicates appear at lower indices. For example, the
428 * asterisk in (5,4,*,3,2,1) would be the insertion point for 3.
429 *
430 * @param {Object} timeout The timeout to insert, with numerical runAtMillis
431 * property.
432 * @param {Array<Object>} queue The queue to insert into, with each element
433 * having a numerical runAtMillis property.
434 * @private
435 */
436goog.testing.MockClock.insert_ = function(timeout, queue) {
437 // Although insertion of N items is quadratic, requiring goog.structs.Heap
438 // from a unit test will make tests more prone to breakage. Since unit
439 // tests are normally small, scalability is not a primary issue.
440
441 // Find an insertion point. Since the queue is in reverse order (so we
442 // can pop rather than unshift), and later timers with the same time stamp
443 // should be executed later, we look for the element strictly greater than
444 // the one we are inserting.
445
446 for (var i = queue.length; i != 0; i--) {
447 if (queue[i - 1].runAtMillis > timeout.runAtMillis) {
448 break;
449 }
450 queue[i] = queue[i - 1];
451 }
452
453 queue[i] = timeout;
454};
455
456
457/**
458 * Maximum 32-bit signed integer.
459 *
460 * Timeouts over this time return immediately in many browsers, due to integer
461 * overflow. Such known browsers include Firefox, Chrome, and Safari, but not
462 * IE.
463 *
464 * @type {number}
465 * @private
466 */
467goog.testing.MockClock.MAX_INT_ = 2147483647;
468
469
470/**
471 * Schedules a function to be called after {@code millis} milliseconds.
472 * Mock implementation for setTimeout.
473 * @param {Function} funcToCall The function to call.
474 * @param {number} millis The number of milliseconds to call it after.
475 * @return {number} The number of timeouts created.
476 * @private
477 */
478goog.testing.MockClock.prototype.setTimeout_ = function(funcToCall, millis) {
479 if (millis > goog.testing.MockClock.MAX_INT_) {
480 throw Error(
481 'Bad timeout value: ' + millis + '. Timeouts over MAX_INT ' +
482 '(24.8 days) cause timeouts to be fired ' +
483 'immediately in most browsers, except for IE.');
484 }
485 this.timeoutsMade_ = this.timeoutsMade_ + 1;
486 this.scheduleFunction_(this.timeoutsMade_, funcToCall, millis, false);
487 return this.timeoutsMade_;
488};
489
490
491/**
492 * Schedules a function to be called every {@code millis} milliseconds.
493 * Mock implementation for setInterval.
494 * @param {Function} funcToCall The function to call.
495 * @param {number} millis The number of milliseconds between calls.
496 * @return {number} The number of timeouts created.
497 * @private
498 */
499goog.testing.MockClock.prototype.setInterval_ = function(funcToCall, millis) {
500 this.timeoutsMade_ = this.timeoutsMade_ + 1;
501 this.scheduleFunction_(this.timeoutsMade_, funcToCall, millis, true);
502 return this.timeoutsMade_;
503};
504
505
506/**
507 * Schedules a function to be called when an animation frame is triggered.
508 * Mock implementation for requestAnimationFrame.
509 * @param {Function} funcToCall The function to call.
510 * @return {number} The number of timeouts created.
511 * @private
512 */
513goog.testing.MockClock.prototype.requestAnimationFrame_ = function(funcToCall) {
514 return this.setTimeout_(goog.bind(function() {
515 if (funcToCall) {
516 funcToCall(this.getCurrentTime());
517 } else if (goog.global.mozRequestAnimationFrame) {
518 var event = new goog.testing.events.Event('MozBeforePaint', goog.global);
519 event['timeStamp'] = this.getCurrentTime();
520 goog.testing.events.fireBrowserEvent(event);
521 }
522 }, this), goog.testing.MockClock.REQUEST_ANIMATION_FRAME_TIMEOUT);
523};
524
525
526/**
527 * Schedules a function to be called immediately after the current JS
528 * execution.
529 * Mock implementation for setImmediate.
530 * @param {Function} funcToCall The function to call.
531 * @return {number} The number of timeouts created.
532 * @private
533 */
534goog.testing.MockClock.prototype.setImmediate_ = function(funcToCall) {
535 return this.setTimeout_(funcToCall, 0);
536};
537
538
539/**
540 * Clears a timeout.
541 * Mock implementation for clearTimeout.
542 * @param {number} timeoutKey The timeout key to clear.
543 * @private
544 */
545goog.testing.MockClock.prototype.clearTimeout_ = function(timeoutKey) {
546 // Some common libraries register static state with timers.
547 // This is bad. It leads to all sorts of crazy test problems where
548 // 1) Test A sets up a new mock clock and a static timer.
549 // 2) Test B sets up a new mock clock, but re-uses the static timer
550 // from Test A.
551 // 3) A timeout key from test A gets cleared, breaking a timeout in
552 // Test B.
553 //
554 // For now, we just hackily fail silently if someone tries to clear a timeout
555 // key before we've allocated it.
556 // Ideally, we should throw an exception if we see this happening.
557 //
558 // TODO(chrishenry): We might also try allocating timeout ids from a global
559 // pool rather than a local pool.
560 if (this.isTimeoutSet(timeoutKey)) {
561 this.deletedKeys_[timeoutKey] = true;
562 }
563};
564
565
566/**
567 * Clears an interval.
568 * Mock implementation for clearInterval.
569 * @param {number} timeoutKey The interval key to clear.
570 * @private
571 */
572goog.testing.MockClock.prototype.clearInterval_ = function(timeoutKey) {
573 this.clearTimeout_(timeoutKey);
574};
575
576
577/**
578 * Clears a requestAnimationFrame.
579 * Mock implementation for cancelRequestAnimationFrame.
580 * @param {number} timeoutKey The requestAnimationFrame key to clear.
581 * @private
582 */
583goog.testing.MockClock.prototype.cancelRequestAnimationFrame_ =
584 function(timeoutKey) {
585 this.clearTimeout_(timeoutKey);
586};