diff --git a/aio/content/examples/testing/src/app/demo/async-helper.spec.ts b/aio/content/examples/testing/src/app/demo/async-helper.spec.ts
index ca0239408d..8f6d340ffb 100644
--- a/aio/content/examples/testing/src/app/demo/async-helper.spec.ts
+++ b/aio/content/examples/testing/src/app/demo/async-helper.spec.ts
@@ -25,21 +25,21 @@ describe('Angular async helper', () => {
async(() => { setTimeout(() => { actuallyDone = true; }, 0); }));
it('should run async test with task', async(() => {
- const id = setInterval(() => {
- actuallyDone = true;
- clearInterval(id);
- }, 100);
- }));
+ const id = setInterval(() => {
+ actuallyDone = true;
+ clearInterval(id);
+ }, 100);
+ }));
it('should run async test with successful promise', async(() => {
- const p = new Promise(resolve => { setTimeout(resolve, 10); });
- p.then(() => { actuallyDone = true; });
- }));
+ const p = new Promise(resolve => { setTimeout(resolve, 10); });
+ p.then(() => { actuallyDone = true; });
+ }));
it('should run async test with failed promise', async(() => {
- const p = new Promise((resolve, reject) => { setTimeout(reject, 10); });
- p.catch(() => { actuallyDone = true; });
- }));
+ const p = new Promise((resolve, reject) => { setTimeout(reject, 10); });
+ p.catch(() => { actuallyDone = true; });
+ }));
// Use done. Can also use async or fakeAsync.
it('should run async test with successful delayed Observable', (done: DoneFn) => {
@@ -48,56 +48,84 @@ describe('Angular async helper', () => {
});
it('should run async test with successful delayed Observable', async(() => {
- const source = of (true).pipe(delay(10));
- source.subscribe(val => actuallyDone = true, err => fail(err));
- }));
+ const source = of (true).pipe(delay(10));
+ source.subscribe(val => actuallyDone = true, err => fail(err));
+ }));
it('should run async test with successful delayed Observable', fakeAsync(() => {
- const source = of (true).pipe(delay(10));
- source.subscribe(val => actuallyDone = true, err => fail(err));
+ const source = of (true).pipe(delay(10));
+ source.subscribe(val => actuallyDone = true, err => fail(err));
- tick(10);
- }));
+ tick(10);
+ }));
});
describe('fakeAsync', () => {
// #docregion fake-async-test-tick
it('should run timeout callback with delay after call tick with millis', fakeAsync(() => {
- let called = false;
- setTimeout(() => { called = true; }, 100);
- tick(100);
- expect(called).toBe(true);
- }));
+ let called = false;
+ setTimeout(() => { called = true; }, 100);
+ tick(100);
+ expect(called).toBe(true);
+ }));
// #enddocregion fake-async-test-tick
+ // #docregion fake-async-test-tick-new-macro-task-sync
+ it('should run new macro task callback with delay after call tick with millis',
+ fakeAsync(() => {
+ function nestedTimer(cb: () => any): void { setTimeout(() => setTimeout(() => cb())); }
+ const callback = jasmine.createSpy('callback');
+ nestedTimer(callback);
+ expect(callback).not.toHaveBeenCalled();
+ tick(0);
+ // the nested timeout will also be triggered
+ expect(callback).toHaveBeenCalled();
+ }));
+ // #enddocregion fake-async-test-tick-new-macro-task-sync
+
+ // #docregion fake-async-test-tick-new-macro-task-async
+ it('should not run new macro task callback with delay after call tick with millis',
+ fakeAsync(() => {
+ function nestedTimer(cb: () => any): void { setTimeout(() => setTimeout(() => cb())); }
+ const callback = jasmine.createSpy('callback');
+ nestedTimer(callback);
+ expect(callback).not.toHaveBeenCalled();
+ tick(0, {processNewMacroTasksSynchronously: false});
+ // the nested timeout will not be triggered
+ expect(callback).not.toHaveBeenCalled();
+ tick(0);
+ expect(callback).toHaveBeenCalled();
+ }));
+ // #enddocregion fake-async-test-tick-new-macro-task-async
+
// #docregion fake-async-test-date
it('should get Date diff correctly in fakeAsync', fakeAsync(() => {
- const start = Date.now();
- tick(100);
- const end = Date.now();
- expect(end - start).toBe(100);
- }));
+ const start = Date.now();
+ tick(100);
+ const end = Date.now();
+ expect(end - start).toBe(100);
+ }));
// #enddocregion fake-async-test-date
// #docregion fake-async-test-rxjs
it('should get Date diff correctly in fakeAsync with rxjs scheduler', fakeAsync(() => {
- // need to add `import 'zone.js/dist/zone-patch-rxjs-fake-async'
- // to patch rxjs scheduler
- let result = null;
- of ('hello').pipe(delay(1000)).subscribe(v => { result = v; });
- expect(result).toBeNull();
- tick(1000);
- expect(result).toBe('hello');
+ // need to add `import 'zone.js/dist/zone-patch-rxjs-fake-async'
+ // to patch rxjs scheduler
+ let result = null;
+ of ('hello').pipe(delay(1000)).subscribe(v => { result = v; });
+ expect(result).toBeNull();
+ tick(1000);
+ expect(result).toBe('hello');
- const start = new Date().getTime();
- let dateDiff = 0;
- interval(1000).pipe(take(2)).subscribe(() => dateDiff = (new Date().getTime() - start));
+ const start = new Date().getTime();
+ let dateDiff = 0;
+ interval(1000).pipe(take(2)).subscribe(() => dateDiff = (new Date().getTime() - start));
- tick(1000);
- expect(dateDiff).toBe(1000);
- tick(1000);
- expect(dateDiff).toBe(2000);
- }));
+ tick(1000);
+ expect(dateDiff).toBe(1000);
+ tick(1000);
+ expect(dateDiff).toBe(2000);
+ }));
// #enddocregion fake-async-test-rxjs
});
diff --git a/aio/content/guide/testing.md b/aio/content/guide/testing.md
index 3aacef659e..0eeb798aed 100644
--- a/aio/content/guide/testing.md
+++ b/aio/content/guide/testing.md
@@ -1266,7 +1266,8 @@ You do have to call [tick()](api/core/testing/tick) to advance the (virtual) clo
Calling [tick()](api/core/testing/tick) simulates the passage of time until all pending asynchronous activities finish.
In this case, it waits for the error handler's `setTimeout()`.
-The [tick()](api/core/testing/tick) function accepts milliseconds as a parameter (defaults to 0 if not provided). The parameter represents how much the virtual clock advances. For example, if you have a `setTimeout(fn, 100)` in a `fakeAsync()` test, you need to use tick(100) to trigger the fn callback.
+The [tick()](api/core/testing/tick) function accepts milliseconds and tickOptions as parameters, the millisecond (defaults to 0 if not provided) parameter represents how much the virtual clock advances. For example, if you have a `setTimeout(fn, 100)` in a `fakeAsync()` test, you need to use tick(100) to trigger the fn callback. The tickOptions is an optional parameter with a property called processNewMacroTasksSynchronously (defaults is true) represents whether to invoke
+new generated macro tasks when ticking.
+
+
+In this example, we have a new macro task (nested setTimeout), by default, when we `tick`, the setTimeout `outside` and `nested` will both be triggered.
+
+
+
+
+And in some case, we don't want to trigger the new maco task when ticking, we can use `tick(milliseconds, {processNewMacroTasksSynchronously: false})` to not invoke new maco task.
+
#### Comparing dates inside fakeAsync()
`fakeAsync()` simulates passage of time, which allows you to calculate the difference between dates inside `fakeAsync()`.
diff --git a/packages/core/test/fake_async_spec.ts b/packages/core/test/fake_async_spec.ts
index 036cabef96..ddf44dfa29 100644
--- a/packages/core/test/fake_async_spec.ts
+++ b/packages/core/test/fake_async_spec.ts
@@ -127,6 +127,31 @@ const ProxyZoneSpec: {assertPresent: () => void} = (Zone as any)['ProxyZoneSpec'
expect(ran).toEqual(true);
}));
+ it('should run new macro tasks created by timer callback', fakeAsync(() => {
+ function nestedTimer(callback: () => any): void {
+ setTimeout(() => setTimeout(() => callback()));
+ }
+ const callback = jasmine.createSpy('callback');
+ nestedTimer(callback);
+ expect(callback).not.toHaveBeenCalled();
+ tick(0);
+ expect(callback).toHaveBeenCalled();
+ }));
+
+ it('should not queue nested timer on tick with processNewMacroTasksSynchronously=false',
+ fakeAsync(() => {
+ function nestedTimer(callback: () => any): void {
+ setTimeout(() => setTimeout(() => callback()));
+ }
+ const callback = jasmine.createSpy('callback');
+ nestedTimer(callback);
+ expect(callback).not.toHaveBeenCalled();
+ tick(0, {processNewMacroTasksSynchronously: false});
+ expect(callback).not.toHaveBeenCalled();
+ flush();
+ expect(callback).toHaveBeenCalled();
+ }));
+
it('should run queued timer only once', fakeAsync(() => {
let cycles = 0;
setTimeout(() => { cycles++; }, 10);
diff --git a/packages/core/testing/src/fake_async.ts b/packages/core/testing/src/fake_async.ts
index 1f84eabdc0..f515987b59 100644
--- a/packages/core/testing/src/fake_async.ts
+++ b/packages/core/testing/src/fake_async.ts
@@ -62,13 +62,54 @@ export function fakeAsync(fn: Function): (...args: any[]) => any {
*
* {@example core/testing/ts/fake_async.ts region='basic'}
*
+ * @param millis, the number of millisecond to advance the virtual timer
+ * @param tickOptions, the options of tick with a flag called
+ * processNewMacroTasksSynchronously, whether to invoke the new macroTasks, by default is
+ * false, means the new macroTasks will be invoked
+ *
+ * For example,
+ *
+ * it ('test with nested setTimeout', fakeAsync(() => {
+ * let nestedTimeoutInvoked = false;
+ * function funcWithNestedTimeout() {
+ * setTimeout(() => {
+ * nestedTimeoutInvoked = true;
+ * });
+ * };
+ * setTimeout(funcWithNestedTimeout);
+ * tick();
+ * expect(nestedTimeoutInvoked).toBe(true);
+ * }));
+ *
+ * in this case, we have a nested timeout (new macroTask), when we tick, both the
+ * funcWithNestedTimeout and the nested timeout both will be invoked.
+ *
+ * it ('test with nested setTimeout', fakeAsync(() => {
+ * let nestedTimeoutInvoked = false;
+ * function funcWithNestedTimeout() {
+ * setTimeout(() => {
+ * nestedTimeoutInvoked = true;
+ * });
+ * };
+ * setTimeout(funcWithNestedTimeout);
+ * tick(0, {processNewMacroTasksSynchronously: false});
+ * expect(nestedTimeoutInvoked).toBe(false);
+ * }));
+ *
+ * if we pass the tickOptions with processNewMacroTasksSynchronously to be false, the nested timeout
+ * will not be invoked.
+ *
+ *
* @publicApi
*/
-export function tick(millis: number = 0): void {
+export function tick(
+ millis: number = 0, tickOptions: {processNewMacroTasksSynchronously: boolean} = {
+ processNewMacroTasksSynchronously: true
+ }): void {
if (fakeAsyncTestModule) {
- return fakeAsyncTestModule.tick(millis);
+ return fakeAsyncTestModule.tick(millis, tickOptions);
} else {
- return tickFallback(millis);
+ return tickFallback(millis, tickOptions);
}
}
diff --git a/packages/core/testing/src/fake_async_fallback.ts b/packages/core/testing/src/fake_async_fallback.ts
index dd08df64ef..0b15a9533e 100644
--- a/packages/core/testing/src/fake_async_fallback.ts
+++ b/packages/core/testing/src/fake_async_fallback.ts
@@ -118,8 +118,11 @@ function _getFakeAsyncZoneSpec(): any {
*
* @publicApi
*/
-export function tickFallback(millis: number = 0): void {
- _getFakeAsyncZoneSpec().tick(millis);
+export function tickFallback(
+ millis: number = 0, tickOptions: {processNewMacroTasksSynchronously: boolean} = {
+ processNewMacroTasksSynchronously: true
+ }): void {
+ _getFakeAsyncZoneSpec().tick(millis, null, tickOptions);
}
/**
diff --git a/packages/zone.js/lib/testing/fake-async.ts b/packages/zone.js/lib/testing/fake-async.ts
index 1c0907a232..d688705cd5 100644
--- a/packages/zone.js/lib/testing/fake-async.ts
+++ b/packages/zone.js/lib/testing/fake-async.ts
@@ -117,7 +117,9 @@ Zone.__load_patch('fakeasync', (global: any, Zone: ZoneType, api: _ZonePrivate)
*
* @experimental
*/
- function tick(millis: number = 0): void { _getFakeAsyncZoneSpec().tick(millis); }
+ 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
diff --git a/packages/zone.js/lib/zone-spec/fake-async-test.ts b/packages/zone.js/lib/zone-spec/fake-async-test.ts
index 9a55158f32..625b9a7763 100644
--- a/packages/zone.js/lib/zone-spec/fake-async-test.ts
+++ b/packages/zone.js/lib/zone-spec/fake-async-test.ts
@@ -72,6 +72,8 @@
private _currentTime: number = 0;
// Current real time in millis.
private _currentRealTime: number = OriginalDate.now();
+ // track requeuePeriodicTimer
+ private _currentTickRequeuePeriodicEntries: any[] = [];
constructor() {}
@@ -81,10 +83,24 @@
setCurrentRealTime(realTime: number) { this._currentRealTime = realTime; }
- scheduleFunction(
- cb: Function, delay: number, args: any[] = [], isPeriodic: boolean = false,
- isRequestAnimationFrame: boolean = false, id: number = -1): number {
- let currentId: number = id < 0 ? Scheduler.nextId++ : id;
+ 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._currentTime + delay;
// Insert so that scheduler queue remains sorted by end time.
@@ -92,11 +108,14 @@
endTime: endTime,
id: currentId,
func: cb,
- args: args,
+ args: options.args !,
delay: delay,
- isPeriodic: isPeriodic,
- isRequestAnimationFrame: isRequestAnimationFrame
+ 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];
@@ -117,21 +136,37 @@
}
}
- tick(millis: number = 0, doTick?: (elapsed: number) => void): void {
+ tick(millis: number = 0, doTick?: (elapsed: number) => void, tickOptions: {
+ processNewMacroTasksSynchronously: boolean
+ } = {processNewMacroTasksSynchronously: true}): void {
let finalTime = this._currentTime + millis;
let lastCurrentTime = 0;
- if (this._schedulerQueue.length === 0 && doTick) {
+ // 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 (this._schedulerQueue.length > 0) {
- let current = this._schedulerQueue[0];
+ 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 = this._schedulerQueue.shift() !;
+ let current = schedulerQueue.shift() !;
+ if (!tickOptions.processNewMacroTasksSynchronously) {
+ const idx = this._schedulerQueue.indexOf(current);
+ if (idx >= 0) {
+ this._schedulerQueue.splice(idx, 1);
+ }
+ }
lastCurrentTime = this._currentTime;
this._currentTime = current.endTime;
if (doTick) {
@@ -143,6 +178,21 @@
// 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._currentTime;
@@ -274,7 +324,8 @@
return () => {
// Requeue the timer callback if it's not been canceled.
if (this.pendingPeriodicTimers.indexOf(id) !== -1) {
- this._scheduler.scheduleFunction(fn, interval, args, true, false, id);
+ this._scheduler.scheduleFunction(
+ fn, interval, {args, isPeriodic: true, id, isRequeuePeriodic: true});
}
};
}
@@ -287,7 +338,8 @@
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, false, !isTimer);
+ let id =
+ this._scheduler.scheduleFunction(cb, delay, {args, isRequestAnimationFrame: !isTimer});
if (isTimer) {
this.pendingTimers.push(id);
}
@@ -308,7 +360,7 @@
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, true);
+ this._scheduler.scheduleFunction(cb, interval, {args, isPeriodic: true});
this.pendingPeriodicTimers.push(id);
return id;
}
@@ -380,10 +432,12 @@
FakeAsyncTestZoneSpec.resetDate();
}
- tick(millis: number = 0, doTick?: (elapsed: number) => void): void {
+ tick(millis: number = 0, doTick?: (elapsed: number) => void, tickOptions: {
+ processNewMacroTasksSynchronously: boolean
+ } = {processNewMacroTasksSynchronously: true}): void {
FakeAsyncTestZoneSpec.assertInZone();
this.flushMicrotasks();
- this._scheduler.tick(millis, doTick);
+ this._scheduler.tick(millis, doTick, tickOptions);
if (this._lastError !== null) {
this._resetLastErrorAndThrow();
}
diff --git a/packages/zone.js/test/zone-spec/fake-async-test.spec.ts b/packages/zone.js/test/zone-spec/fake-async-test.spec.ts
index f538f6af88..6c7b82ed68 100644
--- a/packages/zone.js/test/zone-spec/fake-async-test.spec.ts
+++ b/packages/zone.js/test/zone-spec/fake-async-test.spec.ts
@@ -145,6 +145,22 @@ describe('FakeAsyncTestZoneSpec', () => {
});
}));
+ it('should not queue new macro task on tick with processNewMacroTasksSynchronously=false',
+ () => {
+ function nestedTimer(callback: () => any): void {
+ setTimeout(() => setTimeout(() => callback()));
+ }
+ fakeAsyncTestZone.run(() => {
+ const callback = jasmine.createSpy('callback');
+ nestedTimer(callback);
+ expect(callback).not.toHaveBeenCalled();
+ testZoneSpec.tick(0, null, {processNewMacroTasksSynchronously: false});
+ expect(callback).not.toHaveBeenCalled();
+ testZoneSpec.flush();
+ expect(callback).toHaveBeenCalled();
+ });
+ });
+
it('should run queued timer after sufficient clock ticks', () => {
fakeAsyncTestZone.run(() => {
let ran = false;
diff --git a/tools/public_api_guard/core/testing.d.ts b/tools/public_api_guard/core/testing.d.ts
index 111156ec83..e443062de5 100644
--- a/tools/public_api_guard/core/testing.d.ts
+++ b/tools/public_api_guard/core/testing.d.ts
@@ -134,7 +134,9 @@ export declare type TestModuleMetadata = {
aotSummaries?: () => any[];
};
-export declare function tick(millis?: number): void;
+export declare function tick(millis?: number, tickOptions?: {
+ processNewMacroTasksSynchronously: boolean;
+}): void;
export declare function withModule(moduleDef: TestModuleMetadata): InjectSetupWrapper;
export declare function withModule(moduleDef: TestModuleMetadata, fn: Function): () => any;