Skip to content

Commit dea7234

Browse files
JiaLiPassionalxhub
authored andcommitted
fix(zone.js): async-test should only call done once (#45025)
`AsyncTestZoneSpec` triggers jasmine `done()` function multiple times and causes warning ``` An asynchronous function called its 'done' callback more than once. This is a bug in the spec, beforeAll, beforeEach, afterAll, or afterEach function in question. This will be treated as an error in a future version. See<https://jasmine.github.io/tutorials/upgrading_to_Jasmine_4.0#deprecations-due-to-calling-done-multiple-times> for more information ``` The reproduce case will be running some `Zone.run()` inside `waitForAsync()`. ``` it('multiple done', waitForAsync(() => { Zone.current.run(() => {}); Zone.current.run(() => {}); })); ``` The reason the `done()` is called in the `onInvoke()` hook is to handle the case that the testBody is totally sync, but we should only do this check for the entry function not for all `Zone.run()` scenario. Another issue is if we run nested zone inside `waitForAsync()`, the `onHasTask()` hook will be triggered multiple times, and cause `done()` be triggered multiple times, so we need to only trigger the `done()` when the zone is `AsyncTestZone`. PR Close #45025
1 parent c88e87d commit dea7234

File tree

2 files changed

+79
-2
lines changed

2 files changed

+79
-2
lines changed

packages/zone.js/lib/zone-spec/async-test.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ class AsyncTestZoneSpec implements ZoneSpec {
1313
_pendingMacroTasks: boolean = false;
1414
_alreadyErrored: boolean = false;
1515
_isSync: boolean = false;
16+
entryFunction: Function|null = null;
1617
runZone = Zone.current;
1718
unresolvedChainedPromiseCount = 0;
1819

@@ -108,13 +109,25 @@ class AsyncTestZoneSpec implements ZoneSpec {
108109
onInvoke(
109110
parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, delegate: Function,
110111
applyThis: any, applyArgs?: any[], source?: string): any {
111-
let previousTaskCounts: any = null;
112+
if (!this.entryFunction) {
113+
this.entryFunction = delegate;
114+
}
112115
try {
113116
this._isSync = true;
114117
return parentZoneDelegate.invoke(targetZone, delegate, applyThis, applyArgs, source);
115118
} finally {
116119
const afterTaskCounts: any = (parentZoneDelegate as any)._taskCounts;
117-
if (this._isSync) {
120+
// We need to check the delegate is the same as entryFunction or not.
121+
// Consider the following case.
122+
//
123+
// asyncTestZone.run(() => { // Here the delegate will be the entryFunction
124+
// Zone.current.run(() => { // Here the delegate will not be the entryFunction
125+
// });
126+
// });
127+
//
128+
// We only want to check whether there are async tasks scheduled
129+
// for the entry function.
130+
if (this._isSync && this.entryFunction === delegate) {
118131
this._finishCallbackIfDone();
119132
}
120133
}
@@ -133,6 +146,21 @@ class AsyncTestZoneSpec implements ZoneSpec {
133146

134147
onHasTask(delegate: ZoneDelegate, current: Zone, target: Zone, hasTaskState: HasTaskState) {
135148
delegate.hasTask(target, hasTaskState);
149+
// We should only trigger finishCallback when the target zone is the AsyncTestZone
150+
// Consider the following cases.
151+
//
152+
// const childZone = asyncTestZone.fork({
153+
// name: 'child',
154+
// onHasTask: ...
155+
// });
156+
//
157+
// So we have nested zones declared the onHasTask hook, in this case,
158+
// the onHasTask will be triggered twice, and cause the finishCallbackIfDone()
159+
// is also be invoked twice. So we need to only trigger the finishCallbackIfDone()
160+
// when the current zone is the same as the target zone.
161+
if (current !== target) {
162+
return;
163+
}
136164
if (hasTaskState.change == 'microTask') {
137165
this._pendingMicroTasks = hasTaskState.microTask;
138166
this._finishCallbackIfDone();

packages/zone.js/test/zone-spec/async-test.spec.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,55 @@ describe('AsyncTestZoneSpec', function() {
132132
});
133133
});
134134

135+
it('should not call done multiple times in sync test', (done) => {
136+
const testFn = () => {
137+
Zone.current.run(() => {});
138+
Zone.current.run(() => {});
139+
};
140+
let doneCalledCount = 0;
141+
const testZoneSpec = new AsyncTestZoneSpec(() => {
142+
doneCalledCount++;
143+
}, () => {}, 'name');
144+
145+
const atz = Zone.current.fork(testZoneSpec);
146+
147+
atz.run(testFn);
148+
setTimeout(() => {
149+
expect(doneCalledCount).toBe(1);
150+
done();
151+
});
152+
});
153+
154+
it('should not call done multiple times in async test with nested zone', (done) => {
155+
const testFn = () => {
156+
Promise.resolve(1).then(() => {});
157+
};
158+
let doneCalledCount = 0;
159+
const testZoneSpec = new AsyncTestZoneSpec(() => {
160+
doneCalledCount++;
161+
}, () => {}, 'name');
162+
163+
const atz = Zone.current.fork(testZoneSpec);
164+
const c1 = atz.fork({
165+
name: 'child1',
166+
onHasTask: (delegate, current, target, hasTaskState) => {
167+
return delegate.hasTask(target, hasTaskState);
168+
}
169+
});
170+
const c2 = c1.fork({
171+
name: 'child2',
172+
onHasTask: (delegate, current, target, hasTaskState) => {
173+
return delegate.hasTask(target, hasTaskState);
174+
}
175+
});
176+
177+
c2.run(testFn);
178+
setTimeout(() => {
179+
expect(doneCalledCount).toBe(1);
180+
done();
181+
}, 50);
182+
});
183+
135184
describe('event tasks', ifEnvSupports('document', () => {
136185
let button: HTMLButtonElement;
137186
beforeEach(function() {

0 commit comments

Comments
 (0)