`zone.js` 0.8.25 introduces `zone-testing` bundle and move all `fakeAsync/async` logic from `@angular/core/testing` to `zone.js` package. But in case some user still using the old version of `zone.js`, an old version of `fakeAsync/async` logic were still kept inside `@angular/core/testing` package as `fallback` logic. Since now `Angular8+` already use `zone.js 0.9+`, so those fallback logic is removed. PR Close #37879
		
			
				
	
	
		
			867 lines
		
	
	
		
			28 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			867 lines
		
	
	
		
			28 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| /**
 | |
|  * @license
 | |
|  * Copyright Google LLC All Rights Reserved.
 | |
|  *
 | |
|  * Use of this source code is governed by an MIT-style license that can be
 | |
|  * found in the LICENSE file at https://angular.io/license
 | |
|  */
 | |
| 
 | |
| (function(global: any) {
 | |
| interface ScheduledFunction {
 | |
|   endTime: number;
 | |
|   id: number;
 | |
|   func: Function;
 | |
|   args: any[];
 | |
|   delay: number;
 | |
|   isPeriodic: boolean;
 | |
|   isRequestAnimationFrame: boolean;
 | |
| }
 | |
| 
 | |
| interface MicroTaskScheduledFunction {
 | |
|   func: Function;
 | |
|   args?: any[];
 | |
|   target: any;
 | |
| }
 | |
| 
 | |
| interface MacroTaskOptions {
 | |
|   source: string;
 | |
|   isPeriodic?: boolean;
 | |
|   callbackArgs?: any;
 | |
| }
 | |
| 
 | |
| const OriginalDate = global.Date;
 | |
| // Since when we compile this file to `es2015`, and if we define
 | |
| // this `FakeDate` as `class FakeDate`, and then set `FakeDate.prototype`
 | |
| // there will be an error which is `Cannot assign to read only property 'prototype'`
 | |
| // so we need to use function implementation here.
 | |
| function FakeDate() {
 | |
|   if (arguments.length === 0) {
 | |
|     const d = new OriginalDate();
 | |
|     d.setTime(FakeDate.now());
 | |
|     return d;
 | |
|   } else {
 | |
|     const args = Array.prototype.slice.call(arguments);
 | |
|     return new OriginalDate(...args);
 | |
|   }
 | |
| }
 | |
| 
 | |
| FakeDate.now = function(this: unknown) {
 | |
|   const fakeAsyncTestZoneSpec = Zone.current.get('FakeAsyncTestZoneSpec');
 | |
|   if (fakeAsyncTestZoneSpec) {
 | |
|     return fakeAsyncTestZoneSpec.getFakeSystemTime();
 | |
|   }
 | |
|   return OriginalDate.now.apply(this, arguments);
 | |
| };
 | |
| 
 | |
| FakeDate.UTC = OriginalDate.UTC;
 | |
| FakeDate.parse = OriginalDate.parse;
 | |
| 
 | |
| // keep a reference for zone patched timer function
 | |
| const timers = {
 | |
|   setTimeout: global.setTimeout,
 | |
|   setInterval: global.setInterval,
 | |
|   clearTimeout: global.clearTimeout,
 | |
|   clearInterval: global.clearInterval
 | |
| };
 | |
| 
 | |
| class Scheduler {
 | |
|   // Next scheduler id.
 | |
|   public static nextId: number = 1;
 | |
| 
 | |
|   // Scheduler queue with the tuple of end time and callback function - sorted by end time.
 | |
|   private _schedulerQueue: ScheduledFunction[] = [];
 | |
|   // Current simulated time in millis.
 | |
|   private _currentTickTime: number = 0;
 | |
|   // Current fake system base time in millis.
 | |
|   private _currentFakeBaseSystemTime: number = OriginalDate.now();
 | |
|   // track requeuePeriodicTimer
 | |
|   private _currentTickRequeuePeriodicEntries: any[] = [];
 | |
| 
 | |
|   constructor() {}
 | |
| 
 | |
|   getCurrentTickTime() {
 | |
|     return this._currentTickTime;
 | |
|   }
 | |
| 
 | |
|   getFakeSystemTime() {
 | |
|     return this._currentFakeBaseSystemTime + this._currentTickTime;
 | |
|   }
 | |
| 
 | |
|   setFakeBaseSystemTime(fakeBaseSystemTime: number) {
 | |
|     this._currentFakeBaseSystemTime = fakeBaseSystemTime;
 | |
|   }
 | |
| 
 | |
|   getRealSystemTime() {
 | |
|     return OriginalDate.now();
 | |
|   }
 | |
| 
 | |
|   scheduleFunction(cb: Function, delay: number, options?: {
 | |
|     args?: any[],
 | |
|     isPeriodic?: boolean,
 | |
|     isRequestAnimationFrame?: boolean,
 | |
|     id?: number,
 | |
|     isRequeuePeriodic?: boolean
 | |
|   }): number {
 | |
|     options = {
 | |
|       ...{
 | |
|         args: [],
 | |
|         isPeriodic: false,
 | |
|         isRequestAnimationFrame: false,
 | |
|         id: -1,
 | |
|         isRequeuePeriodic: false
 | |
|       },
 | |
|       ...options
 | |
|     };
 | |
|     let currentId = options.id! < 0 ? Scheduler.nextId++ : options.id!;
 | |
|     let endTime = this._currentTickTime + delay;
 | |
| 
 | |
|     // Insert so that scheduler queue remains sorted by end time.
 | |
|     let newEntry: ScheduledFunction = {
 | |
|       endTime: endTime,
 | |
|       id: currentId,
 | |
|       func: cb,
 | |
|       args: options.args!,
 | |
|       delay: delay,
 | |
|       isPeriodic: options.isPeriodic!,
 | |
|       isRequestAnimationFrame: options.isRequestAnimationFrame!
 | |
|     };
 | |
|     if (options.isRequeuePeriodic!) {
 | |
|       this._currentTickRequeuePeriodicEntries.push(newEntry);
 | |
|     }
 | |
|     let i = 0;
 | |
|     for (; i < this._schedulerQueue.length; i++) {
 | |
|       let currentEntry = this._schedulerQueue[i];
 | |
|       if (newEntry.endTime < currentEntry.endTime) {
 | |
|         break;
 | |
|       }
 | |
|     }
 | |
|     this._schedulerQueue.splice(i, 0, newEntry);
 | |
|     return currentId;
 | |
|   }
 | |
| 
 | |
|   removeScheduledFunctionWithId(id: number): void {
 | |
|     for (let i = 0; i < this._schedulerQueue.length; i++) {
 | |
|       if (this._schedulerQueue[i].id == id) {
 | |
|         this._schedulerQueue.splice(i, 1);
 | |
|         break;
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   removeAll(): void {
 | |
|     this._schedulerQueue = [];
 | |
|   }
 | |
| 
 | |
|   getTimerCount(): number {
 | |
|     return this._schedulerQueue.length;
 | |
|   }
 | |
| 
 | |
|   tickToNext(step: number = 1, doTick?: (elapsed: number) => void, tickOptions?: {
 | |
|     processNewMacroTasksSynchronously: boolean
 | |
|   }) {
 | |
|     if (this._schedulerQueue.length < step) {
 | |
|       return;
 | |
|     }
 | |
|     // Find the last task currently queued in the scheduler queue and tick
 | |
|     // till that time.
 | |
|     const startTime = this._currentTickTime;
 | |
|     const targetTask = this._schedulerQueue[step - 1];
 | |
|     this.tick(targetTask.endTime - startTime, doTick, tickOptions);
 | |
|   }
 | |
| 
 | |
|   tick(millis: number = 0, doTick?: (elapsed: number) => void, tickOptions?: {
 | |
|     processNewMacroTasksSynchronously: boolean
 | |
|   }): void {
 | |
|     let finalTime = this._currentTickTime + millis;
 | |
|     let lastCurrentTime = 0;
 | |
|     tickOptions = Object.assign({processNewMacroTasksSynchronously: true}, tickOptions);
 | |
|     // we need to copy the schedulerQueue so nested timeout
 | |
|     // will not be wrongly called in the current tick
 | |
|     // https://github.com/angular/angular/issues/33799
 | |
|     const schedulerQueue = tickOptions.processNewMacroTasksSynchronously ?
 | |
|         this._schedulerQueue :
 | |
|         this._schedulerQueue.slice();
 | |
|     if (schedulerQueue.length === 0 && doTick) {
 | |
|       doTick(millis);
 | |
|       return;
 | |
|     }
 | |
|     while (schedulerQueue.length > 0) {
 | |
|       // clear requeueEntries before each loop
 | |
|       this._currentTickRequeuePeriodicEntries = [];
 | |
|       let current = schedulerQueue[0];
 | |
|       if (finalTime < current.endTime) {
 | |
|         // Done processing the queue since it's sorted by endTime.
 | |
|         break;
 | |
|       } else {
 | |
|         // Time to run scheduled function. Remove it from the head of queue.
 | |
|         let current = schedulerQueue.shift()!;
 | |
|         if (!tickOptions.processNewMacroTasksSynchronously) {
 | |
|           const idx = this._schedulerQueue.indexOf(current);
 | |
|           if (idx >= 0) {
 | |
|             this._schedulerQueue.splice(idx, 1);
 | |
|           }
 | |
|         }
 | |
|         lastCurrentTime = this._currentTickTime;
 | |
|         this._currentTickTime = current.endTime;
 | |
|         if (doTick) {
 | |
|           doTick(this._currentTickTime - lastCurrentTime);
 | |
|         }
 | |
|         let retval = current.func.apply(
 | |
|             global, current.isRequestAnimationFrame ? [this._currentTickTime] : current.args);
 | |
|         if (!retval) {
 | |
|           // Uncaught exception in the current scheduled function. Stop processing the queue.
 | |
|           break;
 | |
|         }
 | |
| 
 | |
|         // check is there any requeue periodic entry is added in
 | |
|         // current loop, if there is, we need to add to current loop
 | |
|         if (!tickOptions.processNewMacroTasksSynchronously) {
 | |
|           this._currentTickRequeuePeriodicEntries.forEach(newEntry => {
 | |
|             let i = 0;
 | |
|             for (; i < schedulerQueue.length; i++) {
 | |
|               const currentEntry = schedulerQueue[i];
 | |
|               if (newEntry.endTime < currentEntry.endTime) {
 | |
|                 break;
 | |
|               }
 | |
|             }
 | |
|             schedulerQueue.splice(i, 0, newEntry);
 | |
|           });
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|     lastCurrentTime = this._currentTickTime;
 | |
|     this._currentTickTime = finalTime;
 | |
|     if (doTick) {
 | |
|       doTick(this._currentTickTime - lastCurrentTime);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   flushOnlyPendingTimers(doTick?: (elapsed: number) => void): number {
 | |
|     if (this._schedulerQueue.length === 0) {
 | |
|       return 0;
 | |
|     }
 | |
|     // Find the last task currently queued in the scheduler queue and tick
 | |
|     // till that time.
 | |
|     const startTime = this._currentTickTime;
 | |
|     const lastTask = this._schedulerQueue[this._schedulerQueue.length - 1];
 | |
|     this.tick(lastTask.endTime - startTime, doTick, {processNewMacroTasksSynchronously: false});
 | |
|     return this._currentTickTime - startTime;
 | |
|   }
 | |
| 
 | |
|   flush(limit = 20, flushPeriodic = false, doTick?: (elapsed: number) => void): number {
 | |
|     if (flushPeriodic) {
 | |
|       return this.flushPeriodic(doTick);
 | |
|     } else {
 | |
|       return this.flushNonPeriodic(limit, doTick);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   private flushPeriodic(doTick?: (elapsed: number) => void): number {
 | |
|     if (this._schedulerQueue.length === 0) {
 | |
|       return 0;
 | |
|     }
 | |
|     // Find the last task currently queued in the scheduler queue and tick
 | |
|     // till that time.
 | |
|     const startTime = this._currentTickTime;
 | |
|     const lastTask = this._schedulerQueue[this._schedulerQueue.length - 1];
 | |
|     this.tick(lastTask.endTime - startTime, doTick);
 | |
|     return this._currentTickTime - startTime;
 | |
|   }
 | |
| 
 | |
|   private flushNonPeriodic(limit: number, doTick?: (elapsed: number) => void): number {
 | |
|     const startTime = this._currentTickTime;
 | |
|     let lastCurrentTime = 0;
 | |
|     let count = 0;
 | |
|     while (this._schedulerQueue.length > 0) {
 | |
|       count++;
 | |
|       if (count > limit) {
 | |
|         throw new Error(
 | |
|             'flush failed after reaching the limit of ' + limit +
 | |
|             ' tasks. Does your code use a polling timeout?');
 | |
|       }
 | |
| 
 | |
|       // flush only non-periodic timers.
 | |
|       // If the only remaining tasks are periodic(or requestAnimationFrame), finish flushing.
 | |
|       if (this._schedulerQueue.filter(task => !task.isPeriodic && !task.isRequestAnimationFrame)
 | |
|               .length === 0) {
 | |
|         break;
 | |
|       }
 | |
| 
 | |
|       const current = this._schedulerQueue.shift()!;
 | |
|       lastCurrentTime = this._currentTickTime;
 | |
|       this._currentTickTime = current.endTime;
 | |
|       if (doTick) {
 | |
|         // Update any secondary schedulers like Jasmine mock Date.
 | |
|         doTick(this._currentTickTime - lastCurrentTime);
 | |
|       }
 | |
|       const retval = current.func.apply(global, current.args);
 | |
|       if (!retval) {
 | |
|         // Uncaught exception in the current scheduled function. Stop processing the queue.
 | |
|         break;
 | |
|       }
 | |
|     }
 | |
|     return this._currentTickTime - startTime;
 | |
|   }
 | |
| }
 | |
| 
 | |
| class FakeAsyncTestZoneSpec implements ZoneSpec {
 | |
|   static assertInZone(): void {
 | |
|     if (Zone.current.get('FakeAsyncTestZoneSpec') == null) {
 | |
|       throw new Error('The code should be running in the fakeAsync zone to call this function');
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   private _scheduler: Scheduler = new Scheduler();
 | |
|   private _microtasks: MicroTaskScheduledFunction[] = [];
 | |
|   private _lastError: Error|null = null;
 | |
|   private _uncaughtPromiseErrors: {rejection: any}[] =
 | |
|       (Promise as any)[(Zone as any).__symbol__('uncaughtPromiseErrors')];
 | |
| 
 | |
|   pendingPeriodicTimers: number[] = [];
 | |
|   pendingTimers: number[] = [];
 | |
| 
 | |
|   private patchDateLocked = false;
 | |
| 
 | |
|   constructor(
 | |
|       namePrefix: string, private trackPendingRequestAnimationFrame = false,
 | |
|       private macroTaskOptions?: MacroTaskOptions[]) {
 | |
|     this.name = 'fakeAsyncTestZone for ' + namePrefix;
 | |
|     // in case user can't access the construction of FakeAsyncTestSpec
 | |
|     // user can also define macroTaskOptions by define a global variable.
 | |
|     if (!this.macroTaskOptions) {
 | |
|       this.macroTaskOptions = global[Zone.__symbol__('FakeAsyncTestMacroTask')];
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   private _fnAndFlush(fn: Function, completers: {onSuccess?: Function, onError?: Function}):
 | |
|       Function {
 | |
|     return (...args: any[]): boolean => {
 | |
|       fn.apply(global, args);
 | |
| 
 | |
|       if (this._lastError === null) {  // Success
 | |
|         if (completers.onSuccess != null) {
 | |
|           completers.onSuccess.apply(global);
 | |
|         }
 | |
|         // Flush microtasks only on success.
 | |
|         this.flushMicrotasks();
 | |
|       } else {  // Failure
 | |
|         if (completers.onError != null) {
 | |
|           completers.onError.apply(global);
 | |
|         }
 | |
|       }
 | |
|       // Return true if there were no errors, false otherwise.
 | |
|       return this._lastError === null;
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   private static _removeTimer(timers: number[], id: number): void {
 | |
|     let index = timers.indexOf(id);
 | |
|     if (index > -1) {
 | |
|       timers.splice(index, 1);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   private _dequeueTimer(id: number): Function {
 | |
|     return () => {
 | |
|       FakeAsyncTestZoneSpec._removeTimer(this.pendingTimers, id);
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   private _requeuePeriodicTimer(fn: Function, interval: number, args: any[], id: number): Function {
 | |
|     return () => {
 | |
|       // Requeue the timer callback if it's not been canceled.
 | |
|       if (this.pendingPeriodicTimers.indexOf(id) !== -1) {
 | |
|         this._scheduler.scheduleFunction(
 | |
|             fn, interval, {args, isPeriodic: true, id, isRequeuePeriodic: true});
 | |
|       }
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   private _dequeuePeriodicTimer(id: number): Function {
 | |
|     return () => {
 | |
|       FakeAsyncTestZoneSpec._removeTimer(this.pendingPeriodicTimers, id);
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   private _setTimeout(fn: Function, delay: number, args: any[], isTimer = true): number {
 | |
|     let removeTimerFn = this._dequeueTimer(Scheduler.nextId);
 | |
|     // Queue the callback and dequeue the timer on success and error.
 | |
|     let cb = this._fnAndFlush(fn, {onSuccess: removeTimerFn, onError: removeTimerFn});
 | |
|     let id = this._scheduler.scheduleFunction(cb, delay, {args, isRequestAnimationFrame: !isTimer});
 | |
|     if (isTimer) {
 | |
|       this.pendingTimers.push(id);
 | |
|     }
 | |
|     return id;
 | |
|   }
 | |
| 
 | |
|   private _clearTimeout(id: number): void {
 | |
|     FakeAsyncTestZoneSpec._removeTimer(this.pendingTimers, id);
 | |
|     this._scheduler.removeScheduledFunctionWithId(id);
 | |
|   }
 | |
| 
 | |
|   private _setInterval(fn: Function, interval: number, args: any[]): number {
 | |
|     let id = Scheduler.nextId;
 | |
|     let completers = {onSuccess: null as any, onError: this._dequeuePeriodicTimer(id)};
 | |
|     let cb = this._fnAndFlush(fn, completers);
 | |
| 
 | |
|     // Use the callback created above to requeue on success.
 | |
|     completers.onSuccess = this._requeuePeriodicTimer(cb, interval, args, id);
 | |
| 
 | |
|     // Queue the callback and dequeue the periodic timer only on error.
 | |
|     this._scheduler.scheduleFunction(cb, interval, {args, isPeriodic: true});
 | |
|     this.pendingPeriodicTimers.push(id);
 | |
|     return id;
 | |
|   }
 | |
| 
 | |
|   private _clearInterval(id: number): void {
 | |
|     FakeAsyncTestZoneSpec._removeTimer(this.pendingPeriodicTimers, id);
 | |
|     this._scheduler.removeScheduledFunctionWithId(id);
 | |
|   }
 | |
| 
 | |
|   private _resetLastErrorAndThrow(): void {
 | |
|     let error = this._lastError || this._uncaughtPromiseErrors[0];
 | |
|     this._uncaughtPromiseErrors.length = 0;
 | |
|     this._lastError = null;
 | |
|     throw error;
 | |
|   }
 | |
| 
 | |
|   getCurrentTickTime() {
 | |
|     return this._scheduler.getCurrentTickTime();
 | |
|   }
 | |
| 
 | |
|   getFakeSystemTime() {
 | |
|     return this._scheduler.getFakeSystemTime();
 | |
|   }
 | |
| 
 | |
|   setFakeBaseSystemTime(realTime: number) {
 | |
|     this._scheduler.setFakeBaseSystemTime(realTime);
 | |
|   }
 | |
| 
 | |
|   getRealSystemTime() {
 | |
|     return this._scheduler.getRealSystemTime();
 | |
|   }
 | |
| 
 | |
|   static patchDate() {
 | |
|     if (!!global[Zone.__symbol__('disableDatePatching')]) {
 | |
|       // we don't want to patch global Date
 | |
|       // because in some case, global Date
 | |
|       // is already being patched, we need to provide
 | |
|       // an option to let user still use their
 | |
|       // own version of Date.
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (global['Date'] === FakeDate) {
 | |
|       // already patched
 | |
|       return;
 | |
|     }
 | |
|     global['Date'] = FakeDate;
 | |
|     FakeDate.prototype = OriginalDate.prototype;
 | |
| 
 | |
|     // try check and reset timers
 | |
|     // because jasmine.clock().install() may
 | |
|     // have replaced the global timer
 | |
|     FakeAsyncTestZoneSpec.checkTimerPatch();
 | |
|   }
 | |
| 
 | |
|   static resetDate() {
 | |
|     if (global['Date'] === FakeDate) {
 | |
|       global['Date'] = OriginalDate;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   static checkTimerPatch() {
 | |
|     if (global.setTimeout !== timers.setTimeout) {
 | |
|       global.setTimeout = timers.setTimeout;
 | |
|       global.clearTimeout = timers.clearTimeout;
 | |
|     }
 | |
|     if (global.setInterval !== timers.setInterval) {
 | |
|       global.setInterval = timers.setInterval;
 | |
|       global.clearInterval = timers.clearInterval;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   lockDatePatch() {
 | |
|     this.patchDateLocked = true;
 | |
|     FakeAsyncTestZoneSpec.patchDate();
 | |
|   }
 | |
|   unlockDatePatch() {
 | |
|     this.patchDateLocked = false;
 | |
|     FakeAsyncTestZoneSpec.resetDate();
 | |
|   }
 | |
| 
 | |
|   tickToNext(steps: number = 1, doTick?: (elapsed: number) => void, tickOptions: {
 | |
|     processNewMacroTasksSynchronously: boolean
 | |
|   } = {processNewMacroTasksSynchronously: true}): void {
 | |
|     if (steps <= 0) {
 | |
|       return;
 | |
|     }
 | |
|     FakeAsyncTestZoneSpec.assertInZone();
 | |
|     this.flushMicrotasks();
 | |
|     this._scheduler.tickToNext(steps, doTick, tickOptions);
 | |
|     if (this._lastError !== null) {
 | |
|       this._resetLastErrorAndThrow();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   tick(millis: number = 0, doTick?: (elapsed: number) => void, tickOptions: {
 | |
|     processNewMacroTasksSynchronously: boolean
 | |
|   } = {processNewMacroTasksSynchronously: true}): void {
 | |
|     FakeAsyncTestZoneSpec.assertInZone();
 | |
|     this.flushMicrotasks();
 | |
|     this._scheduler.tick(millis, doTick, tickOptions);
 | |
|     if (this._lastError !== null) {
 | |
|       this._resetLastErrorAndThrow();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   flushMicrotasks(): void {
 | |
|     FakeAsyncTestZoneSpec.assertInZone();
 | |
|     const flushErrors = () => {
 | |
|       if (this._lastError !== null || this._uncaughtPromiseErrors.length) {
 | |
|         // If there is an error stop processing the microtask queue and rethrow the error.
 | |
|         this._resetLastErrorAndThrow();
 | |
|       }
 | |
|     };
 | |
|     while (this._microtasks.length > 0) {
 | |
|       let microtask = this._microtasks.shift()!;
 | |
|       microtask.func.apply(microtask.target, microtask.args);
 | |
|     }
 | |
|     flushErrors();
 | |
|   }
 | |
| 
 | |
|   flush(limit?: number, flushPeriodic?: boolean, doTick?: (elapsed: number) => void): number {
 | |
|     FakeAsyncTestZoneSpec.assertInZone();
 | |
|     this.flushMicrotasks();
 | |
|     const elapsed = this._scheduler.flush(limit, flushPeriodic, doTick);
 | |
|     if (this._lastError !== null) {
 | |
|       this._resetLastErrorAndThrow();
 | |
|     }
 | |
|     return elapsed;
 | |
|   }
 | |
| 
 | |
|   flushOnlyPendingTimers(doTick?: (elapsed: number) => void): number {
 | |
|     FakeAsyncTestZoneSpec.assertInZone();
 | |
|     this.flushMicrotasks();
 | |
|     const elapsed = this._scheduler.flushOnlyPendingTimers(doTick);
 | |
|     if (this._lastError !== null) {
 | |
|       this._resetLastErrorAndThrow();
 | |
|     }
 | |
|     return elapsed;
 | |
|   }
 | |
| 
 | |
|   removeAllTimers() {
 | |
|     FakeAsyncTestZoneSpec.assertInZone();
 | |
|     this._scheduler.removeAll();
 | |
|     this.pendingPeriodicTimers = [];
 | |
|     this.pendingTimers = [];
 | |
|   }
 | |
| 
 | |
|   getTimerCount() {
 | |
|     return this._scheduler.getTimerCount() + this._microtasks.length;
 | |
|   }
 | |
| 
 | |
|   // ZoneSpec implementation below.
 | |
| 
 | |
|   name: string;
 | |
| 
 | |
|   properties: {[key: string]: any} = {'FakeAsyncTestZoneSpec': this};
 | |
| 
 | |
|   onScheduleTask(delegate: ZoneDelegate, current: Zone, target: Zone, task: Task): Task {
 | |
|     switch (task.type) {
 | |
|       case 'microTask':
 | |
|         let args = task.data && (task.data as any).args;
 | |
|         // should pass additional arguments to callback if have any
 | |
|         // currently we know process.nextTick will have such additional
 | |
|         // arguments
 | |
|         let additionalArgs: any[]|undefined;
 | |
|         if (args) {
 | |
|           let callbackIndex = (task.data as any).cbIdx;
 | |
|           if (typeof args.length === 'number' && args.length > callbackIndex + 1) {
 | |
|             additionalArgs = Array.prototype.slice.call(args, callbackIndex + 1);
 | |
|           }
 | |
|         }
 | |
|         this._microtasks.push({
 | |
|           func: task.invoke,
 | |
|           args: additionalArgs,
 | |
|           target: task.data && (task.data as any).target
 | |
|         });
 | |
|         break;
 | |
|       case 'macroTask':
 | |
|         switch (task.source) {
 | |
|           case 'setTimeout':
 | |
|             task.data!['handleId'] = this._setTimeout(
 | |
|                 task.invoke, task.data!['delay']!,
 | |
|                 Array.prototype.slice.call((task.data as any)['args'], 2));
 | |
|             break;
 | |
|           case 'setImmediate':
 | |
|             task.data!['handleId'] = this._setTimeout(
 | |
|                 task.invoke, 0, Array.prototype.slice.call((task.data as any)['args'], 1));
 | |
|             break;
 | |
|           case 'setInterval':
 | |
|             task.data!['handleId'] = this._setInterval(
 | |
|                 task.invoke, task.data!['delay']!,
 | |
|                 Array.prototype.slice.call((task.data as any)['args'], 2));
 | |
|             break;
 | |
|           case 'XMLHttpRequest.send':
 | |
|             throw new Error(
 | |
|                 'Cannot make XHRs from within a fake async test. Request URL: ' +
 | |
|                 (task.data as any)['url']);
 | |
|           case 'requestAnimationFrame':
 | |
|           case 'webkitRequestAnimationFrame':
 | |
|           case 'mozRequestAnimationFrame':
 | |
|             // Simulate a requestAnimationFrame by using a setTimeout with 16 ms.
 | |
|             // (60 frames per second)
 | |
|             task.data!['handleId'] = this._setTimeout(
 | |
|                 task.invoke, 16, (task.data as any)['args'],
 | |
|                 this.trackPendingRequestAnimationFrame);
 | |
|             break;
 | |
|           default:
 | |
|             // user can define which macroTask they want to support by passing
 | |
|             // macroTaskOptions
 | |
|             const macroTaskOption = this.findMacroTaskOption(task);
 | |
|             if (macroTaskOption) {
 | |
|               const args = task.data && (task.data as any)['args'];
 | |
|               const delay = args && args.length > 1 ? args[1] : 0;
 | |
|               let callbackArgs = macroTaskOption.callbackArgs ? macroTaskOption.callbackArgs : args;
 | |
|               if (!!macroTaskOption.isPeriodic) {
 | |
|                 // periodic macroTask, use setInterval to simulate
 | |
|                 task.data!['handleId'] = this._setInterval(task.invoke, delay, callbackArgs);
 | |
|                 task.data!.isPeriodic = true;
 | |
|               } else {
 | |
|                 // not periodic, use setTimeout to simulate
 | |
|                 task.data!['handleId'] = this._setTimeout(task.invoke, delay, callbackArgs);
 | |
|               }
 | |
|               break;
 | |
|             }
 | |
|             throw new Error('Unknown macroTask scheduled in fake async test: ' + task.source);
 | |
|         }
 | |
|         break;
 | |
|       case 'eventTask':
 | |
|         task = delegate.scheduleTask(target, task);
 | |
|         break;
 | |
|     }
 | |
|     return task;
 | |
|   }
 | |
| 
 | |
|   onCancelTask(delegate: ZoneDelegate, current: Zone, target: Zone, task: Task): any {
 | |
|     switch (task.source) {
 | |
|       case 'setTimeout':
 | |
|       case 'requestAnimationFrame':
 | |
|       case 'webkitRequestAnimationFrame':
 | |
|       case 'mozRequestAnimationFrame':
 | |
|         return this._clearTimeout(<number>task.data!['handleId']);
 | |
|       case 'setInterval':
 | |
|         return this._clearInterval(<number>task.data!['handleId']);
 | |
|       default:
 | |
|         // user can define which macroTask they want to support by passing
 | |
|         // macroTaskOptions
 | |
|         const macroTaskOption = this.findMacroTaskOption(task);
 | |
|         if (macroTaskOption) {
 | |
|           const handleId: number = <number>task.data!['handleId'];
 | |
|           return macroTaskOption.isPeriodic ? this._clearInterval(handleId) :
 | |
|                                               this._clearTimeout(handleId);
 | |
|         }
 | |
|         return delegate.cancelTask(target, task);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   onInvoke(
 | |
|       delegate: ZoneDelegate, current: Zone, target: Zone, callback: Function, applyThis: any,
 | |
|       applyArgs?: any[], source?: string): any {
 | |
|     try {
 | |
|       FakeAsyncTestZoneSpec.patchDate();
 | |
|       return delegate.invoke(target, callback, applyThis, applyArgs, source);
 | |
|     } finally {
 | |
|       if (!this.patchDateLocked) {
 | |
|         FakeAsyncTestZoneSpec.resetDate();
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   findMacroTaskOption(task: Task) {
 | |
|     if (!this.macroTaskOptions) {
 | |
|       return null;
 | |
|     }
 | |
|     for (let i = 0; i < this.macroTaskOptions.length; i++) {
 | |
|       const macroTaskOption = this.macroTaskOptions[i];
 | |
|       if (macroTaskOption.source === task.source) {
 | |
|         return macroTaskOption;
 | |
|       }
 | |
|     }
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   onHandleError(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, error: any):
 | |
|       boolean {
 | |
|     this._lastError = error;
 | |
|     return false;  // Don't propagate error to parent zone.
 | |
|   }
 | |
| }
 | |
| 
 | |
| // Export the class so that new instances can be created with proper
 | |
| // constructor params.
 | |
| (Zone as any)['FakeAsyncTestZoneSpec'] = FakeAsyncTestZoneSpec;
 | |
| })(typeof window === 'object' && window || typeof self === 'object' && self || global);
 | |
| 
 | |
| Zone.__load_patch('fakeasync', (global: any, Zone: ZoneType, api: _ZonePrivate) => {
 | |
|   const FakeAsyncTestZoneSpec = Zone && (Zone as any)['FakeAsyncTestZoneSpec'];
 | |
|   type ProxyZoneSpecType = {
 | |
|     setDelegate(delegateSpec: ZoneSpec): void; getDelegate(): ZoneSpec; resetDelegate(): void;
 | |
|   };
 | |
| 
 | |
|   function getProxyZoneSpec(): {get(): ProxyZoneSpecType; assertPresent: () => ProxyZoneSpecType} {
 | |
|     return Zone && (Zone as any)['ProxyZoneSpec'];
 | |
|   }
 | |
| 
 | |
|   let _fakeAsyncTestZoneSpec: any = null;
 | |
| 
 | |
|   /**
 | |
|    * Clears out the shared fake async zone for a test.
 | |
|    * To be called in a global `beforeEach`.
 | |
|    *
 | |
|    * @experimental
 | |
|    */
 | |
|   function resetFakeAsyncZone() {
 | |
|     if (_fakeAsyncTestZoneSpec) {
 | |
|       _fakeAsyncTestZoneSpec.unlockDatePatch();
 | |
|     }
 | |
|     _fakeAsyncTestZoneSpec = null;
 | |
|     // in node.js testing we may not have ProxyZoneSpec in which case there is nothing to reset.
 | |
|     getProxyZoneSpec() && getProxyZoneSpec().assertPresent().resetDelegate();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Wraps a function to be executed in the fakeAsync zone:
 | |
|    * - microtasks are manually executed by calling `flushMicrotasks()`,
 | |
|    * - timers are synchronous, `tick()` simulates the asynchronous passage of time.
 | |
|    *
 | |
|    * If there are any pending timers at the end of the function, an exception will be thrown.
 | |
|    *
 | |
|    * Can be used to wrap inject() calls.
 | |
|    *
 | |
|    * ## Example
 | |
|    *
 | |
|    * {@example core/testing/ts/fake_async.ts region='basic'}
 | |
|    *
 | |
|    * @param fn
 | |
|    * @returns The function wrapped to be executed in the fakeAsync zone
 | |
|    *
 | |
|    * @experimental
 | |
|    */
 | |
|   function fakeAsync(fn: Function): (...args: any[]) => any {
 | |
|     // Not using an arrow function to preserve context passed from call site
 | |
|     const fakeAsyncFn: any = function(this: unknown, ...args: any[]) {
 | |
|       const ProxyZoneSpec = getProxyZoneSpec();
 | |
|       if (!ProxyZoneSpec) {
 | |
|         throw new Error(
 | |
|             'ProxyZoneSpec is needed for the async() test helper but could not be found. ' +
 | |
|             'Please make sure that your environment includes zone.js/dist/proxy.js');
 | |
|       }
 | |
|       const proxyZoneSpec = ProxyZoneSpec.assertPresent();
 | |
|       if (Zone.current.get('FakeAsyncTestZoneSpec')) {
 | |
|         throw new Error('fakeAsync() calls can not be nested');
 | |
|       }
 | |
|       try {
 | |
|         // in case jasmine.clock init a fakeAsyncTestZoneSpec
 | |
|         if (!_fakeAsyncTestZoneSpec) {
 | |
|           if (proxyZoneSpec.getDelegate() instanceof FakeAsyncTestZoneSpec) {
 | |
|             throw new Error('fakeAsync() calls can not be nested');
 | |
|           }
 | |
| 
 | |
|           _fakeAsyncTestZoneSpec = new FakeAsyncTestZoneSpec();
 | |
|         }
 | |
| 
 | |
|         let res: any;
 | |
|         const lastProxyZoneSpec = proxyZoneSpec.getDelegate();
 | |
|         proxyZoneSpec.setDelegate(_fakeAsyncTestZoneSpec);
 | |
|         _fakeAsyncTestZoneSpec.lockDatePatch();
 | |
|         try {
 | |
|           res = fn.apply(this, args);
 | |
|           flushMicrotasks();
 | |
|         } finally {
 | |
|           proxyZoneSpec.setDelegate(lastProxyZoneSpec);
 | |
|         }
 | |
| 
 | |
|         if (_fakeAsyncTestZoneSpec.pendingPeriodicTimers.length > 0) {
 | |
|           throw new Error(
 | |
|               `${_fakeAsyncTestZoneSpec.pendingPeriodicTimers.length} ` +
 | |
|               `periodic timer(s) still in the queue.`);
 | |
|         }
 | |
| 
 | |
|         if (_fakeAsyncTestZoneSpec.pendingTimers.length > 0) {
 | |
|           throw new Error(
 | |
|               `${_fakeAsyncTestZoneSpec.pendingTimers.length} timer(s) still in the queue.`);
 | |
|         }
 | |
|         return res;
 | |
|       } finally {
 | |
|         resetFakeAsyncZone();
 | |
|       }
 | |
|     };
 | |
|     (fakeAsyncFn as any).isFakeAsync = true;
 | |
|     return fakeAsyncFn;
 | |
|   }
 | |
| 
 | |
|   function _getFakeAsyncZoneSpec(): any {
 | |
|     if (_fakeAsyncTestZoneSpec == null) {
 | |
|       _fakeAsyncTestZoneSpec = Zone.current.get('FakeAsyncTestZoneSpec');
 | |
|       if (_fakeAsyncTestZoneSpec == null) {
 | |
|         throw new Error('The code should be running in the fakeAsync zone to call this function');
 | |
|       }
 | |
|     }
 | |
|     return _fakeAsyncTestZoneSpec;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Simulates the asynchronous passage of time for the timers in the fakeAsync zone.
 | |
|    *
 | |
|    * The microtasks queue is drained at the very start of this function and after any timer callback
 | |
|    * has been executed.
 | |
|    *
 | |
|    * ## Example
 | |
|    *
 | |
|    * {@example core/testing/ts/fake_async.ts region='basic'}
 | |
|    *
 | |
|    * @experimental
 | |
|    */
 | |
|   function tick(millis: number = 0, ignoreNestedTimeout = false): void {
 | |
|     _getFakeAsyncZoneSpec().tick(millis, null, ignoreNestedTimeout);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Simulates the asynchronous passage of time for the timers in the fakeAsync zone by
 | |
|    * draining the macrotask queue until it is empty. The returned value is the milliseconds
 | |
|    * of time that would have been elapsed.
 | |
|    *
 | |
|    * @param maxTurns
 | |
|    * @returns The simulated time elapsed, in millis.
 | |
|    *
 | |
|    * @experimental
 | |
|    */
 | |
|   function flush(maxTurns?: number): number {
 | |
|     return _getFakeAsyncZoneSpec().flush(maxTurns);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Discard all remaining periodic tasks.
 | |
|    *
 | |
|    * @experimental
 | |
|    */
 | |
|   function discardPeriodicTasks(): void {
 | |
|     const zoneSpec = _getFakeAsyncZoneSpec();
 | |
|     const pendingTimers = zoneSpec.pendingPeriodicTimers;
 | |
|     zoneSpec.pendingPeriodicTimers.length = 0;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Flush any pending microtasks.
 | |
|    *
 | |
|    * @experimental
 | |
|    */
 | |
|   function flushMicrotasks(): void {
 | |
|     _getFakeAsyncZoneSpec().flushMicrotasks();
 | |
|   }
 | |
|   (Zone as any)[api.symbol('fakeAsyncTest')] =
 | |
|       {resetFakeAsyncZone, flushMicrotasks, discardPeriodicTasks, tick, flush, fakeAsync};
 | |
| }, true);
 |