From f22a69afa9029d1dea294fce81e6244a4f592a16 Mon Sep 17 00:00:00 2001 From: "JiaLi.Passion" Date: Thu, 15 Mar 2018 00:15:48 +0900 Subject: [PATCH] feat(test): move async/fakeAsync from angular to zone.js --- gulpfile.js | 23 +- karma-dist.conf.js | 3 +- lib/testing/async-testing.ts | 102 +++++ lib/testing/fake-async.ts | 150 +++++++ lib/testing/zone-testing.ts | 4 +- lib/zone-spec/proxy.ts | 5 +- sauce.conf.js | 112 ++---- test/zone-spec/async-test.spec.ts | 361 +++++++---------- test/zone-spec/fake-async-test.spec.ts | 520 ++++++++++++++++++++++--- test/zone-spec/proxy.spec.ts | 16 +- test/zone-spec/sync-test.spec.ts | 1 - test/zone-spec/task-tracking.spec.ts | 2 - 12 files changed, 922 insertions(+), 377 deletions(-) create mode 100644 lib/testing/async-testing.ts create mode 100644 lib/testing/fake-async.ts diff --git a/gulpfile.js b/gulpfile.js index 2bab13d6f..f870a0929 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -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' + @@ -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) { @@ -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) { @@ -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() { diff --git a/karma-dist.conf.js b/karma-dist.conf.js index a96fe0993..c7946e144 100644 --- a/karma-dist.conf.js +++ b/karma-dist.conf.js @@ -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'); diff --git a/lib/testing/async-testing.ts b/lib/testing/async-testing.ts new file mode 100644 index 000000000..6ad4f608e --- /dev/null +++ b/lib/testing/async-testing.ts @@ -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(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((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); +} diff --git a/lib/testing/fake-async.ts b/lib/testing/fake-async.ts new file mode 100644 index 000000000..7690f26aa --- /dev/null +++ b/lib/testing/fake-async.ts @@ -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(); +} diff --git a/lib/testing/zone-testing.ts b/lib/testing/zone-testing.ts index 51218110e..c566694b9 100644 --- a/lib/testing/zone-testing.ts +++ b/lib/testing/zone-testing.ts @@ -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'; \ No newline at end of file +import './promise-testing'; +export * from './async-testing'; +export * from './fake-async'; \ No newline at end of file diff --git a/lib/zone-spec/proxy.ts b/lib/zone-spec/proxy.ts index 81c3110f3..30e11dc19 100644 --- a/lib/zone-spec/proxy.ts +++ b/lib/zone-spec/proxy.ts @@ -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'; @@ -26,7 +25,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(); @@ -68,7 +67,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); } } diff --git a/sauce.conf.js b/sauce.conf.js index a6026ad50..1aac560bd 100644 --- a/sauce.conf.js +++ b/sauce.conf.js @@ -1,54 +1,26 @@ // Sauce configuration -module.exports = function (config, ignoredLaunchers) { +module.exports = function(config, ignoredLaunchers) { // The WS server is not available with Sauce config.files.unshift('test/saucelabs.js'); var basicLaunchers = { - 'SL_CHROME': { - base: 'SauceLabs', - browserName: 'chrome', - version: '48' - }, - 'SL_CHROME_60': { - base: 'SauceLabs', - browserName: 'chrome', - version: '60' - }, - 'SL_FIREFOX': { - base: 'SauceLabs', - browserName: 'firefox', - version: '52' - }, - 'SL_FIREFOX_54': { - base: 'SauceLabs', - browserName: 'firefox', - version: '54' - }, + 'SL_CHROME': {base: 'SauceLabs', browserName: 'chrome', version: '48'}, + 'SL_CHROME_60': {base: 'SauceLabs', browserName: 'chrome', version: '60'}, + 'SL_FIREFOX': {base: 'SauceLabs', browserName: 'firefox', version: '52'}, + 'SL_FIREFOX_54': {base: 'SauceLabs', browserName: 'firefox', version: '54'}, /*'SL_SAFARI7': { base: 'SauceLabs', browserName: 'safari', platform: 'OS X 10.9', version: '7.0' },*/ - 'SL_SAFARI8': { - base: 'SauceLabs', - browserName: 'safari', - platform: 'OS X 10.10', - version: '8.0' - }, - 'SL_SAFARI9': { - base: 'SauceLabs', - browserName: 'safari', - platform: 'OS X 10.11', - version: '9.0' - }, - 'SL_SAFARI10': { - base: 'SauceLabs', - browserName: 'safari', - platform: 'OS X 10.11', - version: '10.0' - }, + 'SL_SAFARI8': + {base: 'SauceLabs', browserName: 'safari', platform: 'OS X 10.10', version: '8.0'}, + 'SL_SAFARI9': + {base: 'SauceLabs', browserName: 'safari', platform: 'OS X 10.11', version: '9.0'}, + 'SL_SAFARI10': + {base: 'SauceLabs', browserName: 'safari', platform: 'OS X 10.11', version: '10.0'}, /* no longer supported in SauceLabs 'SL_IOS7': { @@ -57,24 +29,14 @@ module.exports = function (config, ignoredLaunchers) { platform: 'OS X 10.10', version: '7.1' },*/ - 'SL_IOS8': { + /*'SL_IOS8': { base: 'SauceLabs', browserName: 'iphone', platform: 'OS X 10.10', version: '8.4' - }, - 'SL_IOS9': { - base: 'SauceLabs', - browserName: 'iphone', - platform: 'OS X 10.10', - version: '9.3' - }, - 'SL_IOS10': { - base: 'SauceLabs', - browserName: 'iphone', - platform: 'OS X 10.10', - version: '10.2' - }, + },*/ + 'SL_IOS9': {base: 'SauceLabs', browserName: 'iphone', platform: 'OS X 10.10', version: '9.3'}, + 'SL_IOS10': {base: 'SauceLabs', browserName: 'iphone', platform: 'OS X 10.10', version: '10.2'}, 'SL_IE9': { base: 'SauceLabs', browserName: 'internet explorer', @@ -125,24 +87,9 @@ module.exports = function (config, ignoredLaunchers) { platform: 'Linux', version: '4.3' },*/ - 'SL_ANDROID4.4': { - base: 'SauceLabs', - browserName: 'android', - platform: 'Linux', - version: '4.4' - }, - 'SL_ANDROID5.1': { - base: 'SauceLabs', - browserName: 'android', - platform: 'Linux', - version: '5.1' - }, - 'SL_ANDROID6.0': { - base: 'SauceLabs', - browserName: 'android', - platform: 'Linux', - version: '6.0' - }, + 'SL_ANDROID4.4': {base: 'SauceLabs', browserName: 'android', platform: 'Linux', version: '4.4'}, + 'SL_ANDROID5.1': {base: 'SauceLabs', browserName: 'android', platform: 'Linux', version: '5.1'}, + 'SL_ANDROID6.0': {base: 'SauceLabs', browserName: 'android', platform: 'Linux', version: '6.0'}, 'SL_ANDROID7.1': { base: 'SauceLabs', browserName: 'Chrome', @@ -158,7 +105,11 @@ module.exports = function (config, ignoredLaunchers) { customLaunchers = basicLaunchers; } else { Object.keys(basicLaunchers).forEach(function(key) { - if (ignoredLaunchers.filter(function(ignore) {return ignore === key;}).length === 0) { + if (ignoredLaunchers + .filter(function(ignore) { + return ignore === key; + }) + .length === 0) { customLaunchers[key] = basicLaunchers[key]; } }); @@ -173,11 +124,11 @@ module.exports = function (config, ignoredLaunchers) { startConnect: false, recordVideo: false, recordScreenshots: false, - options: { - 'selenium-version': '2.53.0', - 'command-timeout': 600, - 'idle-timeout': 600, - 'max-duration': 5400 + options: { + 'selenium-version': '2.53.0', + 'command-timeout': 600, + 'idle-timeout': 600, + 'max-duration': 5400 } }, @@ -189,13 +140,12 @@ module.exports = function (config, ignoredLaunchers) { singleRun: true, - plugins: [ - 'karma-*' - ] + plugins: ['karma-*'] }); if (process.env.TRAVIS) { - config.sauceLabs.build = 'TRAVIS #' + process.env.TRAVIS_BUILD_NUMBER + ' (' + process.env.TRAVIS_BUILD_ID + ')'; + config.sauceLabs.build = + 'TRAVIS #' + process.env.TRAVIS_BUILD_NUMBER + ' (' + process.env.TRAVIS_BUILD_ID + ')'; config.sauceLabs.tunnelIdentifier = process.env.TRAVIS_JOB_NUMBER; process.env.SAUCE_ACCESS_KEY = process.env.SAUCE_ACCESS_KEY.split('').reverse().join(''); diff --git a/test/zone-spec/async-test.spec.ts b/test/zone-spec/async-test.spec.ts index 98610da00..95918ff25 100644 --- a/test/zone-spec/async-test.spec.ts +++ b/test/zone-spec/async-test.spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import '../../lib/zone-spec/async-test'; +import {asyncTest} from '../../lib/testing/async-testing'; import {ifEnvSupports} from '../test-util'; describe('AsyncTestZoneSpec', function() { @@ -345,268 +345,189 @@ describe('AsyncTestZoneSpec', function() { }); }); - describe('ProxyZone with AsyncTestZoneSpec', () => { - const ProxyZoneSpec = (Zone as any)['ProxyZoneSpec']; - const AsyncTestZoneSpec = (Zone as any)['AsyncTestZoneSpec']; - - function testAsync(fn: Function, doneFn?: Function) { - return function(done: any) { - runInTestZone(fn, this, function() { - if (doneFn) { - doneFn(); - } - done(); - }, (err: any) => { - if (typeof err === 'string') { - return done.fail(new Error(err)); - } else { - done.fail(err); - } - }); - }; - } - - function runInTestZone( - fn: Function, context: any, finishCallback: Function, failCallback: Function) { - const currentZone = Zone.current; - 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); - } + function wrapAsyncTest(fn: Function, doneFn?: Function) { + return function(done: Function) { + const asyncWrapper = asyncTest(fn); + return asyncWrapper.apply(this, [function() { + if (doneFn) { + doneFn(); + } + return done.apply(this, arguments); + }]); + }; + } + describe('async', () => { describe('test without beforeEach', () => { const logs: string[] = []; - it('should automatically done after async tasks finished', testAsync(() => { - setTimeout(() => { - logs.push('timeout'); - }, 100); - }, () => { - expect(logs).toEqual(['timeout']); - logs.splice(0); - })); - - it('should automatically done after all nested async tasks finished', testAsync(() => { - setTimeout(() => { - logs.push('timeout'); - setTimeout(() => { - logs.push('nested timeout'); - }, 100); - }, 100); - }, () => { - expect(logs).toEqual(['timeout', 'nested timeout']); - logs.splice(0); - })); - - it('should automatically done after multiple async tasks finished', testAsync(() => { - setTimeout(() => { - logs.push('1st timeout'); - }, 100); - - setTimeout(() => { - logs.push('2nd timeout'); - }, 100); - }, () => { - expect(logs).toEqual(['1st timeout', '2nd timeout']); - logs.splice(0); - })); + it('should automatically done after async tasks finished', + wrapAsyncTest( + () => { + setTimeout(() => { + logs.push('timeout'); + }, 100); + }, + () => { + expect(logs).toEqual(['timeout']); + logs.splice(0); + })); + + it('should automatically done after all nested async tasks finished', + wrapAsyncTest( + () => { + setTimeout(() => { + logs.push('timeout'); + setTimeout(() => { + logs.push('nested timeout'); + }, 100); + }, 100); + }, + () => { + expect(logs).toEqual(['timeout', 'nested timeout']); + logs.splice(0); + })); + + it('should automatically done after multiple async tasks finished', + wrapAsyncTest( + () => { + setTimeout(() => { + logs.push('1st timeout'); + }, 100); + + setTimeout(() => { + logs.push('2nd timeout'); + }, 100); + }, + () => { + expect(logs).toEqual(['1st timeout', '2nd timeout']); + logs.splice(0); + })); }); - + describe('test with sync beforeEach', () => { const logs: string[] = []; - + beforeEach(() => { logs.splice(0); logs.push('beforeEach'); }); - - it('should automatically done after async tasks finished', testAsync(() => { - setTimeout(() => { - logs.push('timeout'); - }, 100); - }, () => { - expect(logs).toEqual(['beforeEach', 'timeout']); - })); + + it('should automatically done after async tasks finished', + wrapAsyncTest( + () => { + setTimeout(() => { + logs.push('timeout'); + }, 100); + }, + () => { + expect(logs).toEqual(['beforeEach', 'timeout']); + })); }); - + describe('test with async beforeEach', () => { const logs: string[] = []; - - beforeEach(testAsync(() => { + + beforeEach(wrapAsyncTest(() => { setTimeout(() => { logs.splice(0); logs.push('beforeEach'); }, 100); })); - - it('should automatically done after async tasks finished', testAsync(() => { - setTimeout(() => { - logs.push('timeout'); - }, 100); - }, () => { - expect(logs).toEqual(['beforeEach', 'timeout']); - })); - - it('should automatically done after all nested async tasks finished', testAsync(() => { - setTimeout(() => { - logs.push('timeout'); - setTimeout(() => { - logs.push('nested timeout'); - }, 100); - }, 100); - }, () => { - expect(logs).toEqual(['beforeEach', 'timeout', 'nested timeout']); - })); - - it('should automatically done after multiple async tasks finished', testAsync(() => { - setTimeout(() => { - logs.push('1st timeout'); - }, 100); - - setTimeout(() => { - logs.push('2nd timeout'); - }, 100); - }, () => { - expect(logs).toEqual(['beforeEach', '1st timeout', '2nd timeout']); - })); + + it('should automatically done after async tasks finished', + wrapAsyncTest( + () => { + setTimeout(() => { + logs.push('timeout'); + }, 100); + }, + () => { + expect(logs).toEqual(['beforeEach', 'timeout']); + })); + + it('should automatically done after all nested async tasks finished', + wrapAsyncTest( + () => { + setTimeout(() => { + logs.push('timeout'); + setTimeout(() => { + logs.push('nested timeout'); + }, 100); + }, 100); + }, + () => { + expect(logs).toEqual(['beforeEach', 'timeout', 'nested timeout']); + })); + + it('should automatically done after multiple async tasks finished', + wrapAsyncTest( + () => { + setTimeout(() => { + logs.push('1st timeout'); + }, 100); + + setTimeout(() => { + logs.push('2nd timeout'); + }, 100); + }, + () => { + expect(logs).toEqual(['beforeEach', '1st timeout', '2nd timeout']); + })); }); - + describe('test with async beforeEach and sync afterEach', () => { const logs: string[] = []; - - beforeEach(testAsync(() => { + + beforeEach(wrapAsyncTest(() => { setTimeout(() => { expect(logs).toEqual([]); logs.push('beforeEach'); }, 100); })); - + afterEach(() => { logs.splice(0); }); - - it('should automatically done after async tasks finished', testAsync(() => { - setTimeout(() => { - logs.push('timeout'); - }, 100); - }, () => { - expect(logs).toEqual(['beforeEach', 'timeout']); - })); + + it('should automatically done after async tasks finished', + wrapAsyncTest( + () => { + setTimeout(() => { + logs.push('timeout'); + }, 100); + }, + () => { + expect(logs).toEqual(['beforeEach', 'timeout']); + })); }); - + describe('test with async beforeEach and async afterEach', () => { const logs: string[] = []; - - beforeEach(testAsync(() => { + + beforeEach(wrapAsyncTest(() => { setTimeout(() => { expect(logs).toEqual([]); logs.push('beforeEach'); }, 100); })); - - afterEach(testAsync(() => { + + afterEach(wrapAsyncTest(() => { setTimeout(() => { logs.splice(0); }, 100); })); - - it('should automatically done after async tasks finished', testAsync(() => { - setTimeout(() => { - logs.push('timeout'); - }, 100); - }, () => { - expect(logs).toEqual(['beforeEach', 'timeout']); - })); - }); - describe('return promise', () => { - let value = 'init'; - it('should only call finish once', testAsync(() => { - return new Promise((resolve, _) => { - setTimeout(() => { - value = 'timeout'; - resolve(); - }, 100); - }); - }, () => { - expect(value).toEqual('timeout'); - })); + it('should automatically done after async tasks finished', + wrapAsyncTest( + () => { + setTimeout(() => { + logs.push('timeout'); + }, 100); + }, + () => { + expect(logs).toEqual(['beforeEach', 'timeout']); + })); }); }); - describe('should be able to handle async for both beforeEach and it', () => { - let log: string[]; - const AsyncTestZoneSpec = (Zone as any)['AsyncTestZoneSpec']; - - function asyncTest(testBody: () => void, finishCallback: Function, failCallback: Function) { - return function() { - const proxyZoneSpec = Zone.current.get('ProxyZoneSpec'); - if (!proxyZoneSpec) { - throw new Error('ProxyZone not found!'); - } - const lastDelegate = proxyZoneSpec.getDelegate(); - // construct AsyncTestZoneSpec in parent zone - // to prevent infinite loop - Zone.current.parent.run(() => { - proxyZoneSpec.setDelegate(new AsyncTestZoneSpec(() => { - proxyZoneSpec.setDelegate(lastDelegate); - finishCallback(); - }, () => { - proxyZoneSpec.setDelegate(lastDelegate); - failCallback(); - }), 'async'); - }); - testBody.apply(this, arguments); - }; - } - - beforeEach(asyncTest(() => { - log = []; - setTimeout(() => { - log.push('beforeEach'); - }, 50); - }, () => { - expect(log).toEqual(['beforeEach']); - }, () => { - fail('should not fail'); - })); - - it('should support asyncTest with an async beforeEach', asyncTest(() => { - setTimeout(() => { - log.push('timeout'); - }, 50); - }, () => { - expect(log).toEqual(['beforeEach', 'timeout']); - }, () => { - fail('should not fail'); - })); - }); }); diff --git a/test/zone-spec/fake-async-test.spec.ts b/test/zone-spec/fake-async-test.spec.ts index 9889cff39..f1e23abb8 100644 --- a/test/zone-spec/fake-async-test.spec.ts +++ b/test/zone-spec/fake-async-test.spec.ts @@ -6,13 +6,14 @@ * found in the LICENSE file at https://angular.io/license */ -import '../../lib/zone-spec/fake-async-test'; +import 'rxjs/add/operator/delay'; +import '../../lib/rxjs/rxjs-fake-async'; + +import {Observable} from 'rxjs/Observable'; import {isNode, patchMacroTask} from '../../lib/common/utils'; +import {discardPeriodicTasks, fakeAsync, flush, flushMicrotasks, tick} from '../../lib/testing/fake-async'; import {ifEnvSupports} from '../test-util'; -import {Observable} from 'rxjs/Observable'; -import 'rxjs/add/operator/delay'; -import '../../lib/rxjs/rxjs-fake-async'; function supportNode() { return isNode; @@ -212,7 +213,7 @@ describe('FakeAsyncTestZoneSpec', () => { let id = setTimeout((arg1, arg2) => { value = arg1 + arg2; }, 0, 'expected', ' value'); - + testZoneSpec.tick(); expect(value).toEqual('expected value'); }); @@ -245,7 +246,7 @@ describe('FakeAsyncTestZoneSpec', () => { let id = setInterval((arg1, arg2) => { value = arg1 + arg2; }, 10, 'expected', ' value'); - + testZoneSpec.tick(10); expect(value).toEqual('expected value'); }); @@ -859,8 +860,7 @@ describe('FakeAsyncTestZoneSpec', () => { let fakeAsyncTestZone: Zone; beforeEach(() => { - testZoneSpec = new FakeAsyncTestZoneSpec( - 'name', false); + testZoneSpec = new FakeAsyncTestZoneSpec('name', false); fakeAsyncTestZone = Zone.current.fork(testZoneSpec); }); @@ -874,46 +874,50 @@ describe('FakeAsyncTestZoneSpec', () => { }); }); - describe('fakeAsyncTest should patch jasmine.clock', ifEnvSupports(() => { - return typeof jasmine.clock === 'function'; - }, () => { - beforeEach(() => { - jasmine.clock().install(); - }); + describe( + 'fakeAsyncTest should patch jasmine.clock', + ifEnvSupports( + () => { + return typeof jasmine.clock === 'function'; + }, + () => { + beforeEach(() => { + jasmine.clock().install(); + }); - afterEach(() => { - jasmine.clock().uninstall(); - }); + afterEach(() => { + jasmine.clock().uninstall(); + }); - it('should get date diff correctly', () => { - const start = Date.now(); - jasmine.clock().tick(100); - const end = Date.now(); - expect(end - start).toBe(100); - }); - - it('should mock date correctly', () => { - const baseTime = new Date(2013, 9, 23); - jasmine.clock().mockDate(baseTime); - const start = Date.now(); - expect(start).toBe(baseTime.getTime()); - jasmine.clock().tick(100); - const end = Date.now(); - expect(end - start).toBe(100); - expect(end).toBe(baseTime.getTime() + 100); - }); - - it('should handle new Date correctly', () => { - const baseTime = new Date(2013, 9, 23); - jasmine.clock().mockDate(baseTime); - const start = new Date(); - expect(start.getTime()).toBe(baseTime.getTime()); - jasmine.clock().tick(100); - const end = new Date(); - expect(end.getTime() - start.getTime()).toBe(100); - expect(end.getTime()).toBe(baseTime.getTime() + 100); - }); - })); + it('should get date diff correctly', () => { + const start = Date.now(); + jasmine.clock().tick(100); + const end = Date.now(); + expect(end - start).toBe(100); + }); + + it('should mock date correctly', () => { + const baseTime = new Date(2013, 9, 23); + jasmine.clock().mockDate(baseTime); + const start = Date.now(); + expect(start).toBe(baseTime.getTime()); + jasmine.clock().tick(100); + const end = Date.now(); + expect(end - start).toBe(100); + expect(end).toBe(baseTime.getTime() + 100); + }); + + it('should handle new Date correctly', () => { + const baseTime = new Date(2013, 9, 23); + jasmine.clock().mockDate(baseTime); + const start = new Date(); + expect(start.getTime()).toBe(baseTime.getTime()); + jasmine.clock().tick(100); + const end = new Date(); + expect(end.getTime() - start.getTime()).toBe(100); + expect(end.getTime()).toBe(baseTime.getTime() + 100); + }); + })); describe('fakeAsyncTest should patch rxjs scheduler', () => { let FakeAsyncTestZoneSpec = (Zone as any)['FakeAsyncTestZoneSpec']; @@ -921,8 +925,7 @@ describe('FakeAsyncTestZoneSpec', () => { let fakeAsyncTestZone: Zone; beforeEach(() => { - testZoneSpec = new FakeAsyncTestZoneSpec( - 'name', false); + testZoneSpec = new FakeAsyncTestZoneSpec('name', false); fakeAsyncTestZone = Zone.current.fork(testZoneSpec); }); @@ -944,3 +947,424 @@ describe('FakeAsyncTestZoneSpec', () => { }); }); }); + +class Log { + logItems: any[]; + + constructor() { + this.logItems = []; + } + + add(value: any /** TODO #9100 */): void { + this.logItems.push(value); + } + + fn(value: any /** TODO #9100 */) { + return (a1: any = null, a2: any = null, a3: any = null, a4: any = null, a5: any = null) => { + this.logItems.push(value); + }; + } + + clear(): void { + this.logItems = []; + } + + result(): string { + return this.logItems.join('; '); + } +} + +const resolvedPromise = Promise.resolve(null); +const ProxyZoneSpec: {assertPresent: () => void} = (Zone as any)['ProxyZoneSpec']; + +{ + describe('fake async', () => { + it('should run synchronous code', () => { + let ran = false; + fakeAsync(() => { + ran = true; + })(); + + expect(ran).toEqual(true); + }); + + it('should pass arguments to the wrapped function', () => { + fakeAsync((foo: any /** TODO #9100 */, bar: any /** TODO #9100 */) => { + expect(foo).toEqual('foo'); + expect(bar).toEqual('bar'); + })('foo', 'bar'); + }); + + + it('should throw on nested calls', () => { + expect(() => { + fakeAsync(() => { + fakeAsync((): any /** TODO #9100 */ => null)(); + })(); + }).toThrowError('fakeAsync() calls can not be nested'); + }); + + it('should flush microtasks before returning', () => { + let thenRan = false; + + fakeAsync(() => { + resolvedPromise.then(_ => { + thenRan = true; + }); + })(); + + expect(thenRan).toEqual(true); + }); + + + it('should propagate the return value', () => { + expect(fakeAsync(() => 'foo')()).toEqual('foo'); + }); + + describe('Promise', () => { + it('should run asynchronous code', fakeAsync(() => { + let thenRan = false; + resolvedPromise.then((_) => { + thenRan = true; + }); + + expect(thenRan).toEqual(false); + + flushMicrotasks(); + expect(thenRan).toEqual(true); + })); + + it('should run chained thens', fakeAsync(() => { + const log = new Log(); + + resolvedPromise.then((_) => log.add(1)).then((_) => log.add(2)); + + expect(log.result()).toEqual(''); + + flushMicrotasks(); + expect(log.result()).toEqual('1; 2'); + })); + + it('should run Promise created in Promise', fakeAsync(() => { + const log = new Log(); + + resolvedPromise.then((_) => { + log.add(1); + resolvedPromise.then((_) => log.add(2)); + }); + + expect(log.result()).toEqual(''); + + flushMicrotasks(); + expect(log.result()).toEqual('1; 2'); + })); + + it('should complain if the test throws an exception during async calls', () => { + expect(() => { + fakeAsync(() => { + resolvedPromise.then((_) => { + throw new Error('async'); + }); + flushMicrotasks(); + })(); + }).toThrowError(/Uncaught \(in promise\): Error: async/); + }); + + it('should complain if a test throws an exception', () => { + expect(() => { + fakeAsync(() => { + throw new Error('sync'); + })(); + }).toThrowError('sync'); + }); + + }); + + describe('timers', () => { + it('should run queued zero duration timer on zero tick', fakeAsync(() => { + let ran = false; + setTimeout(() => { + ran = true; + }, 0); + + expect(ran).toEqual(false); + + tick(); + expect(ran).toEqual(true); + })); + + + it('should run queued timer after sufficient clock ticks', fakeAsync(() => { + let ran = false; + setTimeout(() => { + ran = true; + }, 10); + + tick(6); + expect(ran).toEqual(false); + + tick(6); + expect(ran).toEqual(true); + })); + + it('should run queued timer only once', fakeAsync(() => { + let cycles = 0; + setTimeout(() => { + cycles++; + }, 10); + + tick(10); + expect(cycles).toEqual(1); + + tick(10); + expect(cycles).toEqual(1); + + tick(10); + expect(cycles).toEqual(1); + })); + + it('should not run cancelled timer', fakeAsync(() => { + let ran = false; + const id = setTimeout(() => { + ran = true; + }, 10); + clearTimeout(id); + + tick(10); + expect(ran).toEqual(false); + })); + + it('should throw an error on dangling timers', () => { + expect(() => { + fakeAsync(() => { + setTimeout(() => {}, 10); + })(); + }).toThrowError('1 timer(s) still in the queue.'); + }); + + it('should throw an error on dangling periodic timers', () => { + expect(() => { + fakeAsync(() => { + setInterval(() => {}, 10); + })(); + }).toThrowError('1 periodic timer(s) still in the queue.'); + }); + + it('should run periodic timers', fakeAsync(() => { + let cycles = 0; + const id = setInterval(() => { + cycles++; + }, 10); + + tick(10); + expect(cycles).toEqual(1); + + tick(10); + expect(cycles).toEqual(2); + + tick(10); + expect(cycles).toEqual(3); + clearInterval(id); + })); + + it('should not run cancelled periodic timer', fakeAsync(() => { + let ran = false; + const id = setInterval(() => { + ran = true; + }, 10); + clearInterval(id); + + tick(10); + expect(ran).toEqual(false); + })); + + it('should be able to cancel periodic timers from a callback', fakeAsync(() => { + let cycles = 0; + let id: any /** TODO #9100 */; + + id = setInterval(() => { + cycles++; + clearInterval(id); + }, 10); + + tick(10); + expect(cycles).toEqual(1); + + tick(10); + expect(cycles).toEqual(1); + })); + + it('should clear periodic timers', fakeAsync(() => { + let cycles = 0; + const id = setInterval(() => { + cycles++; + }, 10); + + tick(10); + expect(cycles).toEqual(1); + + discardPeriodicTasks(); + + // Tick once to clear out the timer which already started. + tick(10); + expect(cycles).toEqual(2); + + tick(10); + // Nothing should change + expect(cycles).toEqual(2); + })); + + it('should process microtasks before timers', fakeAsync(() => { + const log = new Log(); + + resolvedPromise.then((_) => log.add('microtask')); + + setTimeout(() => log.add('timer'), 9); + + const id = setInterval(() => log.add('periodic timer'), 10); + + expect(log.result()).toEqual(''); + + tick(10); + expect(log.result()).toEqual('microtask; timer; periodic timer'); + clearInterval(id); + })); + + it('should process micro-tasks created in timers before next timers', fakeAsync(() => { + const log = new Log(); + + resolvedPromise.then((_) => log.add('microtask')); + + setTimeout(() => { + log.add('timer'); + resolvedPromise.then((_) => log.add('t microtask')); + }, 9); + + const id = setInterval(() => { + log.add('periodic timer'); + resolvedPromise.then((_) => log.add('pt microtask')); + }, 10); + + tick(10); + expect(log.result()) + .toEqual('microtask; timer; t microtask; periodic timer; pt microtask'); + + tick(10); + expect(log.result()) + .toEqual( + 'microtask; timer; t microtask; periodic timer; pt microtask; periodic timer; pt microtask'); + clearInterval(id); + })); + + it('should flush tasks', fakeAsync(() => { + let ran = false; + setTimeout(() => { + ran = true; + }, 10); + + flush(); + expect(ran).toEqual(true); + })); + + it('should flush multiple tasks', fakeAsync(() => { + let ran = false; + let ran2 = false; + setTimeout(() => { + ran = true; + }, 10); + setTimeout(() => { + ran2 = true; + }, 30); + + let elapsed = flush(); + + expect(ran).toEqual(true); + expect(ran2).toEqual(true); + expect(elapsed).toEqual(30); + })); + + it('should move periodic tasks', fakeAsync(() => { + let ran = false; + let count = 0; + setInterval(() => { + count++; + }, 10); + setTimeout(() => { + ran = true; + }, 35); + + let elapsed = flush(); + + expect(count).toEqual(3); + expect(ran).toEqual(true); + expect(elapsed).toEqual(35); + + discardPeriodicTasks(); + })); + }); + + describe('outside of the fakeAsync zone', () => { + it('calling flushMicrotasks should throw', () => { + expect(() => { + flushMicrotasks(); + }).toThrowError('The code should be running in the fakeAsync zone to call this function'); + }); + + it('calling tick should throw', () => { + expect(() => { + tick(); + }).toThrowError('The code should be running in the fakeAsync zone to call this function'); + }); + + it('calling flush should throw', () => { + expect(() => { + flush(); + }).toThrowError('The code should be running in the fakeAsync zone to call this function'); + }); + + it('calling discardPeriodicTasks should throw', () => { + expect(() => { + discardPeriodicTasks(); + }).toThrowError('The code should be running in the fakeAsync zone to call this function'); + }); + }); + + describe('only one `fakeAsync` zone per test', () => { + let zoneInBeforeEach: Zone; + let zoneInTest1: Zone; + beforeEach(fakeAsync(() => { + zoneInBeforeEach = Zone.current; + })); + + it('should use the same zone as in beforeEach', fakeAsync(() => { + zoneInTest1 = Zone.current; + expect(zoneInTest1).toBe(zoneInBeforeEach); + })); + }); + }); + + describe('ProxyZone', () => { + beforeEach(() => { + ProxyZoneSpec.assertPresent(); + }); + + afterEach(() => { + ProxyZoneSpec.assertPresent(); + }); + + it('should allow fakeAsync zone to retroactively set a zoneSpec outside of fakeAsync', () => { + ProxyZoneSpec.assertPresent(); + let state: string = 'not run'; + const testZone = Zone.current.fork({name: 'test-zone'}); + (fakeAsync(() => { + testZone.run(() => { + Promise.resolve('works').then((v) => state = v); + expect(state).toEqual('not run'); + flushMicrotasks(); + expect(state).toEqual('works'); + }); + }))(); + expect(state).toEqual('works'); + }); + }); +} \ No newline at end of file diff --git a/test/zone-spec/proxy.spec.ts b/test/zone-spec/proxy.spec.ts index d815ce631..950353fef 100644 --- a/test/zone-spec/proxy.spec.ts +++ b/test/zone-spec/proxy.spec.ts @@ -6,8 +6,6 @@ * found in the LICENSE file at https://angular.io/license */ -import '../../lib/zone-spec/proxy'; - describe('ProxySpec', () => { let ProxyZoneSpec: any; let delegate: ZoneSpec; @@ -192,16 +190,10 @@ describe('ProxySpec', () => { setTimeout(() => { expect(log).toEqual([ - 'zoneSpec1 hasTask: false,true', - 'zoneSpec2 hasTask: false,true', - 'zoneSpec2 hasTask: true,true', - 'zoneSpec2 hasTask: true,true', - 'then in zoneSpec2', - 'then in zoneSpec2', - 'zoneSpec2 hasTask: false,true', - 'timeout in zoneSpec1', - 'timeout in null spec', - 'zoneSpec2 hasTask: false,false' + 'zoneSpec1 hasTask: false,true', 'zoneSpec2 hasTask: false,true', + 'zoneSpec2 hasTask: true,true', 'zoneSpec2 hasTask: true,true', 'then in zoneSpec2', + 'then in zoneSpec2', 'zoneSpec2 hasTask: false,true', 'timeout in zoneSpec1', + 'timeout in null spec', 'zoneSpec2 hasTask: false,false' ]); done(); }, 300); diff --git a/test/zone-spec/sync-test.spec.ts b/test/zone-spec/sync-test.spec.ts index 2dbe9cb1b..d51dff686 100644 --- a/test/zone-spec/sync-test.spec.ts +++ b/test/zone-spec/sync-test.spec.ts @@ -6,7 +6,6 @@ * found in the LICENSE file at https://angular.io/license */ -import '../../lib/zone-spec/sync-test'; import {ifEnvSupports} from '../test-util'; describe('SyncTestZoneSpec', () => { diff --git a/test/zone-spec/task-tracking.spec.ts b/test/zone-spec/task-tracking.spec.ts index e9537579c..1c7115d2a 100644 --- a/test/zone-spec/task-tracking.spec.ts +++ b/test/zone-spec/task-tracking.spec.ts @@ -6,8 +6,6 @@ * found in the LICENSE file at https://angular.io/license */ -import '../../lib/zone-spec/task-tracking'; - import {supportPatchXHROnProperty} from '../test-util'; declare const global: any;