# 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 |
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 | */
58 | goog.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 | };
89 | goog.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 | */
98 | goog.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 | */
106 | goog.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 | */
114 | goog.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 | */
123 | goog.testing.MockClock.prototype.replacer_ = null;
124 |
125 |
126 | /**
127 | * The current simulated time in milliseconds.
128 | * @type {number}
129 | * @private
130 | */
131 | goog.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 | */
142 | goog.testing.MockClock.prototype.timeoutDelay_ = 0;
143 |
144 |
145 | /**
146 | * The real set timeout for reference.
147 | * @const @private {!Function}
148 | */
149 | goog.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 | */
156 | goog.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 | */
196 | goog.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 | */
231 | goog.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 */
243 | goog.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 | */
255 | goog.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 | */
270 | goog.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 | */
280 | goog.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 | */
290 | goog.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 | */
302 | goog.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 | */
327 | goog.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 | */
353 | goog.testing.MockClock.prototype.getTimeoutsMade = function() {
354 | return this.timeoutsMade_;
355 | };
356 |
357 |
358 | /**
359 | * @return {number} The MockClock's current time in milliseconds.
360 | */
361 | goog.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 | */
375 | goog.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 | */
388 | goog.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 | */
421 | goog.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 | */
453 | goog.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 | */
484 | goog.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 | */
495 | goog.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 | */
519 | goog.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 | */
536 | goog.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 | */
557 | goog.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 | */
568 | goog.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 | */
592 | goog.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 | */
603 | goog.testing.MockClock.prototype.cancelRequestAnimationFrame_ =
604 | function(timeoutKey) {
605 | this.clearTimeout_(timeoutKey);
606 | };