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

Commit

Permalink
feat(test): can handle non zone aware task in promise (#1014)
Browse files Browse the repository at this point in the history
  • Loading branch information
JiaLiPassion authored and mhevery committed Feb 10, 2018
1 parent 2613109 commit 6852f1d
Show file tree
Hide file tree
Showing 9 changed files with 147 additions and 3 deletions.
10 changes: 10 additions & 0 deletions gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,14 @@ gulp.task('build/zone-patch-socket-io.min.js', ['compile-esm'], function(cb) {
return generateScript('./lib/extra/socket-io.ts', 'zone-patch-socket-io.min.js', true, cb);
});

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

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

gulp.task('build/bluebird.js', ['compile-esm'], function(cb) {
return generateScript('./lib/extra/bluebird.ts', 'zone-bluebird.js', false, cb);
});
Expand Down Expand Up @@ -323,6 +331,8 @@ gulp.task('build', [
'build/zone-patch-user-media.min.js',
'build/zone-patch-socket-io.js',
'build/zone-patch-socket-io.min.js',
'build/zone-patch-promise-testing.js',
'build/zone-patch-promise-testing.min.js',
'build/zone-mix.js',
'build/bluebird.js',
'build/bluebird.min.js',
Expand Down
1 change: 1 addition & 0 deletions karma-dist.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@ module.exports = function (config) {
config.files.push('dist/sync-test.js');
config.files.push('dist/task-tracking.js');
config.files.push('dist/wtf.js');
config.files.push('dist/zone-patch-promise-test.js');
config.files.push('build/test/main.js');
};
2 changes: 1 addition & 1 deletion lib/common/promise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ Zone.__load_patch('ZoneAwarePromise', (global: any, Zone: ZoneType, api: _ZonePr
// if error occurs, should always return this error
resolvePromise(chainPromise, false, error);
}
});
}, chainPromise as TaskData);
}

const ZONE_AWARE_PROMISE_TO_STRING = 'function ZoneAwarePromise() { [native code] }';
Expand Down
68 changes: 68 additions & 0 deletions lib/testing/promise-testing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

/**
* Promise for async/fakeAsync zoneSpec test
* can support async operation which not supported by zone.js
* such as
* it ('test jsonp in AsyncZone', async() => {
* new Promise(res => {
* jsonp(url, (data) => {
* // success callback
* res(data);
* });
* }).then((jsonpResult) => {
* // get jsonp result.
*
* // user will expect AsyncZoneSpec wait for
* // then, but because jsonp is not zone aware
* // AsyncZone will finish before then is called.
* });
* });
*/
Zone.__load_patch('promisefortest', (global: any, Zone: ZoneType, api: _ZonePrivate) => {
const symbolState: string = api.symbol('state');
const UNRESOLVED: null = null;
const symbolParentUnresolved = api.symbol('parentUnresolved');

// patch Promise.prototype.then to keep an internal
// number for tracking unresolved chained promise
// we will decrease this number when the parent promise
// being resolved/rejected and chained promise was
// scheduled as a microTask.
// so we can know such kind of chained promise still
// not resolved in AsyncTestZone
(Promise as any)[api.symbol('patchPromiseForTest')] = function patchPromiseForTest() {
let oriThen = (Promise as any)[Zone.__symbol__('ZonePromiseThen')];
if (oriThen) {
return;
}
oriThen = (Promise as any)[Zone.__symbol__('ZonePromiseThen')] = Promise.prototype.then;
Promise.prototype.then = function() {
const chained = oriThen.apply(this, arguments);
if (this[symbolState] === UNRESOLVED) {
// parent promise is unresolved.
const asyncTestZoneSpec = Zone.current.get('AsyncTestZoneSpec');
if (asyncTestZoneSpec) {
asyncTestZoneSpec.unresolvedChainedPromiseCount ++;
chained[symbolParentUnresolved] = true;
}
}
return chained;
};
};

(Promise as any)[api.symbol('unPatchPromiseForTest')] = function unpatchPromiseForTest() {
// restore origin then
const oriThen = (Promise as any)[Zone.__symbol__('ZonePromiseThen')];
if (oriThen) {
Promise.prototype.then = oriThen;
(Promise as any)[Zone.__symbol__('ZonePromiseThen')] = undefined;
}
};
});
3 changes: 2 additions & 1 deletion lib/testing/zone-testing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ import '../zone-spec/proxy';
import '../zone-spec/sync-test';
import '../jasmine/jasmine';
import '../zone-spec/async-test';
import '../zone-spec/fake-async-test';
import '../zone-spec/fake-async-test';
import './promise-testing';
37 changes: 36 additions & 1 deletion lib/zone-spec/async-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,27 @@
*/

