fake-async-test.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. /**
  2. * @license
  3. * Copyright Google Inc. All Rights Reserved.
  4. *
  5. * Use of this source code is governed by an MIT-style license that can be
  6. * found in the LICENSE file at https://angular.io/license
  7. */
  8. (function(global: any) {
  9. interface ScheduledFunction {
  10. endTime: number;
  11. id: number;
  12. func: Function;
  13. args: any[];
  14. delay: number;
  15. isPeriodic: boolean;
  16. isRequestAnimationFrame: boolean;
  17. }
  18. interface MicroTaskScheduledFunction {
  19. func: Function;
  20. args?: any[];
  21. target: any;
  22. }
  23. interface MacroTaskOptions {
  24. source: string;
  25. isPeriodic?: boolean;
  26. callbackArgs?: any;
  27. }
  28. const OriginalDate = global.Date;
  29. class FakeDate {
  30. constructor() {
  31. if (arguments.length === 0) {
  32. const d = new OriginalDate();
  33. d.setTime(FakeDate.now());
  34. return d;
  35. } else {
  36. const args = Array.prototype.slice.call(arguments);
  37. return new OriginalDate(...args);
  38. }
  39. }
  40. static now() {
  41. const fakeAsyncTestZoneSpec = Zone.current.get('FakeAsyncTestZoneSpec');
  42. if (fakeAsyncTestZoneSpec) {
  43. return fakeAsyncTestZoneSpec.getCurrentRealTime() + fakeAsyncTestZoneSpec.getCurrentTime();
  44. }
  45. return OriginalDate.now.apply(this, arguments);
  46. }
  47. }
  48. (FakeDate as any).UTC = OriginalDate.UTC;
  49. (FakeDate as any).parse = OriginalDate.parse;
  50. // keep a reference for zone patched timer function
  51. const timers = {
  52. setTimeout: global.setTimeout,
  53. setInterval: global.setInterval,
  54. clearTimeout: global.clearTimeout,
  55. clearInterval: global.clearInterval
  56. };
  57. class Scheduler {
  58. // Next scheduler id.
  59. public static nextId: number = 1;
  60. // Scheduler queue with the tuple of end time and callback function - sorted by end time.
  61. private _schedulerQueue: ScheduledFunction[] = [];
  62. // Current simulated time in millis.
  63. private _currentTime: number = 0;
  64. // Current real time in millis.
  65. private _currentRealTime: number = OriginalDate.now();
  66. constructor() {}
  67. getCurrentTime() {
  68. return this._currentTime;
  69. }
  70. getCurrentRealTime() {
  71. return this._currentRealTime;
  72. }
  73. setCurrentRealTime(realTime: number) {
  74. this._currentRealTime = realTime;
  75. }
  76. scheduleFunction(
  77. cb: Function, delay: number, args: any[] = [], isPeriodic: boolean = false,
  78. isRequestAnimationFrame: boolean = false, id: number = -1): number {
  79. let currentId: number = id < 0 ? Scheduler.nextId++ : id;
  80. let endTime = this._currentTime + delay;
  81. // Insert so that scheduler queue remains sorted by end time.
  82. let newEntry: ScheduledFunction = {
  83. endTime: endTime,
  84. id: currentId,
  85. func: cb,
  86. args: args,
  87. delay: delay,
  88. isPeriodic: isPeriodic,
  89. isRequestAnimationFrame: isRequestAnimationFrame
  90. };
  91. let i = 0;
  92. for (; i < this._schedulerQueue.length; i++) {
  93. let currentEntry = this._schedulerQueue[i];
  94. if (newEntry.endTime < currentEntry.endTime) {
  95. break;
  96. }
  97. }
  98. this._schedulerQueue.splice(i, 0, newEntry);
  99. return currentId;
  100. }
  101. removeScheduledFunctionWithId(id: number): void {
  102. for (let i = 0; i < this._schedulerQueue.length; i++) {
  103. if (this._schedulerQueue[i].id == id) {
  104. this._schedulerQueue.splice(i, 1);
  105. break;
  106. }
  107. }
  108. }
  109. tick(millis: number = 0, doTick?: (elapsed: number) => void): void {
  110. let finalTime = this._currentTime + millis;
  111. let lastCurrentTime = 0;
  112. if (this._schedulerQueue.length === 0 && doTick) {
  113. doTick(millis);
  114. return;
  115. }
  116. while (this._schedulerQueue.length > 0) {
  117. let current = this._schedulerQueue[0];
  118. if (finalTime < current.endTime) {
  119. // Done processing the queue since it's sorted by endTime.
  120. break;
  121. } else {
  122. // Time to run scheduled function. Remove it from the head of queue.
  123. let current = this._schedulerQueue.shift()!;
  124. lastCurrentTime = this._currentTime;
  125. this._currentTime = current.endTime;
  126. if (doTick) {
  127. doTick(this._currentTime - lastCurrentTime);
  128. }
  129. let retval = current.func.apply(
  130. global, current.isRequestAnimationFrame ? [this._currentTime] : current.args);
  131. if (!retval) {
  132. // Uncaught exception in the current scheduled function. Stop processing the queue.
  133. break;
  134. }
  135. }
  136. }
  137. lastCurrentTime = this._currentTime;
  138. this._currentTime = finalTime;
  139. if (doTick) {
  140. doTick(this._currentTime - lastCurrentTime);
  141. }
  142. }
  143. flush(limit = 20, flushPeriodic = false, doTick?: (elapsed: number) => void): number {
  144. if (flushPeriodic) {
  145. return this.flushPeriodic(doTick);
  146. } else {
  147. return this.flushNonPeriodic(limit, doTick);
  148. }
  149. }
  150. private flushPeriodic(doTick?: (elapsed: number) => void): number {
  151. if (this._schedulerQueue.length === 0) {
  152. return 0;
  153. }
  154. // Find the last task currently queued in the scheduler queue and tick
  155. // till that time.
  156. const startTime = this._currentTime;
  157. const lastTask = this._schedulerQueue[this._schedulerQueue.length - 1];
  158. this.tick(lastTask.endTime - startTime, doTick);
  159. return this._currentTime - startTime;
  160. }
  161. private flushNonPeriodic(limit: number, doTick?: (elapsed: number) => void): number {
  162. const startTime = this._currentTime;
  163. let lastCurrentTime = 0;
  164. let count = 0;
  165. while (this._schedulerQueue.length > 0) {
  166. count++;
  167. if (count > limit) {
  168. throw new Error(
  169. 'flush failed after reaching the limit of ' + limit +
  170. ' tasks. Does your code use a polling timeout?');
  171. }
  172. // flush only non-periodic timers.
  173. // If the only remaining tasks are periodic(or requestAnimationFrame), finish flushing.
  174. if (this._schedulerQueue.filter(task => !task.isPeriodic && !task.isRequestAnimationFrame)
  175. .length === 0) {
  176. break;
  177. }
  178. const current = this._schedulerQueue.shift()!;
  179. lastCurrentTime = this._currentTime;
  180. this._currentTime = current.endTime;
  181. if (doTick) {
  182. // Update any secondary schedulers like Jasmine mock Date.
  183. doTick(this._currentTime - lastCurrentTime);
  184. }
  185. const retval = current.func.apply(global, current.args);
  186. if (!retval) {
  187. // Uncaught exception in the current scheduled function. Stop processing the queue.
  188. break;
  189. }
  190. }
  191. return this._currentTime - startTime;
  192. }
  193. }
  194. class FakeAsyncTestZoneSpec implements ZoneSpec {
  195. static assertInZone(): void {
  196. if (Zone.current.get('FakeAsyncTestZoneSpec') == null) {
  197. throw new Error('The code should be running in the fakeAsync zone to call this function');
  198. }
  199. }
  200. private _scheduler: Scheduler = new Scheduler();
  201. private _microtasks: MicroTaskScheduledFunction[] = [];
  202. private _lastError: Error|null = null;
  203. private _uncaughtPromiseErrors: {rejection: any}[] =
  204. (Promise as any)[(Zone as any).__symbol__('uncaughtPromiseErrors')];
  205. pendingPeriodicTimers: number[] = [];
  206. pendingTimers: number[] = [];
  207. private patchDateLocked = false;
  208. constructor(
  209. namePrefix: string, private trackPendingRequestAnimationFrame = false,
  210. private macroTaskOptions?: MacroTaskOptions[]) {
  211. this.name = 'fakeAsyncTestZone for ' + namePrefix;
  212. // in case user can't access the construction of FakeAsyncTestSpec
  213. // user can also define macroTaskOptions by define a global variable.
  214. if (!this.macroTaskOptions) {
  215. this.macroTaskOptions = global[Zone.__symbol__('FakeAsyncTestMacroTask')];
  216. }
  217. }
  218. private _fnAndFlush(fn: Function, completers: {onSuccess?: Function, onError?: Function}):
  219. Function {
  220. return (...args: any[]): boolean => {
  221. fn.apply(global, args);
  222. if (this._lastError === null) { // Success
  223. if (completers.onSuccess != null) {
  224. completers.onSuccess.apply(global);
  225. }
  226. // Flush microtasks only on success.
  227. this.flushMicrotasks();
  228. } else { // Failure
  229. if (completers.onError != null) {
  230. completers.onError.apply(global);
  231. }
  232. }
  233. // Return true if there were no errors, false otherwise.
  234. return this._lastError === null;
  235. };
  236. }
  237. private static _removeTimer(timers: number[], id: number): void {
  238. let index = timers.indexOf(id);
  239. if (index > -1) {
  240. timers.splice(index, 1);
  241. }
  242. }
  243. private _dequeueTimer(id: number): Function {
  244. return () => {
  245. FakeAsyncTestZoneSpec._removeTimer(this.pendingTimers, id);
  246. };
  247. }
  248. private _requeuePeriodicTimer(fn: Function, interval: number, args: any[], id: number): Function {
  249. return () => {
  250. // Requeue the timer callback if it's not been canceled.
  251. if (this.pendingPeriodicTimers.indexOf(id) !== -1) {
  252. this._scheduler.scheduleFunction(fn, interval, args, true, false, id);
  253. }
  254. };
  255. }
  256. private _dequeuePeriodicTimer(id: number): Function {
  257. return () => {
  258. FakeAsyncTestZoneSpec._removeTimer(this.pendingPeriodicTimers, id);
  259. };
  260. }
  261. private _setTimeout(fn: Function, delay: number, args: any[], isTimer = true): number {
  262. let removeTimerFn = this._dequeueTimer(Scheduler.nextId);
  263. // Queue the callback and dequeue the timer on success and error.
  264. let cb = this._fnAndFlush(fn, {onSuccess: removeTimerFn, onError: removeTimerFn});
  265. let id = this._scheduler.scheduleFunction(cb, delay, args, false, !isTimer);
  266. if (isTimer) {
  267. this.pendingTimers.push(id);
  268. }
  269. return id;
  270. }
  271. private _clearTimeout(id: number): void {
  272. FakeAsyncTestZoneSpec._removeTimer(this.pendingTimers, id);
  273. this._scheduler.removeScheduledFunctionWithId(id);
  274. }
  275. private _setInterval(fn: Function, interval: number, args: any[]): number {
  276. let id = Scheduler.nextId;
  277. let completers = {onSuccess: null as any, onError: this._dequeuePeriodicTimer(id)};
  278. let cb = this._fnAndFlush(fn, completers);
  279. // Use the callback created above to requeue on success.
  280. completers.onSuccess = this._requeuePeriodicTimer(cb, interval, args, id);
  281. // Queue the callback and dequeue the periodic timer only on error.
  282. this._scheduler.scheduleFunction(cb, interval, args, true);
  283. this.pendingPeriodicTimers.push(id);
  284. return id;
  285. }
  286. private _clearInterval(id: number): void {
  287. FakeAsyncTestZoneSpec._removeTimer(this.pendingPeriodicTimers, id);
  288. this._scheduler.removeScheduledFunctionWithId(id);
  289. }
  290. private _resetLastErrorAndThrow(): void {
  291. let error = this._lastError || this._uncaughtPromiseErrors[0];
  292. this._uncaughtPromiseErrors.length = 0;
  293. this._lastError = null;
  294. throw error;
  295. }
  296. getCurrentTime() {
  297. return this._scheduler.getCurrentTime();
  298. }
  299. getCurrentRealTime() {
  300. return this._scheduler.getCurrentRealTime();
  301. }
  302. setCurrentRealTime(realTime: number) {
  303. this._scheduler.setCurrentRealTime(realTime);
  304. }
  305. static patchDate() {
  306. if (!!global[Zone.__symbol__('disableDatePatching')]) {
  307. // we don't want to patch global Date
  308. // because in some case, global Date
  309. // is already being patched, we need to provide
  310. // an option to let user still use their
  311. // own version of Date.
  312. return;
  313. }
  314. if (global['Date'] === FakeDate) {
  315. // already patched
  316. return;
  317. }
  318. global['Date'] = FakeDate;
  319. FakeDate.prototype = OriginalDate.prototype;
  320. // try check and reset timers
  321. // because jasmine.clock().install() may
  322. // have replaced the global timer
  323. FakeAsyncTestZoneSpec.checkTimerPatch();
  324. }
  325. static resetDate() {
  326. if (global['Date'] === FakeDate) {
  327. global['Date'] = OriginalDate;
  328. }
  329. }
  330. static checkTimerPatch() {
  331. if (global.setTimeout !== timers.setTimeout) {
  332. global.setTimeout = timers.setTimeout;
  333. global.clearTimeout = timers.clearTimeout;
  334. }
  335. if (global.setInterval !== timers.setInterval) {
  336. global.setInterval = timers.setInterval;
  337. global.clearInterval = timers.clearInterval;
  338. }
  339. }
  340. lockDatePatch() {
  341. this.patchDateLocked = true;
  342. FakeAsyncTestZoneSpec.patchDate();
  343. }
  344. unlockDatePatch() {
  345. this.patchDateLocked = false;
  346. FakeAsyncTestZoneSpec.resetDate();
  347. }
  348. tick(millis: number = 0, doTick?: (elapsed: number) => void): void {
  349. FakeAsyncTestZoneSpec.assertInZone();
  350. this.flushMicrotasks();
  351. this._scheduler.tick(millis, doTick);
  352. if (this._lastError !== null) {
  353. this._resetLastErrorAndThrow();
  354. }
  355. }
  356. flushMicrotasks(): void {
  357. FakeAsyncTestZoneSpec.assertInZone();
  358. const flushErrors = () => {
  359. if (this._lastError !== null || this._uncaughtPromiseErrors.length) {
  360. // If there is an error stop processing the microtask queue and rethrow the error.
  361. this._resetLastErrorAndThrow();
  362. }
  363. };
  364. while (this._microtasks.length > 0) {
  365. let microtask = this._microtasks.shift()!;
  366. microtask.func.apply(microtask.target, microtask.args);
  367. }
  368. flushErrors();
  369. }
  370. flush(limit?: number, flushPeriodic?: boolean, doTick?: (elapsed: number) => void): number {
  371. FakeAsyncTestZoneSpec.assertInZone();
  372. this.flushMicrotasks();
  373. const elapsed = this._scheduler.flush(limit, flushPeriodic, doTick);
  374. if (this._lastError !== null) {
  375. this._resetLastErrorAndThrow();
  376. }
  377. return elapsed;
  378. }
  379. // ZoneSpec implementation below.
  380. name: string;
  381. properties: {[key: string]: any} = {'FakeAsyncTestZoneSpec': this};
  382. onScheduleTask(delegate: ZoneDelegate, current: Zone, target: Zone, task: Task): Task {
  383. switch (task.type) {
  384. case 'microTask':
  385. let args = task.data && (task.data as any).args;
  386. // should pass additional arguments to callback if have any
  387. // currently we know process.nextTick will have such additional
  388. // arguments
  389. let additionalArgs: any[]|undefined;
  390. if (args) {
  391. let callbackIndex = (task.data as any).cbIdx;
  392. if (typeof args.length === 'number' && args.length > callbackIndex + 1) {
  393. additionalArgs = Array.prototype.slice.call(args, callbackIndex + 1);
  394. }
  395. }
  396. this._microtasks.push({
  397. func: task.invoke,
  398. args: additionalArgs,
  399. target: task.data && (task.data as any).target
  400. });
  401. break;
  402. case 'macroTask':
  403. switch (task.source) {
  404. case 'setTimeout':
  405. task.data!['handleId'] = this._setTimeout(
  406. task.invoke, task.data!['delay']!,
  407. Array.prototype.slice.call((task.data as any)['args'], 2));
  408. break;
  409. case 'setImmediate':
  410. task.data!['handleId'] = this._setTimeout(
  411. task.invoke, 0, Array.prototype.slice.call((task.data as any)['args'], 1));
  412. break;
  413. case 'setInterval':
  414. task.data!['handleId'] = this._setInterval(
  415. task.invoke, task.data!['delay']!,
  416. Array.prototype.slice.call((task.data as any)['args'], 2));
  417. break;
  418. case 'XMLHttpRequest.send':
  419. throw new Error(
  420. 'Cannot make XHRs from within a fake async test. Request URL: ' +
  421. (task.data as any)['url']);
  422. case 'requestAnimationFrame':
  423. case 'webkitRequestAnimationFrame':
  424. case 'mozRequestAnimationFrame':
  425. // Simulate a requestAnimationFrame by using a setTimeout with 16 ms.
  426. // (60 frames per second)
  427. task.data!['handleId'] = this._setTimeout(
  428. task.invoke, 16, (task.data as any)['args'],
  429. this.trackPendingRequestAnimationFrame);
  430. break;
  431. default:
  432. // user can define which macroTask they want to support by passing
  433. // macroTaskOptions
  434. const macroTaskOption = this.findMacroTaskOption(task);
  435. if (macroTaskOption) {
  436. const args = task.data && (task.data as any)['args'];
  437. const delay = args && args.length > 1 ? args[1] : 0;
  438. let callbackArgs = macroTaskOption.callbackArgs ? macroTaskOption.callbackArgs : args;
  439. if (!!macroTaskOption.isPeriodic) {
  440. // periodic macroTask, use setInterval to simulate
  441. task.data!['handleId'] = this._setInterval(task.invoke, delay, callbackArgs);
  442. task.data!.isPeriodic = true;
  443. } else {
  444. // not periodic, use setTimeout to simulate
  445. task.data!['handleId'] = this._setTimeout(task.invoke, delay, callbackArgs);
  446. }
  447. break;
  448. }
  449. throw new Error('Unknown macroTask scheduled in fake async test: ' + task.source);
  450. }
  451. break;
  452. case 'eventTask':
  453. task = delegate.scheduleTask(target, task);
  454. break;
  455. }
  456. return task;
  457. }
  458. onCancelTask(delegate: ZoneDelegate, current: Zone, target: Zone, task: Task): any {
  459. switch (task.source) {
  460. case 'setTimeout':
  461. case 'requestAnimationFrame':
  462. case 'webkitRequestAnimationFrame':
  463. case 'mozRequestAnimationFrame':
  464. return this._clearTimeout(<number>task.data!['handleId']);
  465. case 'setInterval':
  466. return this._clearInterval(<number>task.data!['handleId']);
  467. default:
  468. // user can define which macroTask they want to support by passing
  469. // macroTaskOptions
  470. const macroTaskOption = this.findMacroTaskOption(task);
  471. if (macroTaskOption) {
  472. const handleId: number = <number>task.data!['handleId'];
  473. return macroTaskOption.isPeriodic ? this._clearInterval(handleId) :
  474. this._clearTimeout(handleId);
  475. }
  476. return delegate.cancelTask(target, task);
  477. }
  478. }
  479. onInvoke(
  480. delegate: ZoneDelegate, current: Zone, target: Zone, callback: Function, applyThis: any,
  481. applyArgs?: any[], source?: string): any {
  482. try {
  483. FakeAsyncTestZoneSpec.patchDate();
  484. return delegate.invoke(target, callback, applyThis, applyArgs, source);
  485. } finally {
  486. if (!this.patchDateLocked) {
  487. FakeAsyncTestZoneSpec.resetDate();
  488. }
  489. }
  490. }
  491. findMacroTaskOption(task: Task) {
  492. if (!this.macroTaskOptions) {
  493. return null;
  494. }
  495. for (let i = 0; i < this.macroTaskOptions.length; i++) {
  496. const macroTaskOption = this.macroTaskOptions[i];
  497. if (macroTaskOption.source === task.source) {
  498. return macroTaskOption;
  499. }
  500. }
  501. return null;
  502. }
  503. onHandleError(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, error: any):
  504. boolean {
  505. this._lastError = error;
  506. return false; // Don't propagate error to parent zone.
  507. }
  508. }
  509. // Export the class so that new instances can be created with proper
  510. // constructor params.
  511. (Zone as any)['FakeAsyncTestZoneSpec'] = FakeAsyncTestZoneSpec;
  512. })(typeof window === 'object' && window || typeof self === 'object' && self || global);