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

Commit

Permalink
feat(test): move async/fakeAsync from angular to zone.js (#1048)
Browse files Browse the repository at this point in the history
  • Loading branch information
JiaLiPassion authored and mhevery committed Mar 30, 2018
1 parent 57bc80c commit a4b42cd
Show file tree
Hide file tree
Showing 12 changed files with 922 additions and 377 deletions.
23 changes: 15 additions & 8 deletions gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ function generateScript(inFile, outFile, minify, callback) {
},
output: {
format: 'umd',
name: 'zone',
banner: '/**\n' +
'* @license\n' +
'* Copyright Google Inc. All Rights Reserved.\n' +
Expand Down Expand Up @@ -221,19 +222,23 @@ gulp.task('build/zone-patch-socket-io.min.js', ['compile-esm'], function(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);
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);
return generateScript(
'./lib/testing/promise-testing.ts', 'zone-patch-promise-test.min.js', true, cb);
});

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

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

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

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

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

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

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

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

gulp.task('build/closure.js', function() {
Expand Down
3 changes: 2 additions & 1 deletion karma-dist.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
* found in the LICENSE file at https://angular.io/license
*/

module.exports = function (config) {
module.exports = function(config) {
require('./karma-base.conf.js')(config);
config.files.push('build/test/wtf_mock.js');
config.files.push('build/test/test_fake_polyfill.js');
config.files.push('build/test/custom_error.js');
config.files.push('dist/zone.js');
config.files.push('dist/zone-patch-user-media.js');
config.files.push('dist/zone-patch-resize-observer.js');
config.files.push('dist/async-test.js');
config.files.push('dist/fake-async-test.js');
config.files.push('dist/long-stack-trace-zone.js');
Expand Down
102 changes: 102 additions & 0 deletions lib/testing/async-testing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* @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
*/

const _global: any =
typeof window !== 'undefined' && window || typeof self !== 'undefined' && self || global;

/**
* Wraps a test function in an asynchronous test zone. The test will automatically
* complete when all asynchronous calls within this zone are done.
*/
export function asyncTest(fn: Function): (done: any) => any {
// If we're running using the Jasmine test framework, adapt to call the 'done'
// function when asynchronous activity is finished.
if (_global.jasmine) {
// Not using an arrow function to preserve context passed from call site
return function(done: any) {
if (!done) {
// if we run beforeEach in @angular/core/testing/testing_internal then we get no done
// fake it here and assume sync.
done = function() {};
done.fail = function(e: any) {
throw e;
};
}
runInTestZone(fn, this, done, (err: any) => {
if (typeof err === 'string') {
return done.fail(new Error(<string>err));
} else {
done.fail(err);
}
});
};
}
// Otherwise, return a promise which will resolve when asynchronous activity
// is finished. This will be correctly consumed by the Mocha framework with
// it('...', async(myFn)); or can be used in a custom framework.
// Not using an arrow function to preserve context passed from call site
return function() {
return new Promise<void>((finishCallback, failCallback) => {
runInTestZone(fn, this, finishCallback, failCallback);
});
};
}

function runInTestZone(
fn: Function, context: any, finishCallback: Function, failCallback: Function) {
const currentZone = Zone.current;
const AsyncTestZoneSpec = (Zone as any)['AsyncTestZoneSpec'];
if (AsyncTestZoneSpec === undefined) {
throw new Error(
'AsyncTestZoneSpec is needed for the async() test helper but could not be found. ' +
'Please make sure that your environment includes zone.js/dist/async-test.js');
}
const ProxyZoneSpec = (Zone as any)['ProxyZoneSpec'] as {
get(): {setDelegate(spec: ZoneSpec): void; getDelegate(): ZoneSpec;};
assertPresent: () => void;
};
if (ProxyZoneSpec === undefined) {
throw new Error(
'ProxyZoneSpec is needed for the async() test helper but could not be found. ' +
'Please make sure that your environment includes zone.js/dist/proxy.js');
}
const proxyZoneSpec = ProxyZoneSpec.get();
ProxyZoneSpec.assertPresent();
// We need to create the AsyncTestZoneSpec outside the ProxyZone.
// If we do it in ProxyZone then we will get to infinite recursion.
const proxyZone = Zone.current.getZoneWith('ProxyZoneSpec');
const previousDelegate = proxyZoneSpec.getDelegate();
proxyZone.parent.run(() => {
const testZoneSpec: ZoneSpec = new AsyncTestZoneSpec(
() => {
// Need to restore the original zone.
if (proxyZoneSpec.getDelegate() == testZoneSpec) {
// Only reset the zone spec if it's
// sill this one. Otherwise, assume
// it's OK.
proxyZoneSpec.setDelegate(previousDelegate);
}
currentZone.run(() => {
finishCallback();
});
},
(error: any) => {
// Need to restore the original zone.
if (proxyZoneSpec.getDelegate() == testZoneSpec) {
// Only reset the zone spec if it's sill this one. Otherwise, assume it's OK.
proxyZoneSpec.setDelegate(previousDelegate);
}
currentZone.run(() => {
failCallback(error);
});
},
'test');
proxyZoneSpec.setDelegate(testZoneSpec);
});
return Zone.current.runGuarded(fn, context);
}
150 changes: 150 additions & 0 deletions lib/testing/fake-async.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/**
* @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
*/

const FakeAsyncTestZoneSpec = Zone && (Zone as any)['FakeAsyncTestZoneSpec'];
type ProxyZoneSpec = {
setDelegate(delegateSpec: ZoneSpec): void; getDelegate(): ZoneSpec; resetDelegate(): void;
};
const ProxyZoneSpec: {get(): ProxyZoneSpec; assertPresent: () => ProxyZoneSpec} =
Zone && (Zone as any)['ProxyZoneSpec'];

let _fakeAsyncTestZoneSpec: any = null;

/**
* Clears out the shared fake async zone for a test.
* To be called in a global `beforeEach`.
*
* @experimental
*/
export function resetFakeAsyncZone() {
_fakeAsyncTestZoneSpec = null;
// in node.js testing we may not have ProxyZoneSpec in which case there is nothing to reset.
ProxyZoneSpec && ProxyZoneSpec.assertPresent().resetDelegate();
}

let _inFakeAsyncCall = false;

/**
* Wraps a function to be executed in the fakeAsync zone:
* - microtasks are manually executed by calling `flushMicrotasks()`,
* - timers are synchronous, `tick()` simulates the asynchronous passage of time.
*
* If there are any pending timers at the end of the function, an exception will be thrown.
*
* Can be used to wrap inject() calls.
*
* ## Example
*
* {@example core/testing/ts/fake_async.ts region='basic'}
*
* @param fn
* @returns The function wrapped to be executed in the fakeAsync zone
*
* @experimental
*/
export function fakeAsync(fn: Function): (...args: any[]) => any {
// Not using an arrow function to preserve context passed from call site
return function(...args: any[]) {
const proxyZoneSpec = ProxyZoneSpec.assertPresent();
if (_inFakeAsyncCall) {
throw new Error('fakeAsync() calls can not be nested');
}
_inFakeAsyncCall = true;
try {
if (!_fakeAsyncTestZoneSpec) {
if (proxyZoneSpec.getDelegate() instanceof FakeAsyncTestZoneSpec) {
throw new Error('fakeAsync() calls can not be nested');
}

_fakeAsyncTestZoneSpec = new FakeAsyncTestZoneSpec();
}

let res: any;
const lastProxyZoneSpec = proxyZoneSpec.getDelegate();
proxyZoneSpec.setDelegate(_fakeAsyncTestZoneSpec);
try {
res = fn.apply(this, args);
flushMicrotasks();
} finally {
proxyZoneSpec.setDelegate(lastProxyZoneSpec);
}

if (_fakeAsyncTestZoneSpec.pendingPeriodicTimers.length > 0) {
throw new Error(
`${_fakeAsyncTestZoneSpec.pendingPeriodicTimers.length} ` +
`periodic timer(s) still in the queue.`);
}

if (_fakeAsyncTestZoneSpec.pendingTimers.length > 0) {
throw new Error(
`${_fakeAsyncTestZoneSpec.pendingTimers.length} timer(s) still in the queue.`);
}
return res;
} finally {
_inFakeAsyncCall = false;
resetFakeAsyncZone();
}
};
}

function _getFakeAsyncZoneSpec(): any {
if (_fakeAsyncTestZoneSpec == null) {
throw new Error('The code should be running in the fakeAsync zone to call this function');
}
return _fakeAsyncTestZoneSpec;
}

/**
* Simulates the asynchronous passage of time for the timers in the fakeAsync zone.
*
* The microtasks queue is drained at the very start of this function and after any timer callback
* has been executed.
*
* ## Example
*
* {@example core/testing/ts/fake_async.ts region='basic'}
*
* @experimental
*/
export function tick(millis: number = 0): void {
_getFakeAsyncZoneSpec().tick(millis);
}

/**
* Simulates the asynchronous passage of time for the timers in the fakeAsync zone by
* draining the macrotask queue until it is empty. The returned value is the milliseconds
* of time that would have been elapsed.
*
* @param maxTurns
* @returns The simulated time elapsed, in millis.
*
* @experimental
*/
export function flush(maxTurns?: number): number {
return _getFakeAsyncZoneSpec().flush(maxTurns);
}

/**
* Discard all remaining periodic tasks.
*
* @experimental
*/
export function discardPeriodicTasks(): void {
const zoneSpec = _getFakeAsyncZoneSpec();
const pendingTimers = zoneSpec.pendingPeriodicTimers;
zoneSpec.pendingPeriodicTimers.length = 0;
}

/**
* Flush any pending microtasks.
*
* @experimental
*/
export function flushMicrotasks(): void {
_getFakeAsyncZoneSpec().flushMicrotasks();
}
4 changes: 3 additions & 1 deletion lib/testing/zone-testing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ import '../zone-spec/sync-test';
import '../jasmine/jasmine';
import '../zone-spec/async-test';
import '../zone-spec/fake-async-test';
import './promise-testing';
import './promise-testing';
export * from './async-testing';
export * from './fake-async';
5 changes: 2 additions & 3 deletions lib/zone-spec/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
* 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
*/

class ProxyZoneSpec implements ZoneSpec {
name: string = 'ProxyZone';

Expand All @@ -28,7 +27,7 @@ class ProxyZoneSpec implements ZoneSpec {
}

static assertPresent(): ProxyZoneSpec {
if (!this.isLoaded()) {
if (!ProxyZoneSpec.isLoaded()) {
throw new Error(`Expected to be running in 'ProxyZone', but it was not found.`);
}
return ProxyZoneSpec.get();
Expand Down Expand Up @@ -69,7 +68,7 @@ class ProxyZoneSpec implements ZoneSpec {
// last delegateSpec has microTask or macroTask
// should call onHasTask in current delegateSpec
this.isNeedToTriggerHasTask = false;
this.onHasTask(parentZoneDelegate, currentZone, targetZone, this.lastTaskState);
this.onHasTask(parentZoneDelegate, currentZone, targetZone, this.lastTaskState);
}
}

Expand Down
Loading

0 comments on commit a4b42cd

Please sign in to comment.