# 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 |
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 | * 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 | */
312 | goog.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 | */
338 | goog.testing.MockClock.prototype.getTimeoutsMade = function() {
339 | return this.timeoutsMade_;
340 | };
341 |
342 |
343 | /**
344 | * @return {number} The MockClock's current time in milliseconds.
345 | */
346 | goog.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 | */
360 | goog.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 | */
371 | goog.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 | */
404 | goog.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 | */
436 | goog.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 | */
467 | goog.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 | */
478 | goog.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 | */
499 | goog.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 | */
513 | goog.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 | */
534 | goog.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 | */
545 | goog.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 | */
572 | goog.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 | */
583 | goog.testing.MockClock.prototype.cancelRequestAnimationFrame_ =
584 | function(timeoutKey) {
585 | this.clearTimeout_(timeoutKey);
586 | };