Skip to content
This repository was archived by the owner on Feb 26, 2024. It is now read-only.

Commit aeeb05c

Browse files
juliemrmhevery
authored andcommitted
feat(zonespec): add a spec for asynchronous tests
This spec is constructed with a done callback and a fail callback, and waits until all asynchronous tasks are completed before exiting. Closes #275
1 parent 4d108ce commit aeeb05c

File tree

4 files changed

+335
-1
lines changed

4 files changed

+335
-1
lines changed

Diff for: gulpfile.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ gulp.task('build/wtf.min.js', function(cb) {
9898
return generateBrowserScript('./lib/zone-spec/wtf.ts', 'wtf.min.js', true, cb);
9999
});
100100

101+
gulp.task('build/async-test.js', function(cb) {
102+
return generateBrowserScript('./lib/zone-spec/async-test.ts', 'async-test.js', false, cb);
103+
});
104+
101105
gulp.task('build', [
102106
'build/zone.js',
103107
'build/zone.js.d.ts',
@@ -108,7 +112,8 @@ gulp.task('build', [
108112
'build/long-stack-trace-zone.js',
109113
'build/long-stack-trace-zone.min.js',
110114
'build/wtf.js',
111-
'build/wtf.min.js'
115+
'build/wtf.min.js',
116+
'build/async-test.js'
112117
]);
113118

114119

Diff for: lib/zone-spec/async-test.ts

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
(function() {
2+
class AsyncTestZoneSpec implements ZoneSpec {
3+
_finishCallback: Function;
4+
_failCallback: Function;
5+
_pendingMicroTasks: boolean = false;
6+
_pendingMacroTasks: boolean = false;
7+
_alreadyErrored: boolean = false;
8+
runZone = Zone.current;
9+
10+
constructor(finishCallback: Function, failCallback: Function, namePrefix: string) {
11+
this._finishCallback = finishCallback;
12+
this._failCallback = failCallback;
13+
this.name = 'asyncTestZone for ' + namePrefix;
14+
}
15+
16+
_finishCallbackIfDone() {
17+
if (!(this._pendingMicroTasks || this._pendingMacroTasks)) {
18+
// We do this because we would like to catch unhandled rejected promises.
19+
// To do this quickly when there are native promises, we must run using an unwrapped
20+
// promise implementation.
21+
var symbol = (<any>Zone).__symbol__;
22+
var NativePromise: typeof Promise = <any>window[symbol('Promise')];
23+
if (NativePromise) {
24+
NativePromise.resolve(true)[symbol('then')](() => {
25+
if (!this._alreadyErrored) {
26+
this.runZone.run(this._finishCallback);
27+
}
28+
});
29+
} else {
30+
// For implementations which do not have nativePromise, use setTimeout(0). This is slower,
31+
// but it also works because Zones will handle errors when rejected promises have no
32+
// listeners after one macrotask.
33+
this.runZone.run(() => {
34+
setTimeout(() => {
35+
if (!this._alreadyErrored) {
36+
this._finishCallback();
37+
}
38+
}, 0);
39+
});
40+
}
41+
}
42+
}
43+
44+
// ZoneSpec implementation below.
45+
46+
name: string;
47+
48+
// Note - we need to use onInvoke at the moment to call finish when a test is
49+
// fully synchronous. TODO(juliemr): remove this when the logic for
50+
// onHasTask changes and it calls whenever the task queues are dirty.
51+
onInvoke(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
52+
delegate: Function, applyThis: any, applyArgs: any[], source: string): any {
53+
try {
54+
return parentZoneDelegate.invoke(targetZone, delegate, applyThis, applyArgs, source);
55+
} finally {
56+
this._finishCallbackIfDone();
57+
}
58+
}
59+
60+
onHandleError(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
61+
error: any): boolean {
62+
// Let the parent try to handle the error.
63+
var result = parentZoneDelegate.handleError(targetZone, error);
64+
if (result) {
65+
this._failCallback(error.message ? error.message : 'unknown error');
66+
this._alreadyErrored = true;
67+
}
68+
return false;
69+
}
70+
71+
onScheduleTask(delegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task): Task {
72+
if (task.type == 'macroTask' && task.source == 'setInterval') {
73+
this._failCallback('Cannot use setInterval from within an async zone test.');
74+
return;
75+
}
76+
77+
return delegate.scheduleTask(targetZone, task);
78+
}
79+
80+
onHasTask(delegate: ZoneDelegate, current: Zone, target: Zone, hasTaskState: HasTaskState) {
81+
delegate.hasTask(target, hasTaskState);
82+
83+
if (hasTaskState.change == 'microTask') {
84+
this._pendingMicroTasks = hasTaskState.microTask;
85+
this._finishCallbackIfDone();
86+
} else if (hasTaskState.change == 'macroTask') {
87+
this._pendingMacroTasks = hasTaskState.macroTask;
88+
this._finishCallbackIfDone();
89+
}
90+
}
91+
}
92+
93+
// Export the class so that new instances can be created with proper
94+
// constructor params.
95+
Zone['AsyncTestZoneSpec'] = AsyncTestZoneSpec;
96+
})();

Diff for: test/async-test.spec.ts

+232
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
import '../lib/zone-spec/async-test';
2+
3+
describe('AsyncTestZoneSpec', function() {
4+
var log;
5+
var AsyncTestZoneSpec = Zone['AsyncTestZoneSpec'];
6+
7+
function finishCallback() {
8+
log.push('finish');
9+
}
10+
11+
function failCallback() {
12+
log.push('fail');
13+
}
14+
15+
beforeEach(() => {
16+
log = [];
17+
});
18+
19+
it('should call finish after zone is run', (done) => {
20+
var finished = false;
21+
var testZoneSpec = new AsyncTestZoneSpec(() => {
22+
expect(finished).toBe(true);
23+
done();
24+
}, failCallback, 'name');
25+
26+
var atz = Zone.current.fork(testZoneSpec);
27+
28+
atz.run(function() {
29+
finished = true;
30+
});
31+
});
32+
33+
it('should call finish after a setTimeout is done', (done) => {
34+
var finished = false;
35+
36+
var testZoneSpec = new AsyncTestZoneSpec(() => {
37+
expect(finished).toBe(true);
38+
done();
39+
}, () => {
40+
done.fail('async zone called failCallback unexpectedly');
41+
}, 'name');
42+
43+
var atz = Zone.current.fork(testZoneSpec);
44+
45+
atz.run(function() {
46+
setTimeout(() => {
47+
finished = true;
48+
}, 10);
49+
});
50+
});
51+
52+
it('should call finish after microtasks are done', (done) => {
53+
var finished = false;
54+
55+
var testZoneSpec = new AsyncTestZoneSpec(() => {
56+
expect(finished).toBe(true);
57+
done();
58+
}, () => {
59+
done.fail('async zone called failCallback unexpectedly');
60+
}, 'name');
61+
62+
var atz = Zone.current.fork(testZoneSpec);
63+
64+
atz.run(function() {
65+
Promise.resolve().then(() => {
66+
finished = true;
67+
});
68+
});
69+
});
70+
71+
it('should call finish after both micro and macrotasks are done', (done) => {
72+
var finished = false;
73+
74+
var testZoneSpec = new AsyncTestZoneSpec(() => {
75+
expect(finished).toBe(true);
76+
done();
77+
}, () => {
78+
done.fail('async zone called failCallback unexpectedly');
79+
}, 'name');
80+
81+
var atz = Zone.current.fork(testZoneSpec);
82+
83+
atz.run(function() {
84+
var deferred = new Promise((resolve, reject) => {
85+
setTimeout(() => {
86+
resolve();
87+
}, 10)
88+
}).then(() => {
89+
finished = true;
90+
});
91+
});
92+
});
93+
94+
it('should call finish after both macro and microtasks are done', (done) => {
95+
var finished = false;
96+
97+
var testZoneSpec = new AsyncTestZoneSpec(() => {
98+
expect(finished).toBe(true);
99+
done();
100+
}, () => {
101+
done.fail('async zone called failCallback unexpectedly');
102+
}, 'name');
103+
104+
var atz = Zone.current.fork(testZoneSpec);
105+
106+
atz.run(function() {
107+
Promise.resolve().then(() => {
108+
setTimeout(() => {
109+
finished = true;
110+
}, 10);
111+
});
112+
});
113+
});
114+
115+
describe('event tasks', () => {
116+
var button;
117+
beforeEach(function() {
118+
button = document.createElement('button');
119+
document.body.appendChild(button);
120+
});
121+
afterEach(function() {
122+
document.body.removeChild(button);
123+
});
124+
125+
it('should call finish after an event task is done', (done) => {
126+
var finished = false;
127+
128+
var testZoneSpec = new AsyncTestZoneSpec(() => {
129+
expect(finished).toBe(true);
130+
done();
131+
}, () => {
132+
done.fail('async zone called failCallback unexpectedly');
133+
}, 'name');
134+
135+
var atz = Zone.current.fork(testZoneSpec);
136+
137+
atz.run(function() {
138+
button.addEventListener('click', () => {
139+
finished = true;
140+
});
141+
142+
var clickEvent = document.createEvent('Event');
143+
clickEvent.initEvent('click', true, true);
144+
145+
button.dispatchEvent(clickEvent);
146+
});
147+
});
148+
149+
it('should call finish after an event task is done asynchronously', (done) => {
150+
var finished = false;
151+
152+
var testZoneSpec = new AsyncTestZoneSpec(() => {
153+
expect(finished).toBe(true);
154+
done();
155+
}, () => {
156+
done.fail('async zone called failCallback unexpectedly');
157+
}, 'name');
158+
159+
var atz = Zone.current.fork(testZoneSpec);
160+
161+
atz.run(function() {
162+
button.addEventListener('click', () => {
163+
setTimeout(() => {
164+
finished = true;
165+
}, 10);
166+
});
167+
168+
var clickEvent = document.createEvent('Event');
169+
clickEvent.initEvent('click', true, true);
170+
171+
button.dispatchEvent(clickEvent);
172+
});
173+
});
174+
});
175+
176+
it('should fail if setInterval is used', (done) => {
177+
var finished = false;
178+
179+
var testZoneSpec = new AsyncTestZoneSpec(() => {
180+
done.fail('expected failCallback to be called');
181+
}, (err) => {
182+
expect(err).toEqual('Cannot use setInterval from within an async zone test.');
183+
done();
184+
}, 'name');
185+
186+
var atz = Zone.current.fork(testZoneSpec);
187+
188+
atz.run(function() {
189+
setInterval(() => {
190+
}, 100);
191+
});
192+
});
193+
194+
it('should fail if an error is thrown asynchronously', (done) => {
195+
var finished = false;
196+
197+
var testZoneSpec = new AsyncTestZoneSpec(() => {
198+
done.fail('expected failCallback to be called');
199+
}, (err) => {
200+
expect(err).toEqual('my error');
201+
done();
202+
}, 'name');
203+
204+
var atz = Zone.current.fork(testZoneSpec);
205+
206+
atz.run(function() {
207+
setTimeout(() => {
208+
throw new Error('my error');
209+
}, 10);
210+
});
211+
});
212+
213+
it('should fail if a promise rejection is unhandled', (done) => {
214+
var finished = false;
215+
216+
var testZoneSpec = new AsyncTestZoneSpec(() => {
217+
done.fail('expected failCallback to be called');
218+
}, (err) => {
219+
expect(err).toEqual('Uncaught (in promise): my reason');
220+
done();
221+
}, 'name');
222+
223+
var atz = Zone.current.fork(testZoneSpec);
224+
225+
atz.run(function() {
226+
Promise.reject('my reason');
227+
});
228+
229+
});
230+
});
231+
232+
export var __something__;

Diff for: test/browser_entry_point.ts

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import './test-env-setup';
1212

1313
// List all tests here:
1414
import './long-stack-trace-zone.spec';
15+
import './async-test.spec';
1516
import './microtasks.spec';
1617
import './zone.spec';
1718
import './integration/brick.spec';

0 commit comments

Comments
 (0)