feat: add an tickOptions parameter with property processNewMacroTasksSynchronously. (#33838)

This option will control whether to invoke the new macro tasks when ticking.

Close #33799

PR Close #33838
This commit is contained in:
JiaLiPassion
2019-12-03 00:56:56 +09:00
committed by Miško Hevery
parent 2562a3b1b0
commit 17b862cf82
9 changed files with 256 additions and 68 deletions

View File

@ -25,21 +25,21 @@ describe('Angular async helper', () => {
async(() => { setTimeout(() => { actuallyDone = true; }, 0); })); async(() => { setTimeout(() => { actuallyDone = true; }, 0); }));
it('should run async test with task', async(() => { it('should run async test with task', async(() => {
const id = setInterval(() => { const id = setInterval(() => {
actuallyDone = true; actuallyDone = true;
clearInterval(id); clearInterval(id);
}, 100); }, 100);
})); }));
it('should run async test with successful promise', async(() => { it('should run async test with successful promise', async(() => {
const p = new Promise(resolve => { setTimeout(resolve, 10); }); const p = new Promise(resolve => { setTimeout(resolve, 10); });
p.then(() => { actuallyDone = true; }); p.then(() => { actuallyDone = true; });
})); }));
it('should run async test with failed promise', async(() => { it('should run async test with failed promise', async(() => {
const p = new Promise((resolve, reject) => { setTimeout(reject, 10); }); const p = new Promise((resolve, reject) => { setTimeout(reject, 10); });
p.catch(() => { actuallyDone = true; }); p.catch(() => { actuallyDone = true; });
})); }));
// Use done. Can also use async or fakeAsync. // Use done. Can also use async or fakeAsync.
it('should run async test with successful delayed Observable', (done: DoneFn) => { 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(() => { it('should run async test with successful delayed Observable', async(() => {
const source = of (true).pipe(delay(10)); const source = of (true).pipe(delay(10));
source.subscribe(val => actuallyDone = true, err => fail(err)); source.subscribe(val => actuallyDone = true, err => fail(err));
})); }));
it('should run async test with successful delayed Observable', fakeAsync(() => { it('should run async test with successful delayed Observable', fakeAsync(() => {
const source = of (true).pipe(delay(10)); const source = of (true).pipe(delay(10));
source.subscribe(val => actuallyDone = true, err => fail(err)); source.subscribe(val => actuallyDone = true, err => fail(err));
tick(10); tick(10);
})); }));
}); });
describe('fakeAsync', () => { describe('fakeAsync', () => {
// #docregion fake-async-test-tick // #docregion fake-async-test-tick
it('should run timeout callback with delay after call tick with millis', fakeAsync(() => { it('should run timeout callback with delay after call tick with millis', fakeAsync(() => {
let called = false; let called = false;
setTimeout(() => { called = true; }, 100); setTimeout(() => { called = true; }, 100);
tick(100); tick(100);
expect(called).toBe(true); expect(called).toBe(true);
})); }));
// #enddocregion fake-async-test-tick // #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 // #docregion fake-async-test-date
it('should get Date diff correctly in fakeAsync', fakeAsync(() => { it('should get Date diff correctly in fakeAsync', fakeAsync(() => {
const start = Date.now(); const start = Date.now();
tick(100); tick(100);
const end = Date.now(); const end = Date.now();
expect(end - start).toBe(100); expect(end - start).toBe(100);
})); }));
// #enddocregion fake-async-test-date // #enddocregion fake-async-test-date
// #docregion fake-async-test-rxjs // #docregion fake-async-test-rxjs
it('should get Date diff correctly in fakeAsync with rxjs scheduler', fakeAsync(() => { it('should get Date diff correctly in fakeAsync with rxjs scheduler', fakeAsync(() => {
// need to add `import 'zone.js/dist/zone-patch-rxjs-fake-async' // need to add `import 'zone.js/dist/zone-patch-rxjs-fake-async'
// to patch rxjs scheduler // to patch rxjs scheduler
let result = null; let result = null;
of ('hello').pipe(delay(1000)).subscribe(v => { result = v; }); of ('hello').pipe(delay(1000)).subscribe(v => { result = v; });
expect(result).toBeNull(); expect(result).toBeNull();
tick(1000); tick(1000);
expect(result).toBe('hello'); expect(result).toBe('hello');
const start = new Date().getTime(); const start = new Date().getTime();
let dateDiff = 0; let dateDiff = 0;
interval(1000).pipe(take(2)).subscribe(() => dateDiff = (new Date().getTime() - start)); interval(1000).pipe(take(2)).subscribe(() => dateDiff = (new Date().getTime() - start));
tick(1000); tick(1000);
expect(dateDiff).toBe(1000); expect(dateDiff).toBe(1000);
tick(1000); tick(1000);
expect(dateDiff).toBe(2000); expect(dateDiff).toBe(2000);
})); }));
// #enddocregion fake-async-test-rxjs // #enddocregion fake-async-test-rxjs
}); });

