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

Commit 581146b

Browse files
committed
feat(test): can handle non zone aware task in promise
1 parent fd91152 commit 581146b

File tree

8 files changed

+150
-2
lines changed

8 files changed

+150
-2
lines changed

gulpfile.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,14 @@ gulp.task('build/zone-patch-user-media.min.js', ['compile-esm'], function(cb) {
192192
return generateScript('./lib/browser/webapis-user-media.ts', 'zone-patch-user-media.min.js', true, cb);
193193
});
194194

195+
gulp.task('build/zone-patch-promise-test.js', ['compile-esm'], function(cb) {
196+
return generateScript('./lib/testing/promise-testing.ts', 'zone-patch-promise-test.js', false, cb);
197+
});
198+
199+
gulp.task('build/zone-patch-promise-test.min.js', ['compile-esm'], function(cb) {
200+
return generateScript('./lib/testing/promise-testing.ts', 'zone-patch-promise-test.min.js', true, cb);
201+
});
202+
195203
gulp.task('build/bluebird.js', ['compile-esm'], function(cb) {
196204
return generateScript('./lib/extra/bluebird.ts', 'zone-bluebird.js', false, cb);
197205
});
@@ -297,6 +305,8 @@ gulp.task('build', [
297305
'build/zone-patch-electron.min.js',
298306
'build/zone-patch-user-media.js',
299307
'build/zone-patch-user-media.min.js',
308+
'build/zone-patch-promise-testing.js',
309+
'build/zone-patch-promise-testing.min.js',
300310
'build/zone-mix.js',
301311
'build/bluebird.js',
302312
'build/bluebird.min.js',

karma-dist.conf.js

Lines changed: 1 addition & 0 deletions
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
};

lib/common/promise.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ Zone.__load_patch('ZoneAwarePromise', (global: any, Zone: ZoneType, api: _ZonePr
241241
} catch (error) {
242242
resolvePromise(chainPromise, false, error);
243243
}
244-
});
244+
}, chainPromise as TaskData);
245245
}
246246

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

lib/testing/promise-testing.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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 props = Zone.current.get('chainedPromise');
51+
if (props) {
52+
let count = props.count;
53+
if (typeof count !== 'number') {
54+
count = 0;
55+
}
56+
props.count ++;
57+
}
58+
chained[symbolParentUnresolved] = true;
59+
}
60+
return chained;
61+
};
62+
};
63+
64+
(Promise as any)[api.symbol('unpatchPromiseForTest')] = function unpatchPromiseForTest() {
65+
// restore origin then
66+
const oriThen = (Promise as any)[Zone.__symbol__('ZonePromiseThen')];
67+
if (oriThen) {
68+
Promise.prototype.then = oriThen;
69+
(Promise as any)[Zone.__symbol__('ZonePromiseThen')] = undefined;
70+
}
71+
};
72+
});

lib/zone-spec/async-test.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
*/
88

99
class AsyncTestZoneSpec implements ZoneSpec {
10+
static symbolParentUnresolved = Zone.__symbol__('parentUnresolved');
11+
1012
_finishCallback: Function;
1113
_failCallback: Function;
1214
_pendingMicroTasks: boolean = false;
@@ -18,10 +20,15 @@ class AsyncTestZoneSpec implements ZoneSpec {
1820
this._finishCallback = finishCallback;
1921
this._failCallback = failCallback;
2022
this.name = 'asyncTestZone for ' + namePrefix;
23+
this.properties = {
24+
'chainedPromise': {
25+
'count': 0
26+
}
27+
};
2128
}
2229

2330
_finishCallbackIfDone() {
24-
if (!(this._pendingMicroTasks || this._pendingMacroTasks)) {
31+
if (!(this._pendingMicroTasks || this._pendingMacroTasks || this.properties['chainedPromise']['count'] !== 0)) {
2532
// We do this because we would like to catch unhandled rejected promises.
2633
this.runZone.run(() => {
2734
setTimeout(() => {
@@ -37,6 +44,21 @@ class AsyncTestZoneSpec implements ZoneSpec {
3744

3845
name: string;
3946

47+
properties: {[key: string]: any};
48+
49+
50+
onScheduleTask(delegate: ZoneDelegate, current: Zone, target: Zone, task: Task): Task {
51+
if (task.type === 'microTask' && task.data && task.data instanceof Promise) {
52+
// check whether the promise is a chained promise
53+
if ((task.data as any)[AsyncTestZoneSpec.symbolParentUnresolved] === true) {
54+
// chained promise is being scheduled
55+
const props = this.properties['chainedPromise'];
56+
props['count'] --;
57+
}
58+
}
59+
return delegate.scheduleTask(target, task);
60+
}
61+
4062
// Note - we need to use onInvoke at the moment to call finish when a test is
4163
// fully synchronous. TODO(juliemr): remove this when the logic for
4264
// onHasTask changes and it calls whenever the task queues are dirty.

test/browser-zone-setup.ts

Lines changed: 1 addition & 0 deletions
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';

test/node_entry_point.ts

Lines changed: 1 addition & 0 deletions
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

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

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,4 +316,45 @@ describe('AsyncTestZoneSpec', function() {
316316
});
317317

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

0 commit comments

Comments
 (0)