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

Commit a4b42cd

Browse files
JiaLiPassionmhevery
authored andcommitted
feat(test): move async/fakeAsync from angular to zone.js (#1048)
1 parent 57bc80c commit a4b42cd

File tree

12 files changed

+922
-377
lines changed

12 files changed

+922
-377
lines changed

gulpfile.js

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ function generateScript(inFile, outFile, minify, callback) {
3131
},
3232
output: {
3333
format: 'umd',
34+
name: 'zone',
3435
banner: '/**\n' +
3536
'* @license\n' +
3637
'* Copyright Google Inc. All Rights Reserved.\n' +
@@ -221,19 +222,23 @@ gulp.task('build/zone-patch-socket-io.min.js', ['compile-esm'], function(cb) {
221222
});
222223

223224
gulp.task('build/zone-patch-promise-testing.js', ['compile-esm'], function(cb) {
224-
return generateScript('./lib/testing/promise-testing.ts', 'zone-patch-promise-test.js', false, cb);
225+
return generateScript(
226+
'./lib/testing/promise-testing.ts', 'zone-patch-promise-test.js', false, cb);
225227
});
226228

227229
gulp.task('build/zone-patch-promise-testing.min.js', ['compile-esm'], function(cb) {
228-
return generateScript('./lib/testing/promise-testing.ts', 'zone-patch-promise-test.min.js', true, cb);
230+
return generateScript(
231+
'./lib/testing/promise-testing.ts', 'zone-patch-promise-test.min.js', true, cb);
229232
});
230233

231234
gulp.task('build/zone-patch-resize-observer.js', ['compile-esm'], function(cb) {
232-
return generateScript('./lib/browser/webapis-resize-observer.ts', 'zone-patch-resize-observer.js', false, cb);
235+
return generateScript(
236+
'./lib/browser/webapis-resize-observer.ts', 'zone-patch-resize-observer.js', false, cb);
233237
});
234238

235239
gulp.task('build/zone-patch-resize-observer.min.js', ['compile-esm'], function(cb) {
236-
return generateScript('./lib/browser/webapis-resize-observer.ts', 'zone-patch-resize-observer.min.js', true, cb);
240+
return generateScript(
241+
'./lib/browser/webapis-resize-observer.ts', 'zone-patch-resize-observer.min.js', true, cb);
237242
});
238243

239244
gulp.task('build/bluebird.js', ['compile-esm'], function(cb) {
@@ -245,11 +250,11 @@ gulp.task('build/bluebird.min.js', ['compile-esm'], function(cb) {
245250
});
246251

247252
gulp.task('build/zone-patch-jsonp.js', ['compile-esm'], function(cb) {
248-
return generateScript('./lib/extra/jsonp.ts', 'zone-patch-jsonp.js', false, cb);
253+
return generateScript('./lib/extra/jsonp.ts', 'zone-patch-jsonp.js', false, cb);
249254
});
250255

251256
gulp.task('build/zone-patch-jsonp.min.js', ['compile-esm'], function(cb) {
252-
return generateScript('./lib/extra/jsonp.ts', 'zone-patch-jsonp.min.js', true, cb);
257+
return generateScript('./lib/extra/jsonp.ts', 'zone-patch-jsonp.min.js', true, cb);
253258
});
254259

255260
gulp.task('build/jasmine-patch.js', ['compile-esm'], function(cb) {
@@ -323,11 +328,13 @@ gulp.task('build/rxjs.min.js', ['compile-esm'], function(cb) {
323328
});
324329

325330
gulp.task('build/rxjs-fake-async.js', ['compile-esm'], function(cb) {
326-
return generateScript('./lib/rxjs/rxjs-fake-async.ts', 'zone-patch-rxjs-fake-async.js', false, cb);
331+
return generateScript(
332+
'./lib/rxjs/rxjs-fake-async.ts', 'zone-patch-rxjs-fake-async.js', false, cb);
327333
});
328334

329335
gulp.task('build/rxjs-fake-async.min.js', ['compile-esm'], function(cb) {
330-
return generateScript('./lib/rxjs/rxjs-fake-async.ts', 'zone-patch-rxjs-fake-async.min.js', true, cb);
336+
return generateScript(
337+
'./lib/rxjs/rxjs-fake-async.ts', 'zone-patch-rxjs-fake-async.min.js', true, cb);
331338
});
332339

333340
gulp.task('build/closure.js', function() {

karma-dist.conf.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
module.exports = function (config) {
9+
module.exports = function(config) {
1010
require('./karma-base.conf.js')(config);
1111
config.files.push('build/test/wtf_mock.js');
1212
config.files.push('build/test/test_fake_polyfill.js');
1313
config.files.push('build/test/custom_error.js');
1414
config.files.push('dist/zone.js');
1515
config.files.push('dist/zone-patch-user-media.js');
16+
config.files.push('dist/zone-patch-resize-observer.js');
1617
config.files.push('dist/async-test.js');
1718
config.files.push('dist/fake-async-test.js');
1819
config.files.push('dist/long-stack-trace-zone.js');

lib/testing/async-testing.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
const _global: any =
10+
typeof window !== 'undefined' && window || typeof self !== 'undefined' && self || global;
11+
12+
/**
13+
* Wraps a test function in an asynchronous test zone. The test will automatically
14+
* complete when all asynchronous calls within this zone are done.
15+
*/
16+
export function asyncTest(fn: Function): (done: any) => any {
17+
// If we're running using the Jasmine test framework, adapt to call the 'done'
18+
// function when asynchronous activity is finished.
19+
if (_global.jasmine) {
20+
// Not using an arrow function to preserve context passed from call site
21+
return function(done: any) {
22+
if (!done) {
23+
// if we run beforeEach in @angular/core/testing/testing_internal then we get no done
24+
// fake it here and assume sync.
25+
done = function() {};
26+
done.fail = function(e: any) {
27+
throw e;
28+
};
29+
}
30+
runInTestZone(fn, this, done, (err: any) => {
31+
if (typeof err === 'string') {
32+
return done.fail(new Error(<string>err));
33+
} else {
34+
done.fail(err);
35+
}
36+
});
37+
};
38+
}
39+
// Otherwise, return a promise which will resolve when asynchronous activity
40+
// is finished. This will be correctly consumed by the Mocha framework with
41+
// it('...', async(myFn)); or can be used in a custom framework.
42+
// Not using an arrow function to preserve context passed from call site
43+
return function() {
44+
return new Promise<void>((finishCallback, failCallback) => {
45+
runInTestZone(fn, this, finishCallback, failCallback);
46+
});
47+
};
48+
}
49+
50+
function runInTestZone(
51+
fn: Function, context: any, finishCallback: Function, failCallback: Function) {
52+
const currentZone = Zone.current;
53+
const AsyncTestZoneSpec = (Zone as any)['AsyncTestZoneSpec'];
54+
if (AsyncTestZoneSpec === undefined) {
55+
throw new Error(
56+
'AsyncTestZoneSpec is needed for the async() test helper but could not be found. ' +
57+
'Please make sure that your environment includes zone.js/dist/async-test.js');
58+
}
59+
const ProxyZoneSpec = (Zone as any)['ProxyZoneSpec'] as {
60+
get(): {setDelegate(spec: ZoneSpec): void; getDelegate(): ZoneSpec;};
61+
assertPresent: () => void;
62+
};
63+
if (ProxyZoneSpec === undefined) {
64+
throw new Error(
65+
'ProxyZoneSpec is needed for the async() test helper but could not be found. ' +
66+
'Please make sure that your environment includes zone.js/dist/proxy.js');
67+
}
68+
const proxyZoneSpec = ProxyZoneSpec.get();
69+
ProxyZoneSpec.assertPresent();
70+
// We need to create the AsyncTestZoneSpec outside the ProxyZone.
71+
// If we do it in ProxyZone then we will get to infinite recursion.
72+
const proxyZone = Zone.current.getZoneWith('ProxyZoneSpec');
73+
const previousDelegate = proxyZoneSpec.getDelegate();
74+
proxyZone.parent.run(() => {
75+
const testZoneSpec: ZoneSpec = new AsyncTestZoneSpec(
76+
() => {
77+
// Need to restore the original zone.
78+
if (proxyZoneSpec.getDelegate() == testZoneSpec) {
79+
// Only reset the zone spec if it's
80+
// sill this one. Otherwise, assume
81+
// it's OK.
82+
proxyZoneSpec.setDelegate(previousDelegate);
83+
}
84+
currentZone.run(() => {
85+
finishCallback();
86+
});
87+
},
88+
(error: any) => {
89+
// Need to restore the original zone.
90+
if (proxyZoneSpec.getDelegate() == testZoneSpec) {
91+
// Only reset the zone spec if it's sill this one. Otherwise, assume it's OK.
92+
proxyZoneSpec.setDelegate(previousDelegate);
93+
}
94+
currentZone.run(() => {
95+
failCallback(error);
96+
});
97+
},
98+
'test');
99+
proxyZoneSpec.setDelegate(testZoneSpec);
100+
});
101+
return Zone.current.runGuarded(fn, context);
102+
}

lib/testing/fake-async.ts

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
const FakeAsyncTestZoneSpec = Zone && (Zone as any)['FakeAsyncTestZoneSpec'];
10+
type ProxyZoneSpec = {
11+
setDelegate(delegateSpec: ZoneSpec): void; getDelegate(): ZoneSpec; resetDelegate(): void;
12+
};
13+
const ProxyZoneSpec: {get(): ProxyZoneSpec; assertPresent: () => ProxyZoneSpec} =
14+
Zone && (Zone as any)['ProxyZoneSpec'];
15+
16+
let _fakeAsyncTestZoneSpec: any = null;
17+
18+
/**
19+
* Clears out the shared fake async zone for a test.
20+
* To be called in a global `beforeEach`.
21+
*
22+
* @experimental
23+
*/
24+
export function resetFakeAsyncZone() {
25+
_fakeAsyncTestZoneSpec = null;
26+
// in node.js testing we may not have ProxyZoneSpec in which case there is nothing to reset.
27+
ProxyZoneSpec && ProxyZoneSpec.assertPresent().resetDelegate();
28+
}
29+
30+
let _inFakeAsyncCall = false;
31+
32+
/**
33+
* Wraps a function to be executed in the fakeAsync zone:
34+
* - microtasks are manually executed by calling `flushMicrotasks()`,
35+
* - timers are synchronous, `tick()` simulates the asynchronous passage of time.
36+
*
37+
* If there are any pending timers at the end of the function, an exception will be thrown.
38+
*
39+
* Can be used to wrap inject() calls.
40+
*
41+
* ## Example
42+
*
43+
* {@example core/testing/ts/fake_async.ts region='basic'}
44+
*
45+
* @param fn
46+
* @returns The function wrapped to be executed in the fakeAsync zone
47+
*
48+
* @experimental
49+
*/
50+
export function fakeAsync(fn: Function): (...args: any[]) => any {
51+
// Not using an arrow function to preserve context passed from call site
52+
return function(...args: any[]) {
53+
const proxyZoneSpec = ProxyZoneSpec.assertPresent();
54+
if (_inFakeAsyncCall) {
55+
throw new Error('fakeAsync() calls can not be nested');
56+
}
57+
_inFakeAsyncCall = true;
58+
try {
59+
if (!_fakeAsyncTestZoneSpec) {
60+
if (proxyZoneSpec.getDelegate() instanceof FakeAsyncTestZoneSpec) {
61+
throw new Error('fakeAsync() calls can not be nested');
62+
}
63+
64+
_fakeAsyncTestZoneSpec = new FakeAsyncTestZoneSpec();
65+
}
66+
67+
let res: any;
68+
const lastProxyZoneSpec = proxyZoneSpec.getDelegate();
69+
proxyZoneSpec.setDelegate(_fakeAsyncTestZoneSpec);
70+
try {
71+
res = fn.apply(this, args);
72+
flushMicrotasks();
73+
} finally {
74+
proxyZoneSpec.setDelegate(lastProxyZoneSpec);
75+
}
76+
77+
if (_fakeAsyncTestZoneSpec.pendingPeriodicTimers.length > 0) {
78+
throw new Error(
79+
`${_fakeAsyncTestZoneSpec.pendingPeriodicTimers.length} ` +
80+
`periodic timer(s) still in the queue.`);
81+
}
82+
83+
if (_fakeAsyncTestZoneSpec.pendingTimers.length > 0) {
84+
throw new Error(
85+
`${_fakeAsyncTestZoneSpec.pendingTimers.length} timer(s) still in the queue.`);
86+
}
87+
return res;
88+
} finally {
89+
_inFakeAsyncCall = false;
90+
resetFakeAsyncZone();
91+
}
92+
};
93+
}
94+
95+
function _getFakeAsyncZoneSpec(): any {
96+
if (_fakeAsyncTestZoneSpec == null) {
97+
throw new Error('The code should be running in the fakeAsync zone to call this function');
98+
}
99+
return _fakeAsyncTestZoneSpec;
100+
}
101+
102+
/**
103+
* Simulates the asynchronous passage of time for the timers in the fakeAsync zone.
104+
*
105+
* The microtasks queue is drained at the very start of this function and after any timer callback
106+
* has been executed.
107+
*
108+
* ## Example
109+
*
110+
* {@example core/testing/ts/fake_async.ts region='basic'}
111+
*
112+
* @experimental
113+
*/
114+
export function tick(millis: number = 0): void {
115+
_getFakeAsyncZoneSpec().tick(millis);
116+
}
117+
118+
/**
119+
* Simulates the asynchronous passage of time for the timers in the fakeAsync zone by
120+
* draining the macrotask queue until it is empty. The returned value is the milliseconds
121+
* of time that would have been elapsed.
122+
*
123+
* @param maxTurns
124+
* @returns The simulated time elapsed, in millis.
125+
*
126+
* @experimental
127+
*/
128+
export function flush(maxTurns?: number): number {
129+
return _getFakeAsyncZoneSpec().flush(maxTurns);
130+
}
131+
132+
/**
133+
* Discard all remaining periodic tasks.
134+
*
135+
* @experimental
136+
*/
137+
export function discardPeriodicTasks(): void {
138+
const zoneSpec = _getFakeAsyncZoneSpec();
139+
const pendingTimers = zoneSpec.pendingPeriodicTimers;
140+
zoneSpec.pendingPeriodicTimers.length = 0;
141+
}
142+
143+
/**
144+
* Flush any pending microtasks.
145+
*
146+
* @experimental
147+
*/
148+
export function flushMicrotasks(): void {
149+
_getFakeAsyncZoneSpec().flushMicrotasks();
150+
}

lib/testing/zone-testing.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,6 @@ import '../zone-spec/sync-test';
1313
import '../jasmine/jasmine';
1414
import '../zone-spec/async-test';
1515
import '../zone-spec/fake-async-test';
16-
import './promise-testing';
16+
import './promise-testing';
17+
export * from './async-testing';
18+
export * from './fake-async';

lib/zone-spec/proxy.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
* Use of this source code is governed by an MIT-style license that can be
66
* found in the LICENSE file at https://angular.io/license
77
*/
8-
98
class ProxyZoneSpec implements ZoneSpec {
109
name: string = 'ProxyZone';
1110

@@ -28,7 +27,7 @@ class ProxyZoneSpec implements ZoneSpec {
2827
}
2928

3029
static assertPresent(): ProxyZoneSpec {
31-
if (!this.isLoaded()) {
30+
if (!ProxyZoneSpec.isLoaded()) {
3231
throw new Error(`Expected to be running in 'ProxyZone', but it was not found.`);
3332
}
3433
return ProxyZoneSpec.get();
@@ -69,7 +68,7 @@ class ProxyZoneSpec implements ZoneSpec {
6968
// last delegateSpec has microTask or macroTask
7069
// should call onHasTask in current delegateSpec
7170
this.isNeedToTriggerHasTask = false;
72-
this.onHasTask(parentZoneDelegate, currentZone, targetZone, this.lastTaskState);
71+
this.onHasTask(parentZoneDelegate, currentZone, targetZone, this.lastTaskState);
7372
}
7473
}
7574

0 commit comments

Comments
 (0)