View File

@ -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. 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()`. 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.
<code-example <code-example
path="testing/src/app/demo/async-helper.spec.ts" path="testing/src/app/demo/async-helper.spec.ts"
@ -1276,6 +1277,22 @@ The [tick()](api/core/testing/tick) function accepts milliseconds as a parameter
The [tick()](api/core/testing/tick) function is one of the Angular testing utilities that you import with `TestBed`. The [tick()](api/core/testing/tick) function is one of the Angular testing utilities that you import with `TestBed`.
It's a companion to `fakeAsync()` and you can only call it within a `fakeAsync()` body. It's a companion to `fakeAsync()` and you can only call it within a `fakeAsync()` body.
#### tickOptions
<code-example
path="testing/src/app/demo/async-helper.spec.ts"
region="fake-async-test-tick-new-macro-task-sync">
</code-example>
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.
<code-example
path="testing/src/app/demo/async-helper.spec.ts"
region="fake-async-test-tick-new-macro-task-async">
</code-example>
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() #### Comparing dates inside fakeAsync()
`fakeAsync()` simulates passage of time, which allows you to calculate the difference between dates inside `fakeAsync()`. `fakeAsync()` simulates passage of time, which allows you to calculate the difference between dates inside `fakeAsync()`.

View File

@ -127,6 +127,31 @@ const ProxyZoneSpec: {assertPresent: () => void} = (Zone as any)['ProxyZoneSpec'
expect(ran).toEqual(true); 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(() => { it('should run queued timer only once', fakeAsync(() => {
let cycles = 0; let cycles = 0;
setTimeout(() => { cycles++; }, 10); setTimeout(() => { cycles++; }, 10);

View File

@ -62,13 +62,54 @@ export function fakeAsync(fn: Function): (...args: any[]) => any {
* *
* {@example core/testing/ts/fake_async.ts region='basic'} * {@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 * @publicApi
*/ */
export function tick(millis: number = 0): void { export function tick(
millis: number = 0, tickOptions: {processNewMacroTasksSynchronously: boolean} = {
processNewMacroTasksSynchronously: true
}): void {
if (fakeAsyncTestModule) { if (fakeAsyncTestModule) {
return fakeAsyncTestModule.tick(millis); return fakeAsyncTestModule.tick(millis, tickOptions);
} else { } else {
return tickFallback(millis); return tickFallback(millis, tickOptions);
} }
} }

View File

@ -118,8 +118,11 @@ function _getFakeAsyncZoneSpec(): any {
* *
* @publicApi * @publicApi
*/ */
export function tickFallback(millis: number = 0): void { export function tickFallback(
_getFakeAsyncZoneSpec().tick(millis); millis: number = 0, tickOptions: {processNewMacroTasksSynchronously: boolean} = {
processNewMacroTasksSynchronously: true
}): void {
_getFakeAsyncZoneSpec().tick(millis, null, tickOptions);
} }
/** /**

View File

@ -117,7 +117,9 @@ Zone.__load_patch('fakeasync', (global: any, Zone: ZoneType, api: _ZonePrivate)
* *
* @experimental * @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 * Simulates the asynchronous passage of time for the timers in the fakeAsync zone by

View File

@ -72,6 +72,8 @@
private _currentTime: number = 0; private _currentTime: number = 0;
// Current real time in millis. // Current real time in millis.
private _currentRealTime: number = OriginalDate.now(); private _currentRealTime: number = OriginalDate.now();
// track requeuePeriodicTimer
private _currentTickRequeuePeriodicEntries: any[] = [];
constructor() {} constructor() {}
@ -81,10 +83,24 @@
setCurrentRealTime(realTime: number) { this._currentRealTime = realTime; } setCurrentRealTime(realTime: number) { this._currentRealTime = realTime; }
scheduleFunction( scheduleFunction(cb: Function, delay: number, options?: {
cb: Function, delay: number, args: any[] = [], isPeriodic: boolean = false, args?: any[],
isRequestAnimationFrame: boolean = false, id: number = -1): number { isPeriodic?: boolean,
let currentId: number = id < 0 ? Scheduler.nextId++ : id; 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; let endTime = this._currentTime + delay;
// Insert so that scheduler queue remains sorted by end time. // Insert so that scheduler queue remains sorted by end time.
@ -92,11 +108,14 @@
endTime: endTime, endTime: endTime,
id: currentId, id: currentId,
func: cb, func: cb,
args: args, args: options.args !,
delay: delay, delay: delay,
isPeriodic: isPeriodic, isPeriodic: options.isPeriodic !,
isRequestAnimationFrame: isRequestAnimationFrame isRequestAnimationFrame: options.isRequestAnimationFrame !
}; };
if (options.isRequeuePeriodic !) {
this._currentTickRequeuePeriodicEntries.push(newEntry);
}
let i = 0; let i = 0;
for (; i < this._schedulerQueue.length; i++) { for (; i < this._schedulerQueue.length; i++) {
let currentEntry = this._schedulerQueue[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 finalTime = this._currentTime + millis;
let lastCurrentTime = 0; 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); doTick(millis);
return; return;
} }
while (this._schedulerQueue.length > 0) { while (schedulerQueue.length > 0) {
let current = this._schedulerQueue[0]; // clear requeueEntries before each loop
this._currentTickRequeuePeriodicEntries = [];
let current = schedulerQueue[0];
if (finalTime < current.endTime) { if (finalTime < current.endTime) {
// Done processing the queue since it's sorted by endTime. // Done processing the queue since it's sorted by endTime.
break; break;
} else { } else {
// Time to run scheduled function. Remove it from the head of queue. // 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; lastCurrentTime = this._currentTime;
this._currentTime = current.endTime; this._currentTime = current.endTime;
if (doTick) { if (doTick) {
@ -143,6 +178,21 @@
// Uncaught exception in the current scheduled function. Stop processing the queue. // Uncaught exception in the current scheduled function. Stop processing the queue.
break; 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; lastCurrentTime = this._currentTime;
@ -274,7 +324,8 @@
return () => { return () => {
// Requeue the timer callback if it's not been canceled. // Requeue the timer callback if it's not been canceled.
if (this.pendingPeriodicTimers.indexOf(id) !== -1) { 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); let removeTimerFn = this._dequeueTimer(Scheduler.nextId);
// Queue the callback and dequeue the timer on success and error. // Queue the callback and dequeue the timer on success and error.
let cb = this._fnAndFlush(fn, {onSuccess: removeTimerFn, onError: removeTimerFn}); 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) { if (isTimer) {
this.pendingTimers.push(id); this.pendingTimers.push(id);
} }
@ -308,7 +360,7 @@
completers.onSuccess = this._requeuePeriodicTimer(cb, interval, args, id); completers.onSuccess = this._requeuePeriodicTimer(cb, interval, args, id);
// Queue the callback and dequeue the periodic timer only on error. // 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); this.pendingPeriodicTimers.push(id);
return id; return id;
} }
@ -380,10 +432,12 @@
FakeAsyncTestZoneSpec.resetDate(); 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(); FakeAsyncTestZoneSpec.assertInZone();
this.flushMicrotasks(); this.flushMicrotasks();
this._scheduler.tick(millis, doTick); this._scheduler.tick(millis, doTick, tickOptions);
if (this._lastError !== null) { if (this._lastError !== null) {
this._resetLastErrorAndThrow(); this._resetLastErrorAndThrow();
} }

View File

@ -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', () => { it('should run queued timer after sufficient clock ticks', () => {
fakeAsyncTestZone.run(() => { fakeAsyncTestZone.run(() => {
let ran = false; let ran = false;

View File

@ -134,7 +134,9 @@ export declare type TestModuleMetadata = {
aotSummaries?: () => any[]; 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): InjectSetupWrapper;
export declare function withModule(moduleDef: TestModuleMetadata, fn: Function): () => any; export declare function withModule(moduleDef: TestModuleMetadata, fn: Function): () => any;