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

Commit 6852f1d

Browse files
JiaLiPassionmhevery
authored andcommitted
feat(test): can handle non zone aware task in promise (#1014)
1 parent 2613109 commit 6852f1d

9 files changed

+147
-3
lines changed

Diff for: gulpfile.js

+10
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,14 @@ gulp.task('build/zone-patch-socket-io.min.js', ['compile-esm'], function(cb) {
200200
return generateScript('./lib/extra/socket-io.ts', 'zone-patch-socket-io.min.js', true, cb);
201201
});
202202

203+
gulp.task('build/zone-patch-promise-testing.js', ['compile-esm'], function(cb) {
204+
return generateScript('./lib/testing/promise-testing.ts', 'zone-patch-promise-test.js', false, cb);
205+
});
206+
207+
gulp.task('build/zone-patch-promise-testing.min.js', ['compile-esm'], function(cb) {
208+
return generateScript('./lib/testing/promise-testing.ts', 'zone-patch-promise-test.min.js', true, cb);
209+
});
210+
203211
gulp.task('build/bluebird.js', ['compile-esm'], function(cb) {
204212
return generateScript('./lib/extra/bluebird.ts', 'zone-bluebird.js', false, cb);
205213
});
@@ -323,6 +331,8 @@ gulp.task('build', [
323331
'build/zone-patch-user-media.min.js',
324332
'build/zone-patch-socket-io.js',
325333
'build/zone-patch-socket-io.min.js',
334+
'build/zone-patch-promise-testing.js',
335+
'build/zone-patch-promise-testing.min.js',
326336
'build/zone-mix.js',
327337
'build/bluebird.js',
328338
'build/bluebird.min.js',

Diff for: karma-dist.conf.js

+1
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,6 @@ module.exports = function (config) {
2020
config.files.push('dist/sync-test.js');
2121
config.files.push('dist/task-tracking.js');
2222
config.files.push('dist/wtf.js');
23+
config.files.push('dist/zone-patch-promise-test.js');
2324
config.files.push('build/test/main.js');
2425
};

Diff for: lib/common/promise.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ Zone.__load_patch('ZoneAwarePromise', (global: any, Zone: ZoneType, api: _ZonePr
268268
// if error occurs, should always return this error
269269
resolvePromise(chainPromise, false, error);
270270
}
271-
});
271+
}, chainPromise as TaskData);
272272
}
273273

274274
const ZONE_AWARE_PROMISE_TO_STRING = 'function ZoneAwarePromise() { [native code] }';

Diff for: lib/testing/promise-testing.ts

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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+
/**
10+
* Promise for async/fakeAsync zoneSpec test
11+
* can support async operation which not supported by zone.js
12+
* such as
13+
* it ('test jsonp in AsyncZone', async() => {
14+
* new Promise(res => {
15+
* jsonp(url, (data) => {
16+
* // success callback
17+
* res(data);
18+
* });
19+
* }).then((jsonpResult) => {
20+
* // get jsonp result.
21+
*
22+
* // user will expect AsyncZoneSpec wait for
23+
* // then, but because jsonp is not zone aware
24+
* // AsyncZone will finish before then is called.
25+
* });
26+
* });
27+
*/
28+
Zone.__load_patch('promisefortest', (global: any, Zone: ZoneType, api: _ZonePrivate) => {
29+
const symbolState: string = api.symbol('state');
30+
const UNRESOLVED: null = null;
31+
const symbolParentUnresolved = api.symbol('parentUnresolved');
32+
33+
// patch Promise.prototype.then to keep an internal
34+
// number for tracking unresolved chained promise
35+
// we will decrease this number when the parent promise
36+
// being resolved/rejected and chained promise was
37+
// scheduled as a microTask.
38+
// so we can know such kind of chained promise still
39+
// not resolved in AsyncTestZone
40+
(Promise as any)[api.symbol('patchPromiseForTest')] = function patchPromiseForTest() {
41+
let oriThen = (Promise as any)[Zone.__symbol__('ZonePromiseThen')];
42+
if (oriThen) {
43+
return;
44+
}
45+
oriThen = (Promise as any)[Zone.__symbol__('ZonePromiseThen')] = Promise.prototype.then;
46+
Promise.prototype.then = function() {
47+
const chained = oriThen.apply(this, arguments);
48+
if (this[symbolState] === UNRESOLVED) {
49+
// parent promise is unresolved.
50+
const asyncTestZoneSpec = Zone.current.get('AsyncTestZoneSpec');
51+
if (asyncTestZoneSpec) {
52+
asyncTestZoneSpec.unresolvedChainedPromiseCount ++;
53+
chained[symbolParentUnresolved] = true;
54+
}
55+
}
56+
return chained;
57+
};
58+
};
59+
60+
(Promise as any)[api.symbol('unPatchPromiseForTest')] = function unpatchPromiseForTest() {
61+
// restore origin then
62+
const oriThen = (Promise as any)[Zone.__symbol__('ZonePromiseThen')];
63+
if (oriThen) {
64+
Promise.prototype.then = oriThen;
65+
(Promise as any)[Zone.__symbol__('ZonePromiseThen')] = undefined;
66+
}
67+
};
68+
});

