mockclock.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635
  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. * @fileoverview Mock Clock implementation for working with setTimeout,
  16. * setInterval, clearTimeout and clearInterval within unit tests.
  17. *
  18. * Derived from jsUnitMockTimeout.js, contributed to JsUnit by
  19. * Pivotal Computer Systems, www.pivotalsf.com
  20. *
  21. */
  22. goog.setTestOnly('goog.testing.MockClock');
  23. goog.provide('goog.testing.MockClock');
  24. goog.require('goog.Disposable');
  25. /** @suppress {extraRequire} */
  26. goog.require('goog.Promise');
  27. goog.require('goog.Thenable');
  28. goog.require('goog.async.run');
  29. goog.require('goog.testing.PropertyReplacer');
  30. goog.require('goog.testing.events');
  31. goog.require('goog.testing.events.Event');
  32. /**
  33. * Class for unit testing code that uses setTimeout and clearTimeout.
  34. *
  35. * NOTE: If you are using MockClock to test code that makes use of
  36. * goog.fx.Animation, then you must either:
  37. *
  38. * 1. Install and dispose of the MockClock in setUpPage() and tearDownPage()
  39. * respectively (rather than setUp()/tearDown()).
  40. *
  41. * or
  42. *
  43. * 2. Ensure that every test clears the animation queue by calling
  44. * mockClock.tick(x) at the end of each test function (where `x` is large
  45. * enough to complete all animations).
  46. *
  47. * Otherwise, if any animation is left pending at the time that
  48. * MockClock.dispose() is called, that will permanently prevent any future
  49. * animations from playing on the page.
  50. *
  51. * @param {boolean=} opt_autoInstall Install the MockClock at construction time.
  52. * @constructor
  53. * @extends {goog.Disposable}
  54. * @final
  55. */
  56. goog.testing.MockClock = function(opt_autoInstall) {
  57. goog.Disposable.call(this);
  58. /**
  59. * Reverse-order queue of timers to fire.
  60. *
  61. * The last item of the queue is popped off. Insertion happens from the
  62. * right. For example, the expiration times for each element of the queue
  63. * might be in the order 300, 200, 200.
  64. *
  65. * @type {Array<Object>}
  66. * @private
  67. */
  68. this.queue_ = [];
  69. /**
  70. * Set of timeouts that should be treated as cancelled.
  71. *
  72. * Rather than removing cancelled timers directly from the queue, this set
  73. * simply marks them as deleted so that they can be ignored when their
  74. * turn comes up. The keys are the timeout keys that are cancelled, each
  75. * mapping to true.
  76. *
  77. * @private {Object<number, boolean>}
  78. */
  79. this.deletedKeys_ = {};
  80. if (opt_autoInstall) {
  81. this.install();
  82. }
  83. };
  84. goog.inherits(goog.testing.MockClock, goog.Disposable);
  85. /**
  86. * Default wait timeout for mocking requestAnimationFrame (in milliseconds).
  87. *
  88. * @type {number}
  89. * @const
  90. */
  91. goog.testing.MockClock.REQUEST_ANIMATION_FRAME_TIMEOUT = 20;
  92. /**
  93. * ID to use for next timeout. Timeout IDs must never be reused, even across
  94. * MockClock instances.
  95. * @public {number}
  96. */
  97. goog.testing.MockClock.nextId = Math.round(Math.random() * 10000);
  98. /**
  99. * Count of the number of setTimeout/setInterval/etc. calls received by this
  100. * instance.
  101. * @type {number}
  102. * @private
  103. */
  104. goog.testing.MockClock.prototype.timeoutsMade_ = 0;
  105. /**
  106. * Count of the number of timeout/interval/etc. callbacks triggered by this
  107. * instance.
  108. * @type {number}
  109. * @private
  110. */
  111. goog.testing.MockClock.prototype.callbacksTriggered_ = 0;
  112. /**
  113. * PropertyReplacer instance which overwrites and resets setTimeout,
  114. * setInterval, etc. or null if the MockClock is not installed.
  115. * @type {goog.testing.PropertyReplacer}
  116. * @private
  117. */
  118. goog.testing.MockClock.prototype.replacer_ = null;
  119. /**
  120. * The current simulated time in milliseconds.
  121. * @type {number}
  122. * @private
  123. */
  124. goog.testing.MockClock.prototype.nowMillis_ = 0;
  125. /**
  126. * Additional delay between the time a timeout was set to fire, and the time
  127. * it actually fires. Useful for testing workarounds for this Firefox 2 bug:
  128. * https://bugzilla.mozilla.org/show_bug.cgi?id=291386
  129. * May be negative.
  130. * @type {number}
  131. * @private
  132. */
  133. goog.testing.MockClock.prototype.timeoutDelay_ = 0;
  134. /**
  135. * The real set timeout for reference.
  136. * @const @private {!Function}
  137. */
  138. goog.testing.MockClock.REAL_SETTIMEOUT_ = goog.global.setTimeout;
  139. /**
  140. * Installs the MockClock by overriding the global object's implementation of
  141. * setTimeout, setInterval, clearTimeout and clearInterval.
  142. */
  143. goog.testing.MockClock.prototype.install = function() {
  144. if (!this.replacer_) {
  145. if (goog.testing.MockClock.REAL_SETTIMEOUT_ !== goog.global.setTimeout) {
  146. if (typeof console !== 'undefined' && console.warn) {
  147. console.warn(
  148. 'Non default setTimeout detected. ' +
  149. 'Use of multiple MockClock instances or other clock mocking ' +
  150. 'should be avoided due to unspecified behavior and ' +
  151. 'the resulting fragility.');
  152. }
  153. }
  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 &&
  167. goog.async.run.forceNextTick(goog.testing.MockClock.REAL_SETTIMEOUT_);
  168. // Replace the requestAnimationFrame functions.
  169. this.replaceRequestAnimationFrame_();
  170. // PropertyReplacer#set can't be called with renameable functions.
  171. this.oldGoogNow_ = goog.now;
  172. goog.now = goog.bind(this.getCurrentTime, this);
  173. }
  174. };
  175. /**
  176. * Installs the mocks for requestAnimationFrame and cancelRequestAnimationFrame.
  177. * @private
  178. */
  179. goog.testing.MockClock.prototype.replaceRequestAnimationFrame_ = function() {
  180. var r = this.replacer_;
  181. var requestFuncs = [
  182. 'requestAnimationFrame', 'webkitRequestAnimationFrame',
  183. 'mozRequestAnimationFrame', 'oRequestAnimationFrame',
  184. 'msRequestAnimationFrame'
  185. ];
  186. var cancelFuncs = [
  187. 'cancelAnimationFrame', 'cancelRequestAnimationFrame',
  188. 'webkitCancelRequestAnimationFrame', 'mozCancelRequestAnimationFrame',
  189. 'oCancelRequestAnimationFrame', 'msCancelRequestAnimationFrame'
  190. ];
  191. for (var i = 0; i < requestFuncs.length; ++i) {
  192. if (goog.global && goog.global[requestFuncs[i]]) {
  193. r.set(
  194. goog.global, requestFuncs[i],
  195. goog.bind(this.requestAnimationFrame_, this));
  196. }
  197. }
  198. for (var i = 0; i < cancelFuncs.length; ++i) {
  199. if (goog.global && goog.global[cancelFuncs[i]]) {
  200. r.set(
  201. goog.global, cancelFuncs[i],
  202. goog.bind(this.cancelRequestAnimationFrame_, this));
  203. }
  204. }
  205. };
  206. /**
  207. * Removes the MockClock's hooks into the global object's functions and revert
  208. * to their original values.
  209. */
  210. goog.testing.MockClock.prototype.uninstall = function() {
  211. if (this.replacer_) {
  212. this.replacer_.reset();
  213. this.replacer_ = null;
  214. goog.now = this.oldGoogNow_;
  215. }
  216. this.resetAsyncQueue_();
  217. };
  218. /** @override */
  219. goog.testing.MockClock.prototype.disposeInternal = function() {
  220. this.uninstall();
  221. this.queue_ = null;
  222. this.deletedKeys_ = null;
  223. goog.testing.MockClock.superClass_.disposeInternal.call(this);
  224. };
  225. /**
  226. * Resets the MockClock, removing all timeouts that are scheduled and resets
  227. * the fake timer count.
  228. */
  229. goog.testing.MockClock.prototype.reset = function() {
  230. this.queue_ = [];
  231. this.deletedKeys_ = {};
  232. this.nowMillis_ = 0;
  233. this.timeoutsMade_ = 0;
  234. this.callbacksTriggered_ = 0;
  235. this.timeoutDelay_ = 0;
  236. this.resetAsyncQueue_();
  237. };
  238. /**
  239. * Resets the async queue when this clock resets.
  240. * @private
  241. */
  242. goog.testing.MockClock.prototype.resetAsyncQueue_ = function() {
  243. goog.async.run.resetQueue();
  244. };
  245. /**
  246. * Sets the amount of time between when a timeout is scheduled to fire and when
  247. * it actually fires.
  248. * @param {number} delay The delay in milliseconds. May be negative.
  249. */
  250. goog.testing.MockClock.prototype.setTimeoutDelay = function(delay) {
  251. this.timeoutDelay_ = delay;
  252. };
  253. /**
  254. * @return {number} delay The amount of time between when a timeout is
  255. * scheduled to fire and when it actually fires, in milliseconds. May
  256. * be negative.
  257. */
  258. goog.testing.MockClock.prototype.getTimeoutDelay = function() {
  259. return this.timeoutDelay_;
  260. };
  261. /**
  262. * Increments the MockClock's time by a given number of milliseconds, running
  263. * any functions that are now overdue.
  264. * @param {number=} opt_millis Number of milliseconds to increment the counter.
  265. * If not specified, clock ticks 1 millisecond.
  266. * @return {number} Current mock time in milliseconds.
  267. */
  268. goog.testing.MockClock.prototype.tick = function(opt_millis) {
  269. if (typeof opt_millis != 'number') {
  270. opt_millis = 1;
  271. }
  272. var endTime = this.nowMillis_ + opt_millis;
  273. this.runFunctionsWithinRange_(endTime);
  274. this.nowMillis_ = endTime;
  275. return endTime;
  276. };
  277. /**
  278. * Takes a promise and then ticks the mock clock. If the promise successfully
  279. * resolves, returns the value produced by the promise. If the promise is
  280. * rejected, it throws the rejection as an exception. If the promise is not
  281. * resolved at all, throws an exception.
  282. * Also ticks the general clock by the specified amount.
  283. *
  284. * @param {!goog.Thenable<T>} promise A promise that should be resolved after
  285. * the mockClock is ticked for the given opt_millis.
  286. * @param {number=} opt_millis Number of milliseconds to increment the counter.
  287. * If not specified, clock ticks 1 millisecond.
  288. * @return {T}
  289. * @template T
  290. */
  291. goog.testing.MockClock.prototype.tickPromise = function(promise, opt_millis) {
  292. var value;
  293. var error;
  294. var resolved = false;
  295. promise.then(
  296. function(v) {
  297. value = v;
  298. resolved = true;
  299. },
  300. function(e) {
  301. error = e;
  302. resolved = true;
  303. });
  304. this.tick(opt_millis);
  305. if (!resolved) {
  306. throw new Error(
  307. 'Promise was expected to be resolved after mock clock tick.');
  308. }
  309. if (error) {
  310. throw error;
  311. }
  312. return value;
  313. };
  314. /**
  315. * @return {number} The number of timeouts or intervals that have been
  316. * scheduled. A setInterval call is only counted once.
  317. */
  318. goog.testing.MockClock.prototype.getTimeoutsMade = function() {
  319. return this.timeoutsMade_;
  320. };
  321. /**
  322. * @return {number} The number of timeout or interval callbacks that have been
  323. * triggered. For setInterval, each callback is counted separately.
  324. */
  325. goog.testing.MockClock.prototype.getCallbacksTriggered = function() {
  326. return this.callbacksTriggered_;
  327. };
  328. /**
  329. * @return {number} The MockClock's current time in milliseconds.
  330. */
  331. goog.testing.MockClock.prototype.getCurrentTime = function() {
  332. return this.nowMillis_;
  333. };
  334. /**
  335. * @param {number} timeoutKey The timeout key.
  336. * @return {boolean} Whether the timer has been set and not cleared,
  337. * independent of the timeout's expiration. In other words, the timeout
  338. * could have passed or could be scheduled for the future. Either way,
  339. * this function returns true or false depending only on whether the
  340. * provided timeoutKey represents a timeout that has been set and not
  341. * cleared.
  342. */
  343. goog.testing.MockClock.prototype.isTimeoutSet = function(timeoutKey) {
  344. return timeoutKey < goog.testing.MockClock.nextId &&
  345. timeoutKey >= goog.testing.MockClock.nextId - this.timeoutsMade_ &&
  346. !this.deletedKeys_[timeoutKey];
  347. };
  348. /**
  349. * Runs any function that is scheduled before a certain time. Timeouts can
  350. * be made to fire early or late if timeoutDelay_ is non-0.
  351. * @param {number} endTime The latest time in the range, in milliseconds.
  352. * @private
  353. */
  354. goog.testing.MockClock.prototype.runFunctionsWithinRange_ = function(endTime) {
  355. var adjustedEndTime = endTime - this.timeoutDelay_;
  356. // Repeatedly pop off the last item since the queue is always sorted.
  357. while (this.queue_ && this.queue_.length &&
  358. this.queue_[this.queue_.length - 1].runAtMillis <= adjustedEndTime) {
  359. var timeout = this.queue_.pop();
  360. if (!(timeout.timeoutKey in this.deletedKeys_)) {
  361. // Only move time forwards.
  362. this.nowMillis_ =
  363. Math.max(this.nowMillis_, timeout.runAtMillis + this.timeoutDelay_);
  364. // Call timeout in global scope and pass the timeout key as the argument.
  365. this.callbacksTriggered_++;
  366. timeout.funcToCall.call(goog.global, timeout.timeoutKey);
  367. // In case the interval was cleared in the funcToCall
  368. if (timeout.recurring) {
  369. this.scheduleFunction_(
  370. timeout.timeoutKey, timeout.funcToCall, timeout.millis, true);
  371. }
  372. }
  373. }
  374. };
  375. /**
  376. * Schedules a function to be run at a certain time.
  377. * @param {number} timeoutKey The timeout key.
  378. * @param {Function} funcToCall The function to call.
  379. * @param {number} millis The number of milliseconds to call it in.
  380. * @param {boolean} recurring Whether to function call should recur.
  381. * @private
  382. */
  383. goog.testing.MockClock.prototype.scheduleFunction_ = function(
  384. timeoutKey, funcToCall, millis, recurring) {
  385. if (!goog.isFunction(funcToCall)) {
  386. // Early error for debuggability rather than dying in the next .tick()
  387. throw new TypeError(
  388. 'The provided callback must be a function, not a ' + typeof funcToCall);
  389. }
  390. var timeout = {
  391. runAtMillis: this.nowMillis_ + millis,
  392. funcToCall: funcToCall,
  393. recurring: recurring,
  394. timeoutKey: timeoutKey,
  395. millis: millis
  396. };
  397. goog.testing.MockClock.insert_(timeout, this.queue_);
  398. };
  399. /**
  400. * Inserts a timer descriptor into a descending-order queue.
  401. *
  402. * Later-inserted duplicates appear at lower indices. For example, the
  403. * asterisk in (5,4,*,3,2,1) would be the insertion point for 3.
  404. *
  405. * @param {Object} timeout The timeout to insert, with numerical runAtMillis
  406. * property.
  407. * @param {Array<Object>} queue The queue to insert into, with each element
  408. * having a numerical runAtMillis property.
  409. * @private
  410. */
  411. goog.testing.MockClock.insert_ = function(timeout, queue) {
  412. // Although insertion of N items is quadratic, requiring goog.structs.Heap
  413. // from a unit test will make tests more prone to breakage. Since unit
  414. // tests are normally small, scalability is not a primary issue.
  415. // Find an insertion point. Since the queue is in reverse order (so we
  416. // can pop rather than unshift), and later timers with the same time stamp
  417. // should be executed later, we look for the element strictly greater than
  418. // the one we are inserting.
  419. for (var i = queue.length; i != 0; i--) {
  420. if (queue[i - 1].runAtMillis > timeout.runAtMillis) {
  421. break;
  422. }
  423. queue[i] = queue[i - 1];
  424. }
  425. queue[i] = timeout;
  426. };
  427. /**
  428. * Maximum 32-bit signed integer.
  429. *
  430. * Timeouts over this time return immediately in many browsers, due to integer
  431. * overflow. Such known browsers include Firefox, Chrome, and Safari, but not
  432. * IE.
  433. *
  434. * @type {number}
  435. * @private
  436. */
  437. goog.testing.MockClock.MAX_INT_ = 2147483647;
  438. /**
  439. * Schedules a function to be called after {@code millis} milliseconds.
  440. * Mock implementation for setTimeout.
  441. * @param {Function} funcToCall The function to call.
  442. * @param {number=} opt_millis The number of milliseconds to call it after.
  443. * @return {number} The number of timeouts created.
  444. * @private
  445. */
  446. goog.testing.MockClock.prototype.setTimeout_ = function(
  447. funcToCall, opt_millis) {
  448. var millis = opt_millis || 0;
  449. if (millis > goog.testing.MockClock.MAX_INT_) {
  450. throw Error(
  451. 'Bad timeout value: ' + millis + '. Timeouts over MAX_INT ' +
  452. '(24.8 days) cause timeouts to be fired ' +
  453. 'immediately in most browsers, except for IE.');
  454. }
  455. this.timeoutsMade_++;
  456. this.scheduleFunction_(
  457. goog.testing.MockClock.nextId, funcToCall, millis, false);
  458. return goog.testing.MockClock.nextId++;
  459. };
  460. /**
  461. * Schedules a function to be called every {@code millis} milliseconds.
  462. * Mock implementation for setInterval.
  463. * @param {Function} funcToCall The function to call.
  464. * @param {number=} opt_millis The number of milliseconds between calls.
  465. * @return {number} The number of timeouts created.
  466. * @private
  467. */
  468. goog.testing.MockClock.prototype.setInterval_ = function(
  469. funcToCall, opt_millis) {
  470. var millis = opt_millis || 0;
  471. this.timeoutsMade_++;
  472. this.scheduleFunction_(
  473. goog.testing.MockClock.nextId, funcToCall, millis, true);
  474. return goog.testing.MockClock.nextId++;
  475. };
  476. /**
  477. * Schedules a function to be called when an animation frame is triggered.
  478. * Mock implementation for requestAnimationFrame.
  479. * @param {Function} funcToCall The function to call.
  480. * @return {number} The number of timeouts created.
  481. * @private
  482. */
  483. goog.testing.MockClock.prototype.requestAnimationFrame_ = function(funcToCall) {
  484. return this.setTimeout_(goog.bind(function() {
  485. if (funcToCall) {
  486. funcToCall(this.getCurrentTime());
  487. } else if (goog.global.mozRequestAnimationFrame) {
  488. var event = new goog.testing.events.Event('MozBeforePaint', goog.global);
  489. event['timeStamp'] = this.getCurrentTime();
  490. goog.testing.events.fireBrowserEvent(event);
  491. }
  492. }, this), goog.testing.MockClock.REQUEST_ANIMATION_FRAME_TIMEOUT);
  493. };
  494. /**
  495. * Schedules a function to be called immediately after the current JS
  496. * execution.
  497. * Mock implementation for setImmediate.
  498. * @param {Function} funcToCall The function to call.
  499. * @return {number} The number of timeouts created.
  500. * @private
  501. */
  502. goog.testing.MockClock.prototype.setImmediate_ = function(funcToCall) {
  503. return this.setTimeout_(funcToCall, 0);
  504. };
  505. /**
  506. * Clears a timeout.
  507. * Mock implementation for clearTimeout.
  508. * @param {number} timeoutKey The timeout key to clear.
  509. * @private
  510. */
  511. goog.testing.MockClock.prototype.clearTimeout_ = function(timeoutKey) {
  512. // Some common libraries register static state with timers.
  513. // This is bad. It leads to all sorts of crazy test problems where
  514. // 1) Test A sets up a new mock clock and a static timer.
  515. // 2) Test B sets up a new mock clock, but re-uses the static timer
  516. // from Test A.
  517. // 3) A timeout key from test A gets cleared, breaking a timeout in
  518. // Test B.
  519. //
  520. // For now, we just hackily fail silently if someone tries to clear a timeout
  521. // key before we've allocated it.
  522. // Ideally, we should throw an exception if we see this happening.
  523. if (this.isTimeoutSet(timeoutKey)) {
  524. this.deletedKeys_[timeoutKey] = true;
  525. }
  526. };
  527. /**
  528. * Clears an interval.
  529. * Mock implementation for clearInterval.
  530. * @param {number} timeoutKey The interval key to clear.
  531. * @private
  532. */
  533. goog.testing.MockClock.prototype.clearInterval_ = function(timeoutKey) {
  534. this.clearTimeout_(timeoutKey);
  535. };
  536. /**
  537. * Clears a requestAnimationFrame.
  538. * Mock implementation for cancelRequestAnimationFrame.
  539. * @param {number} timeoutKey The requestAnimationFrame key to clear.
  540. * @private
  541. */
  542. goog.testing.MockClock.prototype.cancelRequestAnimationFrame_ = function(
  543. timeoutKey) {
  544. this.clearTimeout_(timeoutKey);
  545. };