class AsyncTestZoneSpec implements ZoneSpec {
static symbolParentUnresolved = Zone.__symbol__('parentUnresolved');

_finishCallback: Function;
_failCallback: Function;
_pendingMicroTasks: boolean = false;
_pendingMacroTasks: boolean = false;
_alreadyErrored: boolean = false;
runZone = Zone.current;
unresolvedChainedPromiseCount = 0;

constructor(finishCallback: Function, failCallback: Function, namePrefix: string) {
this._finishCallback = finishCallback;
this._failCallback = failCallback;
this.name = 'asyncTestZone for ' + namePrefix;
this.properties = {
'AsyncTestZoneSpec': this
};
}

_finishCallbackIfDone() {
if (!(this._pendingMicroTasks || this._pendingMacroTasks)) {
if (!(this._pendingMicroTasks || this._pendingMacroTasks || this.unresolvedChainedPromiseCount !== 0)) {
// We do this because we would like to catch unhandled rejected promises.
this.runZone.run(() => {
setTimeout(() => {
Expand All @@ -33,19 +39,48 @@ class AsyncTestZoneSpec implements ZoneSpec {
}
}

patchPromiseForTest() {
const patchPromiseForTest = (Promise as any)[Zone.__symbol__('patchPromiseForTest')];
if (patchPromiseForTest) {
patchPromiseForTest();
}
}

unPatchPromiseForTest() {
const unPatchPromiseForTest = (Promise as any)[Zone.__symbol__('unPatchPromiseForTest')];
if (unPatchPromiseForTest) {
unPatchPromiseForTest();
}
}

// ZoneSpec implementation below.

name: string;

properties: {[key: string]: any};

onScheduleTask(delegate: ZoneDelegate, current: Zone, target: Zone, task: Task): Task {
if (task.type === 'microTask' && task.data && task.data instanceof Promise) {
// check whether the promise is a chained promise
if ((task.data as any)[AsyncTestZoneSpec.symbolParentUnresolved] === true) {
// chained promise is being scheduled
this.unresolvedChainedPromiseCount --;
}
}
return delegate.scheduleTask(target, task);
}

// Note - we need to use onInvoke at the moment to call finish when a test is
// fully synchronous. TODO(juliemr): remove this when the logic for
// onHasTask changes and it calls whenever the task queues are dirty.
onInvoke(
parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, delegate: Function,
applyThis: any, applyArgs: any[], source: string): any {
try {
this.patchPromiseForTest();
return parentZoneDelegate.invoke(targetZone, delegate, applyThis, applyArgs, source);
} finally {
this.unPatchPromiseForTest();
this._finishCallbackIfDone();
}
}
Expand Down
1 change: 1 addition & 0 deletions test/browser-zone-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ import '../lib/zone-spec/sync-test';
import '../lib/zone-spec/task-tracking';
import '../lib/zone-spec/wtf';
import '../lib/extra/cordova';
import '../lib/testing/promise-testing';
1 change: 1 addition & 0 deletions test/node_entry_point.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import '../lib/zone-spec/task-tracking';
import '../lib/zone-spec/wtf';
import '../lib/rxjs/rxjs';

import '../lib/testing/promise-testing';
// Setup test environment
import './test-env-setup-jasmine';

Expand Down
27 changes: 27 additions & 0 deletions test/zone-spec/async-test.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,4 +316,31 @@ describe('AsyncTestZoneSpec', function() {
});

});

describe('non zone aware async task in promise should be detected', () => {
it('should be able to detect non zone aware async task in promise', (done) => {
let finished = false;

const testZoneSpec = new AsyncTestZoneSpec(
() => {
expect(finished).toBe(true);
done();
},
() => {
done.fail('async zone called failCallback unexpectedly');
},
'name');

const atz = Zone.current.fork(testZoneSpec);

atz.run(() => {
new Promise((res, rej) => {
const g: any = typeof window === 'undefined' ? global : window;
g[Zone.__symbol__('setTimeout')](res, 100);
}).then(() => {
finished = true;
});
});
});
});
});

0 comments on commit 6852f1d

Please sign in to comment.