Diff for: lib/testing/zone-testing.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ import '../zone-spec/proxy';
1212
import '../zone-spec/sync-test';
1313
import '../jasmine/jasmine';
1414
import '../zone-spec/async-test';
15-
import '../zone-spec/fake-async-test';
15+
import '../zone-spec/fake-async-test';
16+
import './promise-testing';

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

+36-1
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,27 @@
77
*/
88

99
class AsyncTestZoneSpec implements ZoneSpec {
10+
static symbolParentUnresolved = Zone.__symbol__('parentUnresolved');
11+
1012
_finishCallback: Function;
1113
_failCallback: Function;
1214
_pendingMicroTasks: boolean = false;
1315
_pendingMacroTasks: boolean = false;
1416
_alreadyErrored: boolean = false;
1517
runZone = Zone.current;
18+
unresolvedChainedPromiseCount = 0;
1619

1720
constructor(finishCallback: Function, failCallback: Function, namePrefix: string) {
1821
this._finishCallback = finishCallback;
1922
this._failCallback = failCallback;
2023
this.name = 'asyncTestZone for ' + namePrefix;
24+
this.properties = {
25+
'AsyncTestZoneSpec': this
26+
};
2127
}
2228

2329
_finishCallbackIfDone() {
24-
if (!(this._pendingMicroTasks || this._pendingMacroTasks)) {
30+
if (!(this._pendingMicroTasks || this._pendingMacroTasks || this.unresolvedChainedPromiseCount !== 0)) {
2531
// We do this because we would like to catch unhandled rejected promises.
2632
this.runZone.run(() => {
2733
setTimeout(() => {
@@ -33,19 +39,48 @@ class AsyncTestZoneSpec implements ZoneSpec {
3339
}
3440
}
3541

42+
patchPromiseForTest() {
43+
const patchPromiseForTest = (Promise as any)[Zone.__symbol__('patchPromiseForTest')];
44+
if (patchPromiseForTest) {
45+
patchPromiseForTest();
46+
}
47+
}
48+
49+
unPatchPromiseForTest() {
50+
const unPatchPromiseForTest = (Promise as any)[Zone.__symbol__('unPatchPromiseForTest')];
51+
if (unPatchPromiseForTest) {
52+
unPatchPromiseForTest();
53+
}
54+
}
55+
3656
// ZoneSpec implementation below.
3757

3858
name: string;
3959

60+
properties: {[key: string]: any};
61+
62+
onScheduleTask(delegate: ZoneDelegate, current: Zone, target: Zone, task: Task): Task {
63+
if (task.type === 'microTask' && task.data && task.data instanceof Promise) {
64+
// check whether the promise is a chained promise
65+
if ((task.data as any)[AsyncTestZoneSpec.symbolParentUnresolved] === true) {
66+
// chained promise is being scheduled
67+
this.unresolvedChainedPromiseCount --;
68+
}
69+
}
70+
return delegate.scheduleTask(target, task);
71+
}
72+
4073
// Note - we need to use onInvoke at the moment to call finish when a test is
4174
// fully synchronous. TODO(juliemr): remove this when the logic for
4275
// onHasTask changes and it calls whenever the task queues are dirty.
4376
onInvoke(
4477
parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, delegate: Function,
4578
applyThis: any, applyArgs: any[], source: string): any {
4679
try {
80+
this.patchPromiseForTest();
4781
return parentZoneDelegate.invoke(targetZone, delegate, applyThis, applyArgs, source);
4882
} finally {
83+
this.unPatchPromiseForTest();
4984
this._finishCallbackIfDone();
5085
}
5186
}

Diff for: test/browser-zone-setup.ts

+1
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ import '../lib/zone-spec/sync-test';
1919
import '../lib/zone-spec/task-tracking';
2020
import '../lib/zone-spec/wtf';
2121
import '../lib/extra/cordova';
22+
import '../lib/testing/promise-testing';

Diff for: test/node_entry_point.ts

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import '../lib/zone-spec/task-tracking';
2424
import '../lib/zone-spec/wtf';
2525
import '../lib/rxjs/rxjs';
2626

27+
import '../lib/testing/promise-testing';
2728
// Setup test environment
2829
import './test-env-setup-jasmine';
2930

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

+27
Original file line numberDiff line numberDiff line change
@@ -316,4 +316,31 @@ describe('AsyncTestZoneSpec', function() {
316316
});
317317

318318
});
319+
320+
describe('non zone aware async task in promise should be detected', () => {
321+
it('should be able to detect non zone aware async task in promise', (done) => {
322+
let finished = false;
323+
324+
const testZoneSpec = new AsyncTestZoneSpec(
325+
() => {
326+
expect(finished).toBe(true);
327+
done();
328+
},
329+
() => {
330+
done.fail('async zone called failCallback unexpectedly');
331+
},
332+
'name');
333+
334+
const atz = Zone.current.fork(testZoneSpec);
335+
336+
atz.run(() => {
337+
new Promise((res, rej) => {
338+
const g: any = typeof window === 'undefined' ? global : window;
339+
g[Zone.__symbol__('setTimeout')](res, 100);
340+
}).then(() => {
341+
finished = true;
342+
});
343+
});
344+
});
345+
});
319346
});

0 commit comments

Comments
 (0)