From 1cba56e11fc647a424aecfe4ab3213dbf9ad00d2 Mon Sep 17 00:00:00 2001 From: JiaLiPassion Date: Thu, 2 Jul 2020 06:35:19 +0900 Subject: [PATCH] refactor(core): remove unused fakeAsyncFallback and asyncFallback (#37879) `zone.js` 0.8.25 introduces `zone-testing` bundle and move all `fakeAsync/async` logic from `@angular/core/testing` to `zone.js` package. But in case some user still using the old version of `zone.js`, an old version of `fakeAsync/async` logic were still kept inside `@angular/core/testing` package as `fallback` logic. Since now `Angular8+` already use `zone.js 0.9+`, so those fallback logic is removed. PR Close #37879 --- packages/core/test/render3/BUILD.bazel | 5 +- packages/core/test/render3/ivy/BUILD.bazel | 5 +- packages/core/test/render3/load_domino.ts | 1 - packages/core/testing/src/async.ts | 12 +- packages/core/testing/src/async_fallback.ts | 118 ------------- packages/core/testing/src/fake_async.ts | 24 ++- .../core/testing/src/fake_async_fallback.ts | 163 ------------------ packages/zone.js/lib/testing/async-testing.ts | 97 ----------- packages/zone.js/lib/testing/fake-async.ts | 153 ---------------- packages/zone.js/lib/zone-spec/async-test.ts | 97 +++++++++++ .../zone.js/lib/zone-spec/fake-async-test.ts | 161 +++++++++++++++++ packages/zone.js/lib/zone.ts | 9 +- tools/testing/init_node_spec.ts | 4 +- 13 files changed, 289 insertions(+), 560 deletions(-) delete mode 100644 packages/core/testing/src/async_fallback.ts delete mode 100644 packages/core/testing/src/fake_async_fallback.ts diff --git a/packages/core/test/render3/BUILD.bazel b/packages/core/test/render3/BUILD.bazel index 66855c2a1d..cce0a84309 100644 --- a/packages/core/test/render3/BUILD.bazel +++ b/packages/core/test/render3/BUILD.bazel @@ -76,7 +76,10 @@ ts_library( jasmine_node_test( name = "render3", - bootstrap = [":domino_es5"], + bootstrap = [ + ":domino_es5", + "//tools/testing:node_es5", + ], deps = [ ":render3_node_lib", "//packages/zone.js/lib", diff --git a/packages/core/test/render3/ivy/BUILD.bazel b/packages/core/test/render3/ivy/BUILD.bazel index 3982b9357a..190f2e52d5 100644 --- a/packages/core/test/render3/ivy/BUILD.bazel +++ b/packages/core/test/render3/ivy/BUILD.bazel @@ -16,7 +16,10 @@ ts_library( jasmine_node_test( name = "ivy", - bootstrap = ["//packages/core/test/render3:domino_es5"], + bootstrap = [ + "//packages/core/test/render3:domino_es5", + "//tools/testing:node_es5", + ], tags = [ "ivy-only", ], diff --git a/packages/core/test/render3/load_domino.ts b/packages/core/test/render3/load_domino.ts index ad8b549065..7a53b9a7dd 100644 --- a/packages/core/test/render3/load_domino.ts +++ b/packages/core/test/render3/load_domino.ts @@ -7,7 +7,6 @@ */ // Needed to run animation tests -import 'zone.js/lib/node/rollup-main'; import '@angular/compiler'; // For JIT mode. Must be in front of any other @angular/* imports. import {ɵgetDOM as getDOM} from '@angular/common'; import {DominoAdapter} from '@angular/platform-server/src/domino_adapter'; diff --git a/packages/core/testing/src/async.ts b/packages/core/testing/src/async.ts index eac783214c..a3ff66b3e2 100644 --- a/packages/core/testing/src/async.ts +++ b/packages/core/testing/src/async.ts @@ -5,9 +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 */ - -import {asyncFallback} from './async_fallback'; - /** * Wraps a test function in an asynchronous test zone. The test will automatically * complete when all asynchronous calls within this zone are done. Can be used @@ -38,10 +35,11 @@ export function waitForAsync(fn: Function): (done: any) => any { if (typeof asyncTest === 'function') { return asyncTest(fn); } - // not using new version of zone.js - // TODO @JiaLiPassion, remove this after all library updated to - // newest version of zone.js(0.8.25) - return asyncFallback(fn); + return function() { + return Promise.reject( + 'zone-testing.js is needed for the async() test helper but could not be found. ' + + 'Please make sure that your environment includes zone.js/dist/zone-testing.js'); + }; } /** diff --git a/packages/core/testing/src/async_fallback.ts b/packages/core/testing/src/async_fallback.ts deleted file mode 100644 index 10d3b47f78..0000000000 --- a/packages/core/testing/src/async_fallback.ts +++ /dev/null @@ -1,118 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 - */ - -/** - * async has been moved to zone.js - * this file is for fallback in case old version of zone.js is used - */ -declare var global: any; - -const _global = (typeof window === 'undefined' ? global : window); - -/** - * Wraps a test function in an asynchronous test zone. The test will automatically - * complete when all asynchronous calls within this zone are done. Can be used - * to wrap an {@link inject} call. - * - * Example: - * - * ``` - * it('...', async(inject([AClass], (object) => { - * object.doSomething.then(() => { - * expect(...); - * }) - * }); - * ``` - * - * - */ -export function asyncFallback(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(this: unknown, 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(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(this: unknown) { - return new Promise((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. - currentZone.run(() => { - if (proxyZoneSpec.getDelegate() == testZoneSpec) { - // Only reset the zone spec if it's sill this one. Otherwise, assume it's OK. - proxyZoneSpec.setDelegate(previousDelegate); - } - finishCallback(); - }); - }, - (error: any) => { - // Need to restore the original zone. - currentZone.run(() => { - if (proxyZoneSpec.getDelegate() == testZoneSpec) { - // Only reset the zone spec if it's sill this one. Otherwise, assume it's OK. - proxyZoneSpec.setDelegate(previousDelegate); - } - failCallback(error); - }); - }, - 'test'); - proxyZoneSpec.setDelegate(testZoneSpec); - }); - return Zone.current.runGuarded(fn, context); -} diff --git a/packages/core/testing/src/fake_async.ts b/packages/core/testing/src/fake_async.ts index 5050b9070b..1885f5418f 100644 --- a/packages/core/testing/src/fake_async.ts +++ b/packages/core/testing/src/fake_async.ts @@ -5,11 +5,13 @@ * 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 */ -import {discardPeriodicTasksFallback, fakeAsyncFallback, flushFallback, flushMicrotasksFallback, resetFakeAsyncZoneFallback, tickFallback} from './fake_async_fallback'; - const _Zone: any = typeof Zone !== 'undefined' ? Zone : null; const fakeAsyncTestModule = _Zone && _Zone[_Zone.__symbol__('fakeAsyncTest')]; +const fakeAsyncTestModuleNotLoadedErrorMessage = + `zone-testing.js is needed for the async() test helper but could not be found. + Please make sure that your environment includes zone.js/dist/zone-testing.js`; + /** * Clears out the shared fake async zone for a test. * To be called in a global `beforeEach`. @@ -19,9 +21,8 @@ const fakeAsyncTestModule = _Zone && _Zone[_Zone.__symbol__('fakeAsyncTest')]; export function resetFakeAsyncZone(): void { if (fakeAsyncTestModule) { return fakeAsyncTestModule.resetFakeAsyncZone(); - } else { - return resetFakeAsyncZoneFallback(); } + throw new Error(fakeAsyncTestModuleNotLoadedErrorMessage); } /** @@ -46,9 +47,8 @@ export function resetFakeAsyncZone(): void { export function fakeAsync(fn: Function): (...args: any[]) => any { if (fakeAsyncTestModule) { return fakeAsyncTestModule.fakeAsync(fn); - } else { - return fakeAsyncFallback(fn); } + throw new Error(fakeAsyncTestModuleNotLoadedErrorMessage); } /** @@ -108,9 +108,8 @@ export function tick( }): void { if (fakeAsyncTestModule) { return fakeAsyncTestModule.tick(millis, tickOptions); - } else { - return tickFallback(millis, tickOptions); } + throw new Error(fakeAsyncTestModuleNotLoadedErrorMessage); } /** @@ -126,9 +125,8 @@ export function tick( export function flush(maxTurns?: number): number { if (fakeAsyncTestModule) { return fakeAsyncTestModule.flush(maxTurns); - } else { - return flushFallback(maxTurns); } + throw new Error(fakeAsyncTestModuleNotLoadedErrorMessage); } /** @@ -139,9 +137,8 @@ export function flush(maxTurns?: number): number { export function discardPeriodicTasks(): void { if (fakeAsyncTestModule) { return fakeAsyncTestModule.discardPeriodicTasks(); - } else { - discardPeriodicTasksFallback(); } + throw new Error(fakeAsyncTestModuleNotLoadedErrorMessage); } /** @@ -152,7 +149,6 @@ export function discardPeriodicTasks(): void { export function flushMicrotasks(): void { if (fakeAsyncTestModule) { return fakeAsyncTestModule.flushMicrotasks(); - } else { - return flushMicrotasksFallback(); } + throw new Error(fakeAsyncTestModuleNotLoadedErrorMessage); } diff --git a/packages/core/testing/src/fake_async_fallback.ts b/packages/core/testing/src/fake_async_fallback.ts deleted file mode 100644 index 6026708ece..0000000000 --- a/packages/core/testing/src/fake_async_fallback.ts +++ /dev/null @@ -1,163 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 - */ - -/** - * fakeAsync has been moved to zone.js - * this file is for fallback in case old version of zone.js is used - */ -const _Zone: any = typeof Zone !== 'undefined' ? Zone : null; -const FakeAsyncTestZoneSpec = _Zone && _Zone['FakeAsyncTestZoneSpec']; -type ProxyZoneSpec = { - setDelegate(delegateSpec: ZoneSpec): void; getDelegate(): ZoneSpec; resetDelegate(): void; -}; -const ProxyZoneSpec: {get(): ProxyZoneSpec; assertPresent: () => ProxyZoneSpec} = - _Zone && _Zone['ProxyZoneSpec']; - -let _fakeAsyncTestZoneSpec: any = null; - -/** - * Clears out the shared fake async zone for a test. - * To be called in a global `beforeEach`. - * - * @publicApi - */ -export function resetFakeAsyncZoneFallback() { - if (_fakeAsyncTestZoneSpec) { - _fakeAsyncTestZoneSpec.unlockDatePatch(); - } - _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. - * - * @usageNotes - * ### Example - * - * {@example core/testing/ts/fake_async.ts region='basic'} - * - * @param fn - * @returns The function wrapped to be executed in the fakeAsync zone - * - * @publicApi - */ -export function fakeAsyncFallback(fn: Function): (...args: any[]) => any { - // Not using an arrow function to preserve context passed from call site - return function(this: unknown, ...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); - _fakeAsyncTestZoneSpec.lockDatePatch(); - try { - res = fn.apply(this, args); - flushMicrotasksFallback(); - } 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; - resetFakeAsyncZoneFallback(); - } - }; -} - -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. - * - * @usageNotes - * ### Example - * - * {@example core/testing/ts/fake_async.ts region='basic'} - * - * @publicApi - */ -export function tickFallback( - millis: number = 0, tickOptions: {processNewMacroTasksSynchronously: boolean} = { - processNewMacroTasksSynchronously: true - }): void { - _getFakeAsyncZoneSpec().tick(millis, null, tickOptions); -} - -/** - * 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. - * - * @publicApi - */ -export function flushFallback(maxTurns?: number): number { - return _getFakeAsyncZoneSpec().flush(maxTurns); -} - -/** - * Discard all remaining periodic tasks. - * - * @publicApi - */ -export function discardPeriodicTasksFallback(): void { - const zoneSpec = _getFakeAsyncZoneSpec(); - zoneSpec.pendingPeriodicTimers.length = 0; -} - -/** - * Flush any pending microtasks. - * - * @publicApi - */ -export function flushMicrotasksFallback(): void { - _getFakeAsyncZoneSpec().flushMicrotasks(); -} diff --git a/packages/zone.js/lib/testing/async-testing.ts b/packages/zone.js/lib/testing/async-testing.ts index 50cdc3f4bb..ac804808cd 100644 --- a/packages/zone.js/lib/testing/async-testing.ts +++ b/packages/zone.js/lib/testing/async-testing.ts @@ -6,100 +6,3 @@ * found in the LICENSE file at https://angular.io/license */ import '../zone-spec/async-test'; - -Zone.__load_patch('asynctest', (global: any, Zone: ZoneType, api: _ZonePrivate) => { - /** - * Wraps a test function in an asynchronous test zone. The test will automatically - * complete when all asynchronous calls within this zone are done. - */ - (Zone as any)[api.symbol('asyncTest')] = 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(this: unknown, 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(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(this: unknown) { - return new Promise((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); - } - (testZoneSpec as any).unPatchPromiseForTest(); - 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); - } - (testZoneSpec as any).unPatchPromiseForTest(); - currentZone.run(() => { - failCallback(error); - }); - }, - 'test'); - proxyZoneSpec.setDelegate(testZoneSpec); - (testZoneSpec as any).patchPromiseForTest(); - }); - return Zone.current.runGuarded(fn, context); - } -}); diff --git a/packages/zone.js/lib/testing/fake-async.ts b/packages/zone.js/lib/testing/fake-async.ts index 29b22b5b5b..acd661e852 100644 --- a/packages/zone.js/lib/testing/fake-async.ts +++ b/packages/zone.js/lib/testing/fake-async.ts @@ -6,156 +6,3 @@ * found in the LICENSE file at https://angular.io/license */ import '../zone-spec/fake-async-test'; - -Zone.__load_patch('fakeasync', (global: any, Zone: ZoneType, api: _ZonePrivate) => { - 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 - */ - function resetFakeAsyncZone() { - if (_fakeAsyncTestZoneSpec) { - _fakeAsyncTestZoneSpec.unlockDatePatch(); - } - _fakeAsyncTestZoneSpec = null; - // in node.js testing we may not have ProxyZoneSpec in which case there is nothing to reset. - ProxyZoneSpec && ProxyZoneSpec.assertPresent().resetDelegate(); - } - - /** - * 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 - */ - function fakeAsync(fn: Function): (...args: any[]) => any { - // Not using an arrow function to preserve context passed from call site - const fakeAsyncFn: any = function(this: unknown, ...args: any[]) { - const proxyZoneSpec = ProxyZoneSpec.assertPresent(); - if (Zone.current.get('FakeAsyncTestZoneSpec')) { - throw new Error('fakeAsync() calls can not be nested'); - } - try { - // in case jasmine.clock init a fakeAsyncTestZoneSpec - 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); - _fakeAsyncTestZoneSpec.lockDatePatch(); - 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 { - resetFakeAsyncZone(); - } - }; - (fakeAsyncFn as any).isFakeAsync = true; - return fakeAsyncFn; - } - - function _getFakeAsyncZoneSpec(): any { - if (_fakeAsyncTestZoneSpec == null) { - _fakeAsyncTestZoneSpec = Zone.current.get('FakeAsyncTestZoneSpec'); - 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 - */ - function tick(millis: number = 0, ignoreNestedTimeout = false): void { - _getFakeAsyncZoneSpec().tick(millis, null, ignoreNestedTimeout); - } - - /** - * 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 - */ - function flush(maxTurns?: number): number { - return _getFakeAsyncZoneSpec().flush(maxTurns); - } - - /** - * Discard all remaining periodic tasks. - * - * @experimental - */ - function discardPeriodicTasks(): void { - const zoneSpec = _getFakeAsyncZoneSpec(); - const pendingTimers = zoneSpec.pendingPeriodicTimers; - zoneSpec.pendingPeriodicTimers.length = 0; - } - - /** - * Flush any pending microtasks. - * - * @experimental - */ - function flushMicrotasks(): void { - _getFakeAsyncZoneSpec().flushMicrotasks(); - } - (Zone as any)[api.symbol('fakeAsyncTest')] = - {resetFakeAsyncZone, flushMicrotasks, discardPeriodicTasks, tick, flush, fakeAsync}; -}); diff --git a/packages/zone.js/lib/zone-spec/async-test.ts b/packages/zone.js/lib/zone-spec/async-test.ts index 5c559787fb..11cfeac7ae 100644 --- a/packages/zone.js/lib/zone-spec/async-test.ts +++ b/packages/zone.js/lib/zone-spec/async-test.ts @@ -147,3 +147,100 @@ class AsyncTestZoneSpec implements ZoneSpec { // constructor params. (Zone as any)['AsyncTestZoneSpec'] = AsyncTestZoneSpec; })(typeof window !== 'undefined' && window || typeof self !== 'undefined' && self || global); + +Zone.__load_patch('asynctest', (global: any, Zone: ZoneType, api: _ZonePrivate) => { + /** + * Wraps a test function in an asynchronous test zone. The test will automatically + * complete when all asynchronous calls within this zone are done. + */ + (Zone as any)[api.symbol('asyncTest')] = 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(this: unknown, 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(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(this: unknown) { + return new Promise((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) { + 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); + } + (testZoneSpec as any).unPatchPromiseForTest(); + 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); + } + (testZoneSpec as any).unPatchPromiseForTest(); + currentZone.run(() => { + failCallback(error); + }); + }, + 'test'); + proxyZoneSpec.setDelegate(testZoneSpec); + (testZoneSpec as any).patchPromiseForTest(); + }); + return Zone.current.runGuarded(fn, context); + } +}); diff --git a/packages/zone.js/lib/zone-spec/fake-async-test.ts b/packages/zone.js/lib/zone-spec/fake-async-test.ts index b7ef385c30..0872189674 100644 --- a/packages/zone.js/lib/zone-spec/fake-async-test.ts +++ b/packages/zone.js/lib/zone-spec/fake-async-test.ts @@ -703,3 +703,164 @@ class FakeAsyncTestZoneSpec implements ZoneSpec { // constructor params. (Zone as any)['FakeAsyncTestZoneSpec'] = FakeAsyncTestZoneSpec; })(typeof window === 'object' && window || typeof self === 'object' && self || global); + +Zone.__load_patch('fakeasync', (global: any, Zone: ZoneType, api: _ZonePrivate) => { + const FakeAsyncTestZoneSpec = Zone && (Zone as any)['FakeAsyncTestZoneSpec']; + type ProxyZoneSpecType = { + setDelegate(delegateSpec: ZoneSpec): void; getDelegate(): ZoneSpec; resetDelegate(): void; + }; + + function getProxyZoneSpec(): {get(): ProxyZoneSpecType; assertPresent: () => ProxyZoneSpecType} { + return 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 + */ + function resetFakeAsyncZone() { + if (_fakeAsyncTestZoneSpec) { + _fakeAsyncTestZoneSpec.unlockDatePatch(); + } + _fakeAsyncTestZoneSpec = null; + // in node.js testing we may not have ProxyZoneSpec in which case there is nothing to reset. + getProxyZoneSpec() && getProxyZoneSpec().assertPresent().resetDelegate(); + } + + /** + * 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 + */ + function fakeAsync(fn: Function): (...args: any[]) => any { + // Not using an arrow function to preserve context passed from call site + const fakeAsyncFn: any = function(this: unknown, ...args: any[]) { + const ProxyZoneSpec = getProxyZoneSpec(); + if (!ProxyZoneSpec) { + 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.assertPresent(); + if (Zone.current.get('FakeAsyncTestZoneSpec')) { + throw new Error('fakeAsync() calls can not be nested'); + } + try { + // in case jasmine.clock init a fakeAsyncTestZoneSpec + 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); + _fakeAsyncTestZoneSpec.lockDatePatch(); + 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 { + resetFakeAsyncZone(); + } + }; + (fakeAsyncFn as any).isFakeAsync = true; + return fakeAsyncFn; + } + + function _getFakeAsyncZoneSpec(): any { + if (_fakeAsyncTestZoneSpec == null) { + _fakeAsyncTestZoneSpec = Zone.current.get('FakeAsyncTestZoneSpec'); + 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 + */ + function tick(millis: number = 0, ignoreNestedTimeout = false): void { + _getFakeAsyncZoneSpec().tick(millis, null, ignoreNestedTimeout); + } + + /** + * 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 + */ + function flush(maxTurns?: number): number { + return _getFakeAsyncZoneSpec().flush(maxTurns); + } + + /** + * Discard all remaining periodic tasks. + * + * @experimental + */ + function discardPeriodicTasks(): void { + const zoneSpec = _getFakeAsyncZoneSpec(); + const pendingTimers = zoneSpec.pendingPeriodicTimers; + zoneSpec.pendingPeriodicTimers.length = 0; + } + + /** + * Flush any pending microtasks. + * + * @experimental + */ + function flushMicrotasks(): void { + _getFakeAsyncZoneSpec().flushMicrotasks(); + } + (Zone as any)[api.symbol('fakeAsyncTest')] = + {resetFakeAsyncZone, flushMicrotasks, discardPeriodicTasks, tick, flush, fakeAsync}; +}, true); diff --git a/packages/zone.js/lib/zone.ts b/packages/zone.js/lib/zone.ts index 0c52fec4ad..92bd2f2bbf 100644 --- a/packages/zone.js/lib/zone.ts +++ b/packages/zone.js/lib/zone.ts @@ -313,7 +313,7 @@ interface ZoneType { * load patch for specified native module, allow user to * define their own patch, user can use this API after loading zone.js */ - __load_patch(name: string, fn: _PatchFn): void; + __load_patch(name: string, fn: _PatchFn, ignoreDuplicate?: boolean): void; /** * Zone symbol API to generate a string with __zone_symbol__ prefix @@ -745,9 +745,12 @@ const Zone: ZoneType = (function(global: any) { } // tslint:disable-next-line:require-internal-with-underscore - static __load_patch(name: string, fn: _PatchFn): void { + static __load_patch(name: string, fn: _PatchFn, ignoreDuplicate = false): void { if (patches.hasOwnProperty(name)) { - if (checkDuplicate) { + // `checkDuplicate` option is defined from global variable + // so it works for all modules. + // `ignoreDuplicate` can work for the specified module + if (!ignoreDuplicate && checkDuplicate) { throw Error('Already loaded patch: ' + name); } } else if (!global['__Zone_disable_' + name]) { diff --git a/tools/testing/init_node_spec.ts b/tools/testing/init_node_spec.ts index 3cd708ccec..f78cf3a9aa 100644 --- a/tools/testing/init_node_spec.ts +++ b/tools/testing/init_node_spec.ts @@ -11,8 +11,8 @@ import 'zone.js/lib/zone-spec/long-stack-trace'; import 'zone.js/lib/zone-spec/task-tracking'; import 'zone.js/lib/zone-spec/proxy'; import 'zone.js/lib/zone-spec/sync-test'; -import 'zone.js/lib/zone-spec/async-test'; -import 'zone.js/lib/zone-spec/fake-async-test'; +import 'zone.js/lib/testing/async-testing'; +import 'zone.js/lib/testing/fake-async'; import 'reflect-metadata/Reflect'; // Initialize jasmine with @bazel/jasmine boot() function. This will initialize