fake-async-test.js 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780
  1. 'use strict';
  2. /**
  3. * @license Angular v<unknown>
  4. * (c) 2010-2024 Google LLC. https://angular.io/
  5. * License: MIT
  6. */
  7. const global = (typeof window === 'object' && window) || (typeof self === 'object' && self) || globalThis.global;
  8. const OriginalDate = global.Date;
  9. // Since when we compile this file to `es2015`, and if we define
  10. // this `FakeDate` as `class FakeDate`, and then set `FakeDate.prototype`
  11. // there will be an error which is `Cannot assign to read only property 'prototype'`
  12. // so we need to use function implementation here.
  13. function FakeDate() {
  14. if (arguments.length === 0) {
  15. const d = new OriginalDate();
  16. d.setTime(FakeDate.now());
  17. return d;
  18. }
  19. else {
  20. const args = Array.prototype.slice.call(arguments);
  21. return new OriginalDate(...args);
  22. }
  23. }
  24. FakeDate.now = function () {
  25. const fakeAsyncTestZoneSpec = Zone.current.get('FakeAsyncTestZoneSpec');
  26. if (fakeAsyncTestZoneSpec) {
  27. return fakeAsyncTestZoneSpec.getFakeSystemTime();
  28. }
  29. return OriginalDate.now.apply(this, arguments);
  30. };
  31. FakeDate.UTC = OriginalDate.UTC;
  32. FakeDate.parse = OriginalDate.parse;
  33. // keep a reference for zone patched timer function
  34. let patchedTimers;
  35. const timeoutCallback = function () { };
  36. class Scheduler {
  37. // Next scheduler id.
  38. static { this.nextNodeJSId = 1; }
  39. static { this.nextId = -1; }
  40. constructor() {
  41. // Scheduler queue with the tuple of end time and callback function - sorted by end time.
  42. this._schedulerQueue = [];
  43. // Current simulated time in millis.
  44. this._currentTickTime = 0;
  45. // Current fake system base time in millis.
  46. this._currentFakeBaseSystemTime = OriginalDate.now();
  47. // track requeuePeriodicTimer
  48. this._currentTickRequeuePeriodicEntries = [];
  49. }
  50. static getNextId() {
  51. const id = patchedTimers.nativeSetTimeout.call(global, timeoutCallback, 0);
  52. patchedTimers.nativeClearTimeout.call(global, id);
  53. if (typeof id === 'number') {
  54. return id;
  55. }
  56. // in NodeJS, we just use a number for fakeAsync, since it will not
  57. // conflict with native TimeoutId
  58. return Scheduler.nextNodeJSId++;
  59. }
  60. getCurrentTickTime() {
  61. return this._currentTickTime;
  62. }
  63. getFakeSystemTime() {
  64. return this._currentFakeBaseSystemTime + this._currentTickTime;
  65. }
  66. setFakeBaseSystemTime(fakeBaseSystemTime) {
  67. this._currentFakeBaseSystemTime = fakeBaseSystemTime;
  68. }
  69. getRealSystemTime() {
  70. return OriginalDate.now();
  71. }
  72. scheduleFunction(cb, delay, options) {
  73. options = {
  74. ...{
  75. args: [],
  76. isPeriodic: false,
  77. isRequestAnimationFrame: false,
  78. id: -1,
  79. isRequeuePeriodic: false,
  80. },
  81. ...options,
  82. };
  83. let currentId = options.id < 0 ? Scheduler.nextId : options.id;
  84. Scheduler.nextId = Scheduler.getNextId();
  85. let endTime = this._currentTickTime + delay;
  86. // Insert so that scheduler queue remains sorted by end time.
  87. let newEntry = {
  88. endTime: endTime,
  89. id: currentId,
  90. func: cb,
  91. args: options.args,
  92. delay: delay,
  93. isPeriodic: options.isPeriodic,
  94. isRequestAnimationFrame: options.isRequestAnimationFrame,
  95. };
  96. if (options.isRequeuePeriodic) {
  97. this._currentTickRequeuePeriodicEntries.push(newEntry);
  98. }
  99. let i = 0;
  100. for (; i < this._schedulerQueue.length; i++) {
  101. let currentEntry = this._schedulerQueue[i];
  102. if (newEntry.endTime < currentEntry.endTime) {
  103. break;
  104. }
  105. }
  106. this._schedulerQueue.splice(i, 0, newEntry);
  107. return currentId;
  108. }
  109. removeScheduledFunctionWithId(id) {
  110. for (let i = 0; i < this._schedulerQueue.length; i++) {
  111. if (this._schedulerQueue[i].id == id) {
  112. this._schedulerQueue.splice(i, 1);
  113. break;
  114. }
  115. }
  116. }
  117. removeAll() {
  118. this._schedulerQueue = [];
  119. }
  120. getTimerCount() {
  121. return this._schedulerQueue.length;
  122. }
  123. tickToNext(step = 1, doTick, tickOptions) {
  124. if (this._schedulerQueue.length < step) {
  125. return;
  126. }
  127. // Find the last task currently queued in the scheduler queue and tick
  128. // till that time.
  129. const startTime = this._currentTickTime;
  130. const targetTask = this._schedulerQueue[step - 1];
  131. this.tick(targetTask.endTime - startTime, doTick, tickOptions);
  132. }
  133. tick(millis = 0, doTick, tickOptions) {
  134. let finalTime = this._currentTickTime + millis;
  135. let lastCurrentTime = 0;
  136. tickOptions = Object.assign({ processNewMacroTasksSynchronously: true }, tickOptions);
  137. // we need to copy the schedulerQueue so nested timeout
  138. // will not be wrongly called in the current tick
  139. // https://github.com/angular/angular/issues/33799
  140. const schedulerQueue = tickOptions.processNewMacroTasksSynchronously
  141. ? this._schedulerQueue
  142. : this._schedulerQueue.slice();
  143. if (schedulerQueue.length === 0 && doTick) {
  144. doTick(millis);
  145. return;
  146. }
  147. while (schedulerQueue.length > 0) {
  148. // clear requeueEntries before each loop
  149. this._currentTickRequeuePeriodicEntries = [];
  150. let current = schedulerQueue[0];
  151. if (finalTime < current.endTime) {
  152. // Done processing the queue since it's sorted by endTime.
  153. break;
  154. }
  155. else {
  156. // Time to run scheduled function. Remove it from the head of queue.
  157. let current = schedulerQueue.shift();
  158. if (!tickOptions.processNewMacroTasksSynchronously) {
  159. const idx = this._schedulerQueue.indexOf(current);
  160. if (idx >= 0) {
  161. this._schedulerQueue.splice(idx, 1);
  162. }
  163. }
  164. lastCurrentTime = this._currentTickTime;
  165. this._currentTickTime = current.endTime;
  166. if (doTick) {
  167. doTick(this._currentTickTime - lastCurrentTime);
  168. }
  169. let retval = current.func.apply(global, current.isRequestAnimationFrame ? [this._currentTickTime] : current.args);
  170. if (!retval) {
  171. // Uncaught exception in the current scheduled function. Stop processing the queue.
  172. break;
  173. }
  174. // check is there any requeue periodic entry is added in
  175. // current loop, if there is, we need to add to current loop
  176. if (!tickOptions.processNewMacroTasksSynchronously) {
  177. this._currentTickRequeuePeriodicEntries.forEach((newEntry) => {
  178. let i = 0;
  179. for (; i < schedulerQueue.length; i++) {
  180. const currentEntry = schedulerQueue[i];
  181. if (newEntry.endTime < currentEntry.endTime) {
  182. break;
  183. }
  184. }
  185. schedulerQueue.splice(i, 0, newEntry);
  186. });
  187. }
  188. }
  189. }
  190. lastCurrentTime = this._currentTickTime;
  191. this._currentTickTime = finalTime;
  192. if (doTick) {
  193. doTick(this._currentTickTime - lastCurrentTime);
  194. }
  195. }
  196. flushOnlyPendingTimers(doTick) {
  197. if (this._schedulerQueue.length === 0) {
  198. return 0;
  199. }
  200. // Find the last task currently queued in the scheduler queue and tick
  201. // till that time.
  202. const startTime = this._currentTickTime;
  203. const lastTask = this._schedulerQueue[this._schedulerQueue.length - 1];
  204. this.tick(lastTask.endTime - startTime, doTick, { processNewMacroTasksSynchronously: false });
  205. return this._currentTickTime - startTime;
  206. }
  207. flush(limit = 20, flushPeriodic = false, doTick) {
  208. if (flushPeriodic) {
  209. return this.flushPeriodic(doTick);
  210. }
  211. else {
  212. return this.flushNonPeriodic(limit, doTick);
  213. }
  214. }
  215. flushPeriodic(doTick) {
  216. if (this._schedulerQueue.length === 0) {
  217. return 0;
  218. }
  219. // Find the last task currently queued in the scheduler queue and tick
  220. // till that time.
  221. const startTime = this._currentTickTime;
  222. const lastTask = this._schedulerQueue[this._schedulerQueue.length - 1];
  223. this.tick(lastTask.endTime - startTime, doTick);
  224. return this._currentTickTime - startTime;
  225. }
  226. flushNonPeriodic(limit, doTick) {
  227. const startTime = this._currentTickTime;
  228. let lastCurrentTime = 0;
  229. let count = 0;
  230. while (this._schedulerQueue.length > 0) {
  231. count++;
  232. if (count > limit) {
  233. throw new Error('flush failed after reaching the limit of ' +
  234. limit +
  235. ' tasks. Does your code use a polling timeout?');
  236. }
  237. // flush only non-periodic timers.
  238. // If the only remaining tasks are periodic(or requestAnimationFrame), finish flushing.
  239. if (this._schedulerQueue.filter((task) => !task.isPeriodic && !task.isRequestAnimationFrame)
  240. .length === 0) {
  241. break;
  242. }
  243. const current = this._schedulerQueue.shift();
  244. lastCurrentTime = this._currentTickTime;
  245. this._currentTickTime = current.endTime;
  246. if (doTick) {
  247. // Update any secondary schedulers like Jasmine mock Date.
  248. doTick(this._currentTickTime - lastCurrentTime);
  249. }
  250. const retval = current.func.apply(global, current.args);
  251. if (!retval) {
  252. // Uncaught exception in the current scheduled function. Stop processing the queue.
  253. break;
  254. }
  255. }
  256. return this._currentTickTime - startTime;
  257. }
  258. }
  259. class FakeAsyncTestZoneSpec {
  260. static assertInZone() {
  261. if (Zone.current.get('FakeAsyncTestZoneSpec') == null) {
  262. throw new Error('The code should be running in the fakeAsync zone to call this function');
  263. }
  264. }
  265. constructor(namePrefix, trackPendingRequestAnimationFrame = false, macroTaskOptions) {
  266. this.trackPendingRequestAnimationFrame = trackPendingRequestAnimationFrame;
  267. this.macroTaskOptions = macroTaskOptions;
  268. this._scheduler = new Scheduler();
  269. this._microtasks = [];
  270. this._lastError = null;
  271. this._uncaughtPromiseErrors = Promise[Zone.__symbol__('uncaughtPromiseErrors')];
  272. this.pendingPeriodicTimers = [];
  273. this.pendingTimers = [];
  274. this.patchDateLocked = false;
  275. this.properties = { 'FakeAsyncTestZoneSpec': this };
  276. this.name = 'fakeAsyncTestZone for ' + namePrefix;
  277. // in case user can't access the construction of FakeAsyncTestSpec
  278. // user can also define macroTaskOptions by define a global variable.
  279. if (!this.macroTaskOptions) {
  280. this.macroTaskOptions = global[Zone.__symbol__('FakeAsyncTestMacroTask')];
  281. }
  282. }
  283. _fnAndFlush(fn, completers) {
  284. return (...args) => {
  285. fn.apply(global, args);
  286. if (this._lastError === null) {
  287. // Success
  288. if (completers.onSuccess != null) {
  289. completers.onSuccess.apply(global);
  290. }
  291. // Flush microtasks only on success.
  292. this.flushMicrotasks();
  293. }
  294. else {
  295. // Failure
  296. if (completers.onError != null) {
  297. completers.onError.apply(global);
  298. }
  299. }
  300. // Return true if there were no errors, false otherwise.
  301. return this._lastError === null;
  302. };
  303. }
  304. static _removeTimer(timers, id) {
  305. let index = timers.indexOf(id);
  306. if (index > -1) {
  307. timers.splice(index, 1);
  308. }
  309. }
  310. _dequeueTimer(id) {
  311. return () => {
  312. FakeAsyncTestZoneSpec._removeTimer(this.pendingTimers, id);
  313. };
  314. }
  315. _requeuePeriodicTimer(fn, interval, args, id) {
  316. return () => {
  317. // Requeue the timer callback if it's not been canceled.
  318. if (this.pendingPeriodicTimers.indexOf(id) !== -1) {
  319. this._scheduler.scheduleFunction(fn, interval, {
  320. args,
  321. isPeriodic: true,
  322. id,
  323. isRequeuePeriodic: true,
  324. });
  325. }
  326. };
  327. }
  328. _dequeuePeriodicTimer(id) {
  329. return () => {
  330. FakeAsyncTestZoneSpec._removeTimer(this.pendingPeriodicTimers, id);
  331. };
  332. }
  333. _setTimeout(fn, delay, args, isTimer = true) {
  334. let removeTimerFn = this._dequeueTimer(Scheduler.nextId);
  335. // Queue the callback and dequeue the timer on success and error.
  336. let cb = this._fnAndFlush(fn, { onSuccess: removeTimerFn, onError: removeTimerFn });
  337. let id = this._scheduler.scheduleFunction(cb, delay, { args, isRequestAnimationFrame: !isTimer });
  338. if (isTimer) {
  339. this.pendingTimers.push(id);
  340. }
  341. return id;
  342. }
  343. _clearTimeout(id) {
  344. FakeAsyncTestZoneSpec._removeTimer(this.pendingTimers, id);
  345. this._scheduler.removeScheduledFunctionWithId(id);
  346. }
  347. _setInterval(fn, interval, args) {
  348. let id = Scheduler.nextId;
  349. let completers = { onSuccess: null, onError: this._dequeuePeriodicTimer(id) };
  350. let cb = this._fnAndFlush(fn, completers);
  351. // Use the callback created above to requeue on success.
  352. completers.onSuccess = this._requeuePeriodicTimer(cb, interval, args, id);
  353. // Queue the callback and dequeue the periodic timer only on error.
  354. this._scheduler.scheduleFunction(cb, interval, { args, isPeriodic: true });
  355. this.pendingPeriodicTimers.push(id);
  356. return id;
  357. }
  358. _clearInterval(id) {
  359. FakeAsyncTestZoneSpec._removeTimer(this.pendingPeriodicTimers, id);
  360. this._scheduler.removeScheduledFunctionWithId(id);
  361. }
  362. _resetLastErrorAndThrow() {
  363. let error = this._lastError || this._uncaughtPromiseErrors[0];
  364. this._uncaughtPromiseErrors.length = 0;
  365. this._lastError = null;
  366. throw error;
  367. }
  368. getCurrentTickTime() {
  369. return this._scheduler.getCurrentTickTime();
  370. }
  371. getFakeSystemTime() {
  372. return this._scheduler.getFakeSystemTime();
  373. }
  374. setFakeBaseSystemTime(realTime) {
  375. this._scheduler.setFakeBaseSystemTime(realTime);
  376. }
  377. getRealSystemTime() {
  378. return this._scheduler.getRealSystemTime();
  379. }
  380. static patchDate() {
  381. if (!!global[Zone.__symbol__('disableDatePatching')]) {
  382. // we don't want to patch global Date
  383. // because in some case, global Date
  384. // is already being patched, we need to provide
  385. // an option to let user still use their
  386. // own version of Date.
  387. return;
  388. }
  389. if (global['Date'] === FakeDate) {
  390. // already patched
  391. return;
  392. }
  393. global['Date'] = FakeDate;
  394. FakeDate.prototype = OriginalDate.prototype;
  395. // try check and reset timers
  396. // because jasmine.clock().install() may
  397. // have replaced the global timer
  398. FakeAsyncTestZoneSpec.checkTimerPatch();
  399. }
  400. static resetDate() {
  401. if (global['Date'] === FakeDate) {
  402. global['Date'] = OriginalDate;
  403. }
  404. }
  405. static checkTimerPatch() {
  406. if (!patchedTimers) {
  407. throw new Error('Expected timers to have been patched.');
  408. }
  409. if (global.setTimeout !== patchedTimers.setTimeout) {
  410. global.setTimeout = patchedTimers.setTimeout;
  411. global.clearTimeout = patchedTimers.clearTimeout;
  412. }
  413. if (global.setInterval !== patchedTimers.setInterval) {
  414. global.setInterval = patchedTimers.setInterval;
  415. global.clearInterval = patchedTimers.clearInterval;
  416. }
  417. }
  418. lockDatePatch() {
  419. this.patchDateLocked = true;
  420. FakeAsyncTestZoneSpec.patchDate();
  421. }
  422. unlockDatePatch() {
  423. this.patchDateLocked = false;
  424. FakeAsyncTestZoneSpec.resetDate();
  425. }
  426. tickToNext(steps = 1, doTick, tickOptions = { processNewMacroTasksSynchronously: true }) {
  427. if (steps <= 0) {
  428. return;
  429. }
  430. FakeAsyncTestZoneSpec.assertInZone();
  431. this.flushMicrotasks();
  432. this._scheduler.tickToNext(steps, doTick, tickOptions);
  433. if (this._lastError !== null) {
  434. this._resetLastErrorAndThrow();
  435. }
  436. }
  437. tick(millis = 0, doTick, tickOptions = { processNewMacroTasksSynchronously: true }) {
  438. FakeAsyncTestZoneSpec.assertInZone();
  439. this.flushMicrotasks();
  440. this._scheduler.tick(millis, doTick, tickOptions);
  441. if (this._lastError !== null) {
  442. this._resetLastErrorAndThrow();
  443. }
  444. }
  445. flushMicrotasks() {
  446. FakeAsyncTestZoneSpec.assertInZone();
  447. const flushErrors = () => {
  448. if (this._lastError !== null || this._uncaughtPromiseErrors.length) {
  449. // If there is an error stop processing the microtask queue and rethrow the error.
  450. this._resetLastErrorAndThrow();
  451. }
  452. };
  453. while (this._microtasks.length > 0) {
  454. let microtask = this._microtasks.shift();
  455. microtask.func.apply(microtask.target, microtask.args);
  456. }
  457. flushErrors();
  458. }
  459. flush(limit, flushPeriodic, doTick) {
  460. FakeAsyncTestZoneSpec.assertInZone();
  461. this.flushMicrotasks();
  462. const elapsed = this._scheduler.flush(limit, flushPeriodic, doTick);
  463. if (this._lastError !== null) {
  464. this._resetLastErrorAndThrow();
  465. }
  466. return elapsed;
  467. }
  468. flushOnlyPendingTimers(doTick) {
  469. FakeAsyncTestZoneSpec.assertInZone();
  470. this.flushMicrotasks();
  471. const elapsed = this._scheduler.flushOnlyPendingTimers(doTick);
  472. if (this._lastError !== null) {
  473. this._resetLastErrorAndThrow();
  474. }
  475. return elapsed;
  476. }
  477. removeAllTimers() {
  478. FakeAsyncTestZoneSpec.assertInZone();
  479. this._scheduler.removeAll();
  480. this.pendingPeriodicTimers = [];
  481. this.pendingTimers = [];
  482. }
  483. getTimerCount() {
  484. return this._scheduler.getTimerCount() + this._microtasks.length;
  485. }
  486. onScheduleTask(delegate, current, target, task) {
  487. switch (task.type) {
  488. case 'microTask':
  489. let args = task.data && task.data.args;
  490. // should pass additional arguments to callback if have any
  491. // currently we know process.nextTick will have such additional
  492. // arguments
  493. let additionalArgs;
  494. if (args) {
  495. let callbackIndex = task.data.cbIdx;
  496. if (typeof args.length === 'number' && args.length > callbackIndex + 1) {
  497. additionalArgs = Array.prototype.slice.call(args, callbackIndex + 1);
  498. }
  499. }
  500. this._microtasks.push({
  501. func: task.invoke,
  502. args: additionalArgs,
  503. target: task.data && task.data.target,
  504. });
  505. break;
  506. case 'macroTask':
  507. switch (task.source) {
  508. case 'setTimeout':
  509. task.data['handleId'] = this._setTimeout(task.invoke, task.data['delay'], Array.prototype.slice.call(task.data['args'], 2));
  510. break;
  511. case 'setImmediate':
  512. task.data['handleId'] = this._setTimeout(task.invoke, 0, Array.prototype.slice.call(task.data['args'], 1));
  513. break;
  514. case 'setInterval':
  515. task.data['handleId'] = this._setInterval(task.invoke, task.data['delay'], Array.prototype.slice.call(task.data['args'], 2));
  516. break;
  517. case 'XMLHttpRequest.send':
  518. throw new Error('Cannot make XHRs from within a fake async test. Request URL: ' +
  519. task.data['url']);
  520. case 'requestAnimationFrame':
  521. case 'webkitRequestAnimationFrame':
  522. case 'mozRequestAnimationFrame':
  523. // Simulate a requestAnimationFrame by using a setTimeout with 16 ms.
  524. // (60 frames per second)
  525. task.data['handleId'] = this._setTimeout(task.invoke, 16, task.data['args'], this.trackPendingRequestAnimationFrame);
  526. break;
  527. default:
  528. // user can define which macroTask they want to support by passing
  529. // macroTaskOptions
  530. const macroTaskOption = this.findMacroTaskOption(task);
  531. if (macroTaskOption) {
  532. const args = task.data && task.data['args'];
  533. const delay = args && args.length > 1 ? args[1] : 0;
  534. let callbackArgs = macroTaskOption.callbackArgs ? macroTaskOption.callbackArgs : args;
  535. if (!!macroTaskOption.isPeriodic) {
  536. // periodic macroTask, use setInterval to simulate
  537. task.data['handleId'] = this._setInterval(task.invoke, delay, callbackArgs);
  538. task.data.isPeriodic = true;
  539. }
  540. else {
  541. // not periodic, use setTimeout to simulate
  542. task.data['handleId'] = this._setTimeout(task.invoke, delay, callbackArgs);
  543. }
  544. break;
  545. }
  546. throw new Error('Unknown macroTask scheduled in fake async test: ' + task.source);
  547. }
  548. break;
  549. case 'eventTask':
  550. task = delegate.scheduleTask(target, task);
  551. break;
  552. }
  553. return task;
  554. }
  555. onCancelTask(delegate, current, target, task) {
  556. switch (task.source) {
  557. case 'setTimeout':
  558. case 'requestAnimationFrame':
  559. case 'webkitRequestAnimationFrame':
  560. case 'mozRequestAnimationFrame':
  561. return this._clearTimeout(task.data['handleId']);
  562. case 'setInterval':
  563. return this._clearInterval(task.data['handleId']);
  564. default:
  565. // user can define which macroTask they want to support by passing
  566. // macroTaskOptions
  567. const macroTaskOption = this.findMacroTaskOption(task);
  568. if (macroTaskOption) {
  569. const handleId = task.data['handleId'];
  570. return macroTaskOption.isPeriodic
  571. ? this._clearInterval(handleId)
  572. : this._clearTimeout(handleId);
  573. }
  574. return delegate.cancelTask(target, task);
  575. }
  576. }
  577. onInvoke(delegate, current, target, callback, applyThis, applyArgs, source) {
  578. try {
  579. FakeAsyncTestZoneSpec.patchDate();
  580. return delegate.invoke(target, callback, applyThis, applyArgs, source);
  581. }
  582. finally {
  583. if (!this.patchDateLocked) {
  584. FakeAsyncTestZoneSpec.resetDate();
  585. }
  586. }
  587. }
  588. findMacroTaskOption(task) {
  589. if (!this.macroTaskOptions) {
  590. return null;
  591. }
  592. for (let i = 0; i < this.macroTaskOptions.length; i++) {
  593. const macroTaskOption = this.macroTaskOptions[i];
  594. if (macroTaskOption.source === task.source) {
  595. return macroTaskOption;
  596. }
  597. }
  598. return null;
  599. }
  600. onHandleError(parentZoneDelegate, currentZone, targetZone, error) {
  601. this._lastError = error;
  602. return false; // Don't propagate error to parent zone.
  603. }
  604. }
  605. let _fakeAsyncTestZoneSpec = null;
  606. function getProxyZoneSpec() {
  607. return Zone && Zone['ProxyZoneSpec'];
  608. }
  609. /**
  610. * Clears out the shared fake async zone for a test.
  611. * To be called in a global `beforeEach`.
  612. *
  613. * @experimental
  614. */
  615. function resetFakeAsyncZone() {
  616. if (_fakeAsyncTestZoneSpec) {
  617. _fakeAsyncTestZoneSpec.unlockDatePatch();
  618. }
  619. _fakeAsyncTestZoneSpec = null;
  620. // in node.js testing we may not have ProxyZoneSpec in which case there is nothing to reset.
  621. getProxyZoneSpec() && getProxyZoneSpec().assertPresent().resetDelegate();
  622. }
  623. /**
  624. * Wraps a function to be executed in the fakeAsync zone:
  625. * - microtasks are manually executed by calling `flushMicrotasks()`,
  626. * - timers are synchronous, `tick()` simulates the asynchronous passage of time.
  627. *
  628. * When flush is `false`, if there are any pending timers at the end of the function,
  629. * an exception will be thrown.
  630. *
  631. * Can be used to wrap inject() calls.
  632. *
  633. * ## Example
  634. *
  635. * {@example core/testing/ts/fake_async.ts region='basic'}
  636. *
  637. * @param fn
  638. * @param options
  639. * flush: when true, will drain the macrotask queue after the test function completes.
  640. * @returns The function wrapped to be executed in the fakeAsync zone
  641. *
  642. * @experimental
  643. */
  644. function fakeAsync(fn, options = {}) {
  645. const { flush = true } = options;
  646. // Not using an arrow function to preserve context passed from call site
  647. const fakeAsyncFn = function (...args) {
  648. const ProxyZoneSpec = getProxyZoneSpec();
  649. if (!ProxyZoneSpec) {
  650. throw new Error('ProxyZoneSpec is needed for the async() test helper but could not be found. ' +
  651. 'Please make sure that your environment includes zone.js/plugins/proxy');
  652. }
  653. const proxyZoneSpec = ProxyZoneSpec.assertPresent();
  654. if (Zone.current.get('FakeAsyncTestZoneSpec')) {
  655. throw new Error('fakeAsync() calls can not be nested');
  656. }
  657. try {
  658. // in case jasmine.clock init a fakeAsyncTestZoneSpec
  659. if (!_fakeAsyncTestZoneSpec) {
  660. const FakeAsyncTestZoneSpec = Zone && Zone['FakeAsyncTestZoneSpec'];
  661. if (proxyZoneSpec.getDelegate() instanceof FakeAsyncTestZoneSpec) {
  662. throw new Error('fakeAsync() calls can not be nested');
  663. }
  664. _fakeAsyncTestZoneSpec = new FakeAsyncTestZoneSpec();
  665. }
  666. let res;
  667. const lastProxyZoneSpec = proxyZoneSpec.getDelegate();
  668. proxyZoneSpec.setDelegate(_fakeAsyncTestZoneSpec);
  669. _fakeAsyncTestZoneSpec.lockDatePatch();
  670. try {
  671. res = fn.apply(this, args);
  672. if (flush) {
  673. _fakeAsyncTestZoneSpec.flush(20, true);
  674. }
  675. else {
  676. flushMicrotasks();
  677. }
  678. }
  679. finally {
  680. proxyZoneSpec.setDelegate(lastProxyZoneSpec);
  681. }
  682. if (!flush) {
  683. if (_fakeAsyncTestZoneSpec.pendingPeriodicTimers.length > 0) {
  684. throw new Error(`${_fakeAsyncTestZoneSpec.pendingPeriodicTimers.length} ` +
  685. `periodic timer(s) still in the queue.`);
  686. }
  687. if (_fakeAsyncTestZoneSpec.pendingTimers.length > 0) {
  688. throw new Error(`${_fakeAsyncTestZoneSpec.pendingTimers.length} timer(s) still in the queue.`);
  689. }
  690. }
  691. return res;
  692. }
  693. finally {
  694. resetFakeAsyncZone();
  695. }
  696. };
  697. fakeAsyncFn.isFakeAsync = true;
  698. return fakeAsyncFn;
  699. }
  700. function _getFakeAsyncZoneSpec() {
  701. if (_fakeAsyncTestZoneSpec == null) {
  702. _fakeAsyncTestZoneSpec = Zone.current.get('FakeAsyncTestZoneSpec');
  703. if (_fakeAsyncTestZoneSpec == null) {
  704. throw new Error('The code should be running in the fakeAsync zone to call this function');
  705. }
  706. }
  707. return _fakeAsyncTestZoneSpec;
  708. }
  709. /**
  710. * Simulates the asynchronous passage of time for the timers in the fakeAsync zone.
  711. *
  712. * The microtasks queue is drained at the very start of this function and after any timer
  713. * callback has been executed.
  714. *
  715. * ## Example
  716. *
  717. * {@example core/testing/ts/fake_async.ts region='basic'}
  718. *
  719. * @experimental
  720. */
  721. function tick(millis = 0, ignoreNestedTimeout = false) {
  722. _getFakeAsyncZoneSpec().tick(millis, null, ignoreNestedTimeout);
  723. }
  724. /**
  725. * Simulates the asynchronous passage of time for the timers in the fakeAsync zone by
  726. * draining the macrotask queue until it is empty. The returned value is the milliseconds
  727. * of time that would have been elapsed.
  728. *
  729. * @param maxTurns
  730. * @returns The simulated time elapsed, in millis.
  731. *
  732. * @experimental
  733. */
  734. function flush(maxTurns) {
  735. return _getFakeAsyncZoneSpec().flush(maxTurns);
  736. }
  737. /**
  738. * Discard all remaining periodic tasks.
  739. *
  740. * @experimental
  741. */
  742. function discardPeriodicTasks() {
  743. const zoneSpec = _getFakeAsyncZoneSpec();
  744. zoneSpec.pendingPeriodicTimers;
  745. zoneSpec.pendingPeriodicTimers.length = 0;
  746. }
  747. /**
  748. * Flush any pending microtasks.
  749. *
  750. * @experimental
  751. */
  752. function flushMicrotasks() {
  753. _getFakeAsyncZoneSpec().flushMicrotasks();
  754. }
  755. function patchFakeAsyncTest(Zone) {
  756. // Export the class so that new instances can be created with proper
  757. // constructor params.
  758. Zone['FakeAsyncTestZoneSpec'] = FakeAsyncTestZoneSpec;
  759. Zone.__load_patch('fakeasync', (global, Zone, api) => {
  760. Zone[api.symbol('fakeAsyncTest')] = {
  761. resetFakeAsyncZone,
  762. flushMicrotasks,
  763. discardPeriodicTasks,
  764. tick,
  765. flush,
  766. fakeAsync,
  767. };
  768. }, true);
  769. patchedTimers = {
  770. setTimeout: global.setTimeout,
  771. setInterval: global.setInterval,
  772. clearTimeout: global.clearTimeout,
  773. clearInterval: global.clearInterval,
  774. nativeSetTimeout: global[Zone.__symbol__('setTimeout')],
  775. nativeClearTimeout: global[Zone.__symbol__('clearTimeout')],
  776. };
  777. Scheduler.nextId = Scheduler.getNextId();
  778. }
  779. patchFakeAsyncTest(Zone);