# 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 at6//7//      http://www.apache.org/licenses/LICENSE-2.08//9// Unless required by applicable law or agreed to in writing, software10// 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 and13// limitations under the License.1415/**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 by20 * Pivotal Computer Systems, www.pivotalsf.com21 *22 */2324goog.provide('goog.testing.MockClock');2526goog.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');32333435/**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 of39 *       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 * or45 *46 * 2. Ensure that every test clears the animation queue by calling47 *    mockClock.tick(x) at the end of each test function (where `x` is large48 *    enough to complete all animations).49 *50 * Otherwise, if any animation is left pending at the time that51 * MockClock.dispose() is called, that will permanently prevent any future52 * animations from playing on the page.53 *54 * @param {boolean=} opt_autoInstall Install the MockClock at construction time.55 * @constructor56 * @extends {goog.Disposable}57 * @final58 */59goog.testing.MockClock = function(opt_autoInstall) {60  goog.Disposable.call(this);6162  /**63   * Reverse-order queue of timers to fire.64   *65   * The last item of the queue is popped off.  Insertion happens from the66   * right.  For example, the expiration times for each element of the queue67   * might be in the order 300, 200, 200.68   *69   * @type {Array.<Object>}70   * @private71   */72  this.queue_ = [];7374  /**75   * Set of timeouts that should be treated as cancelled.76   *77   * Rather than removing cancelled timers directly from the queue, this set78   * simply marks them as deleted so that they can be ignored when their79   * turn comes up.  The keys are the timeout keys that are cancelled, each80   * mapping to true.81   *82   * @type {Object}83   * @private84   */85  this.deletedKeys_ = {};8687  if (opt_autoInstall) {88    this.install();89  }90};91goog.inherits(goog.testing.MockClock, goog.Disposable);929394/**95 * Default wait timeout for mocking requestAnimationFrame (in milliseconds).96 *97 * @type {number}98 * @const99 */100goog.testing.MockClock.REQUEST_ANIMATION_FRAME_TIMEOUT = 20;101102103/**104 * Count of the number of timeouts made.105 * @type {number}106 * @private107 */108goog.testing.MockClock.prototype.timeoutsMade_ = 0;109110111/**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 * @private116 */117goog.testing.MockClock.prototype.replacer_ = null;118119120/**121 * Map of deleted keys.  These keys represents keys that were deleted in a122 * clearInterval, timeoutid -> object.123 * @type {Object}124 * @private125 */126goog.testing.MockClock.prototype.deletedKeys_ = null;127128129/**130 * The current simulated time in milliseconds.131 * @type {number}132 * @private133 */134goog.testing.MockClock.prototype.nowMillis_ = 0;135136137/**138 * Additional delay between the time a timeout was set to fire, and the time139 * it actually fires.  Useful for testing workarounds for this Firefox 2 bug:140 * https://bugzilla.mozilla.org/show_bug.cgi?id=291386141 * May be negative.142 * @type {number}143 * @private144 */145goog.testing.MockClock.prototype.timeoutDelay_ = 0;146147148/**149 * Installs the MockClock by overriding the global object's implementation of150 * 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 test161    // Promise-based code, we need to make sure that goog.async.run uses162    // nextTick instead of native browser Promises. This means that it will163    // default to setImmediate, which is replaced above. Note that we test for164    // the presence of goog.async.run.forceNextTick to be resilient to the case165    // where tests replace goog.async.run directly.166    goog.async.run.forceNextTick && goog.async.run.forceNextTick();167168    // Replace the requestAnimationFrame functions.169    this.replaceRequestAnimationFrame_();170171    // PropertyReplacer#set can't be called with renameable functions.172    this.oldGoogNow_ = goog.now;173    goog.now = goog.bind(this.getCurrentTime, this);174  }175};176177178/**179 * Installs the mocks for requestAnimationFrame and cancelRequestAnimationFrame.180 * @private181 */182goog.testing.MockClock.prototype.replaceRequestAnimationFrame_ = function() {183  var r = this.replacer_;184  var requestFuncs = ['requestAnimationFrame',185                      'webkitRequestAnimationFrame',186                      'mozRequestAnimationFrame',187                      'oRequestAnimationFrame',188                      'msRequestAnimationFrame'];189190  var cancelFuncs = ['cancelRequestAnimationFrame',191                     'webkitCancelRequestAnimationFrame',192                     'mozCancelRequestAnimationFrame',193                     'oCancelRequestAnimationFrame',194                     'msCancelRequestAnimationFrame'];195196  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  }202203  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};210211212/**213 * Removes the MockClock's hooks into the global object's functions and revert214 * 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  }222223  this.fireResetEvent();224};225226227/** @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};234235236/**237 * Resets the MockClock, removing all timeouts that are scheduled and resets238 * 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;246247  this.fireResetEvent();248};249250251/**252 * Signals that the mock clock has been reset, allowing objects that253 * maintain their own internal state to reset.254 */255goog.testing.MockClock.prototype.fireResetEvent = function() {256  goog.testing.watchers.signalClockReset();257};258259260/**261 * Sets the amount of time between when a timeout is scheduled to fire and when262 * 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};268269270/**271 * @return {number} delay The amount of time between when a timeout is272 *     scheduled to fire and when it actually fires, in milliseconds.  May273 *     be negative.274 */275goog.testing.MockClock.prototype.getTimeoutDelay = function() {276  return this.timeoutDelay_;277};278279280/**281 * Increments the MockClock's time by a given number of milliseconds, running282 * 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};296297298/**299 * @return {number} The number of timeouts that have been scheduled.300 */301goog.testing.MockClock.prototype.getTimeoutsMade = function() {302  return this.timeoutsMade_;303};304305306/**307 * @return {number} The MockClock's current time in milliseconds.308 */309goog.testing.MockClock.prototype.getCurrentTime = function() {310  return this.nowMillis_;311};312313314/**315 * @param {number} timeoutKey The timeout key.316 * @return {boolean} Whether the timer has been set and not cleared,317 *     independent of the timeout's expiration.  In other words, the timeout318 *     could have passed or could be scheduled for the future.  Either way,319 *     this function returns true or false depending only on whether the320 *     provided timeoutKey represents a timeout that has been set and not321 *     cleared.322 */323goog.testing.MockClock.prototype.isTimeoutSet = function(timeoutKey) {324  return timeoutKey <= this.timeoutsMade_ && !this.deletedKeys_[timeoutKey];325};326327328/**329 * Runs any function that is scheduled before a certain time.  Timeouts can330 * be made to fire early or late if timeoutDelay_ is non-0.331 * @param {number} endTime The latest time in the range, in milliseconds.332 * @private333 */334goog.testing.MockClock.prototype.runFunctionsWithinRange_ = function(335    endTime) {336  var adjustedEndTime = endTime - this.timeoutDelay_;337338  // Repeatedly pop off the last item since the queue is always sorted.339  while (this.queue_ && this.queue_.length &&340      this.queue_[this.queue_.length - 1].runAtMillis <= adjustedEndTime) {341    var timeout = this.queue_.pop();342343    if (!(timeout.timeoutKey in this.deletedKeys_)) {344      // Only move time forwards.345      this.nowMillis_ = Math.max(this.nowMillis_,346          timeout.runAtMillis + this.timeoutDelay_);347      // Call timeout in global scope and pass the timeout key as the argument.348      timeout.funcToCall.call(goog.global, timeout.timeoutKey);349      // In case the interval was cleared in the funcToCall350      if (timeout.recurring) {351        this.scheduleFunction_(352            timeout.timeoutKey, timeout.funcToCall, timeout.millis, true);353      }354    }355  }356};357358359/**360 * Schedules a function to be run at a certain time.361 * @param {number} timeoutKey The timeout key.362 * @param {Function} funcToCall The function to call.363 * @param {number} millis The number of milliseconds to call it in.364 * @param {boolean} recurring Whether to function call should recur.365 * @private366 */367goog.testing.MockClock.prototype.scheduleFunction_ = function(368    timeoutKey, funcToCall, millis, recurring) {369  if (!goog.isFunction(funcToCall)) {370    // Early error for debuggability rather than dying in the next .tick()371    throw new TypeError('The provided callback must be a function, not a ' +372        typeof funcToCall);373  }374375  var timeout = {376    runAtMillis: this.nowMillis_ + millis,377    funcToCall: funcToCall,378    recurring: recurring,379    timeoutKey: timeoutKey,380    millis: millis381  };382383  goog.testing.MockClock.insert_(timeout, this.queue_);384};385386387/**388 * Inserts a timer descriptor into a descending-order queue.389 *390 * Later-inserted duplicates appear at lower indices.  For example, the391 * asterisk in (5,4,*,3,2,1) would be the insertion point for 3.392 *393 * @param {Object} timeout The timeout to insert, with numerical runAtMillis394 *     property.395 * @param {Array.<Object>} queue The queue to insert into, with each element396 *     having a numerical runAtMillis property.397 * @private398 */399goog.testing.MockClock.insert_ = function(timeout, queue) {400  // Although insertion of N items is quadratic, requiring goog.structs.Heap401  // from a unit test will make tests more prone to breakage.  Since unit402  // tests are normally small, scalability is not a primary issue.403404  // Find an insertion point.  Since the queue is in reverse order (so we405  // can pop rather than unshift), and later timers with the same time stamp406  // should be executed later, we look for the element strictly greater than407  // the one we are inserting.408409  for (var i = queue.length; i != 0; i--) {410    if (queue[i - 1].runAtMillis > timeout.runAtMillis) {411      break;412    }413    queue[i] = queue[i - 1];414  }415416  queue[i] = timeout;417};418419420/**421 * Maximum 32-bit signed integer.422 *423 * Timeouts over this time return immediately in many browsers, due to integer424 * overflow.  Such known browsers include Firefox, Chrome, and Safari, but not425 * IE.426 *427 * @type {number}428 * @private429 */430goog.testing.MockClock.MAX_INT_ = 2147483647;431432433/**434 * Schedules a function to be called after {@code millis} milliseconds.435 * Mock implementation for setTimeout.436 * @param {Function} funcToCall The function to call.437 * @param {number} millis The number of milliseconds to call it after.438 * @return {number} The number of timeouts created.439 * @private440 */441goog.testing.MockClock.prototype.setTimeout_ = function(funcToCall, millis) {442  if (millis > goog.testing.MockClock.MAX_INT_) {443    throw Error(444        'Bad timeout value: ' + millis + '.  Timeouts over MAX_INT ' +445        '(24.8 days) cause timeouts to be fired ' +446        'immediately in most browsers, except for IE.');447  }448  this.timeoutsMade_ = this.timeoutsMade_ + 1;449  this.scheduleFunction_(this.timeoutsMade_, funcToCall, millis, false);450  return this.timeoutsMade_;451};452453454/**455 * Schedules a function to be called every {@code millis} milliseconds.456 * Mock implementation for setInterval.457 * @param {Function} funcToCall The function to call.458 * @param {number} millis The number of milliseconds between calls.459 * @return {number} The number of timeouts created.460 * @private461 */462goog.testing.MockClock.prototype.setInterval_ = function(funcToCall, millis) {463  this.timeoutsMade_ = this.timeoutsMade_ + 1;464  this.scheduleFunction_(this.timeoutsMade_, funcToCall, millis, true);465  return this.timeoutsMade_;466};467468469/**470 * Schedules a function to be called when an animation frame is triggered.471 * Mock implementation for requestAnimationFrame.472 * @param {Function} funcToCall The function to call.473 * @return {number} The number of timeouts created.474 * @private475 */476goog.testing.MockClock.prototype.requestAnimationFrame_ = function(funcToCall) {477  return this.setTimeout_(goog.bind(function() {478    if (funcToCall) {479      funcToCall(this.getCurrentTime());480    } else if (goog.global.mozRequestAnimationFrame) {481      var event = new goog.testing.events.Event('MozBeforePaint', goog.global);482      event['timeStamp'] = this.getCurrentTime();483      goog.testing.events.fireBrowserEvent(event);484    }485  }, this), goog.testing.MockClock.REQUEST_ANIMATION_FRAME_TIMEOUT);486};487488489/**490 * Schedules a function to be called immediately after the current JS491 * execution.492 * Mock implementation for setImmediate.493 * @param {Function} funcToCall The function to call.494 * @return {number} The number of timeouts created.495 * @private496 */497goog.testing.MockClock.prototype.setImmediate_ = function(funcToCall) {498  return this.setTimeout_(funcToCall, 0);499};500501502/**503 * Clears a timeout.504 * Mock implementation for clearTimeout.505 * @param {number} timeoutKey The timeout key to clear.506 * @private507 */508goog.testing.MockClock.prototype.clearTimeout_ = function(timeoutKey) {509  // Some common libraries register static state with timers.510  // This is bad. It leads to all sorts of crazy test problems where511  // 1) Test A sets up a new mock clock and a static timer.512  // 2) Test B sets up a new mock clock, but re-uses the static timer513  //    from Test A.514  // 3) A timeout key from test A gets cleared, breaking a timeout in515  //    Test B.516  //517  // For now, we just hackily fail silently if someone tries to clear a timeout518  // key before we've allocated it.519  // Ideally, we should throw an exception if we see this happening.520  //521  // TODO(user): We might also try allocating timeout ids from a global522  // pool rather than a local pool.523  if (this.isTimeoutSet(timeoutKey)) {524    this.deletedKeys_[timeoutKey] = true;525  }526};527528529/**530 * Clears an interval.531 * Mock implementation for clearInterval.532 * @param {number} timeoutKey The interval key to clear.533 * @private534 */535goog.testing.MockClock.prototype.clearInterval_ = function(timeoutKey) {536  this.clearTimeout_(timeoutKey);537};538539540/**541 * Clears a requestAnimationFrame.542 * Mock implementation for cancelRequestAnimationFrame.543 * @param {number} timeoutKey The requestAnimationFrame key to clear.544 * @private545 */546goog.testing.MockClock.prototype.cancelRequestAnimationFrame_ =547    function(timeoutKey) {548  this.clearTimeout_(timeoutKey);549};