diff --git a/dist/async-test.js b/dist/async-test.js new file mode 100644 index 000000000..5c1cc57aa --- /dev/null +++ b/dist/async-test.js @@ -0,0 +1,137 @@ +/******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; + +/******/ // The require function +/******/ function __webpack_require__(moduleId) { + +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) +/******/ return installedModules[moduleId].exports; + +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ exports: {}, +/******/ id: moduleId, +/******/ loaded: false +/******/ }; + +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); + +/******/ // Flag the module as loaded +/******/ module.loaded = true; + +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } + + +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; + +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; + +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; + +/******/ // Load entry module and return exports +/******/ return __webpack_require__(0); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ function(module, exports) { + + (function () { + var AsyncTestZoneSpec = (function () { + function AsyncTestZoneSpec(finishCallback, failCallback, namePrefix) { + this._pendingMicroTasks = false; + this._pendingMacroTasks = false; + this._pendingEventTasks = false; + this._alreadyErrored = false; + this.runZone = Zone.current; + // ZoneSpec implementation below. + this.name = 'asyncTestZone'; + this._finishCallback = finishCallback; + this._failCallback = failCallback; + this.name = 'asyncTestZone for ' + namePrefix; + } + AsyncTestZoneSpec.prototype._finishCallbackIfDone = function () { + var _this = this; + if (!(this._pendingMicroTasks || this._pendingMacroTasks || this._pendingEventTasks)) { + // We do this because we would like to catch unhandled rejected promises. + // To do this quickly when there are native promises, we must run using an unwrapped + // promise implementation. + var symbol = Zone.__symbol__; + var NativePromise = window[symbol('Promise')]; + if (NativePromise) { + NativePromise.resolve(true)[symbol('then')](function () { + if (!_this._alreadyErrored) { + _this.runZone.run(_this._finishCallback); + } + }); + } + else { + // For implementations which do not have nativePromise, use setTimeout(0). This is slower, + // but it also works because Zones will handle errors when rejected promises have no + // listeners after one macrotask. + this.runZone.run(function () { + setTimeout(function () { + if (!_this._alreadyErrored) { + _this._finishCallback(); + } + }, 0); + }); + } + } + }; + AsyncTestZoneSpec.prototype.onInvoke = function (parentZoneDelegate, currentZone, targetZone, delegate, applyThis, applyArgs, source) { + try { + return parentZoneDelegate.invoke(targetZone, delegate, applyThis, applyArgs, source); + } + finally { + this._finishCallbackIfDone(); + } + }; + AsyncTestZoneSpec.prototype.onHandleError = function (parentZoneDelegate, currentZone, targetZone, error) { + // Let the parent try to handle the error. + var result = parentZoneDelegate.handleError(targetZone, error); + if (result) { + this._failCallback(error.message ? error.message : 'unknown error'); + this._alreadyErrored = true; + } + return false; + }; + AsyncTestZoneSpec.prototype.onScheduleTask = function (delegate, currentZone, targetZone, task) { + if (task.type == 'macroTask' && task.source == 'setInterval') { + this._failCallback('Cannot use setInterval from within an async zone test.'); + return; + } + return delegate.scheduleTask(targetZone, task); + }; + AsyncTestZoneSpec.prototype.onHasTask = function (delegate, current, target, hasTaskState) { + delegate.hasTask(target, hasTaskState); + if (hasTaskState.change == 'microTask') { + this._pendingMicroTasks = hasTaskState.microTask; + this._finishCallbackIfDone(); + } + else if (hasTaskState.change == 'macroTask') { + this._pendingMacroTasks = hasTaskState.macroTask; + this._finishCallbackIfDone(); + } + else if (hasTaskState.change == 'eventTask') { + this._finishCallbackIfDone(); + } + }; + return AsyncTestZoneSpec; + }()); + // Export the class so that new instances can be created with proper + // constructor params. + Zone['AsyncTestZoneSpec'] = AsyncTestZoneSpec; + })(); + + +/***/ } +/******/ ]); \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js index df162140a..d2f8a0d41 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -98,6 +98,10 @@ gulp.task('build/wtf.min.js', function(cb) { return generateBrowserScript('./lib/zone-spec/wtf.ts', 'wtf.min.js', true, cb); }); +gulp.task('build/async-test.js', function(cb) { + return generateBrowserScript('./lib/zone-spec/async-test.ts', 'async-test.js', false, cb); +}); + gulp.task('build', [ 'build/zone.js', 'build/zone.js.d.ts', @@ -108,7 +112,8 @@ gulp.task('build', [ 'build/long-stack-trace-zone.js', 'build/long-stack-trace-zone.min.js', 'build/wtf.js', - 'build/wtf.min.js' + 'build/wtf.min.js', + 'build/async-test.js' ]); diff --git a/karma.conf.js b/karma.conf.js index c6ad4c10f..d44ca772b 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -51,7 +51,7 @@ module.exports = function (config) { logLevel: config.LOG_INFO, - browsers: ['Firefox'], + browsers: ['Chrome'], frameworks: ['jasmine'], captureTimeout: 60000, diff --git a/lib/zone-spec/async-test.ts b/lib/zone-spec/async-test.ts new file mode 100644 index 000000000..d2241240d --- /dev/null +++ b/lib/zone-spec/async-test.ts @@ -0,0 +1,96 @@ +(function() { + class AsyncTestZoneSpec implements ZoneSpec { + _finishCallback: Function; + _failCallback: Function; + _pendingMicroTasks: boolean = false; + _pendingMacroTasks: boolean = false; + _pendingEventTasks: boolean = false; + _alreadyErrored = false; + runZone = Zone.current; + + constructor(finishCallback: Function, failCallback: Function, namePrefix: string) { + this._finishCallback = finishCallback; + this._failCallback = failCallback; + this.name = 'asyncTestZone for ' + namePrefix; + } + + _finishCallbackIfDone() { + if (!(this._pendingMicroTasks || this._pendingMacroTasks || this._pendingEventTasks)) { + // We do this because we would like to catch unhandled rejected promises. + // To do this quickly when there are native promises, we must run using an unwrapped + // promise implementation. + var symbol = (Zone).__symbol__; + var NativePromise: typeof Promise = window[symbol('Promise')]; + if (NativePromise) { + NativePromise.resolve(true)[symbol('then')](() => { + if (!this._alreadyErrored) { + this.runZone.run(this._finishCallback); + } + }); + } else { + // For implementations which do not have nativePromise, use setTimeout(0). This is slower, + // but it also works because Zones will handle errors when rejected promises have no + // listeners after one macrotask. + this.runZone.run(() => { + setTimeout(() => { + if (!this._alreadyErrored) { + this._finishCallback(); + } + }, 0); + }); + } + } + } + + // ZoneSpec implementation below. + + name: string = 'asyncTestZone'; + + onInvoke(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, + delegate: Function, applyThis: any, applyArgs: any[], source: string): any { + try { + return parentZoneDelegate.invoke(targetZone, delegate, applyThis, applyArgs, source); + } finally { + this._finishCallbackIfDone(); + } + } + + onHandleError(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, + error: any): boolean { + // Let the parent try to handle the error. + var result = parentZoneDelegate.handleError(targetZone, error); + if (result) { + this._failCallback(error.message ? error.message : 'unknown error'); + this._alreadyErrored = true; + } + return false; + } + + onScheduleTask(delegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task): Task { + if (task.type == 'macroTask' && task.source == 'setInterval') { + this._failCallback('Cannot use setInterval from within an async zone test.'); + return; + } + + return delegate.scheduleTask(targetZone, task); + } + + onHasTask(delegate: ZoneDelegate, current: Zone, target: Zone, hasTaskState: HasTaskState) { + delegate.hasTask(target, hasTaskState); + + if (hasTaskState.change == 'microTask') { + this._pendingMicroTasks = hasTaskState.microTask; + this._finishCallbackIfDone(); + } else if (hasTaskState.change == 'macroTask') { + this._pendingMacroTasks = hasTaskState.macroTask; + this._finishCallbackIfDone(); + } else if (hasTaskState.change == 'eventTask') { + this._finishCallbackIfDone(); + } + } + } + + // Export the class so that new instances can be created with proper + // constructor params. + Zone['AsyncTestZoneSpec'] = AsyncTestZoneSpec; +})(); diff --git a/test/async-test.spec.ts b/test/async-test.spec.ts new file mode 100644 index 000000000..e40b7d871 --- /dev/null +++ b/test/async-test.spec.ts @@ -0,0 +1,172 @@ +import '../lib/zone-spec/async-test'; + +describe('AsyncTestZoneSpec', function() { + var log; + var AsyncTestZoneSpec = Zone['AsyncTestZoneSpec']; + + function finishCallback() { + log.push('finish'); + } + + function failCallback() { + log.push('fail'); + } + + beforeEach(() => { + log = []; + }); + + it('should call finish after zone is run', (done) => { + var finished = false; + var testZoneSpec = new AsyncTestZoneSpec(() => { + expect(finished).toBe(true); + done(); + }, failCallback, 'name'); + + var atz = Zone.current.fork(testZoneSpec); + + atz.run(function() { + finished = true; + }); + }); + + it('should call finish after a setTimeout is done', (done) => { + var finished = false; + + var testZoneSpec = new AsyncTestZoneSpec(() => { + expect(finished).toBe(true); + done(); + }, () => { + done.fail('async zone called failCallback unexpectedly'); + }, 'name'); + + var atz = Zone.current.fork(testZoneSpec); + + atz.run(function() { + setTimeout(() => { + finished = true; + }); + }); + }); + + it('should call finish after a setTimeout is done', (done) => { + var finished = false; + + var testZoneSpec = new AsyncTestZoneSpec(() => { + expect(finished).toBe(true); + done(); + }, () => { + done.fail('async zone called failCallback unexpectedly'); + }, 'name'); + + var atz = Zone.current.fork(testZoneSpec); + + atz.run(function() { + setTimeout(() => { + finished = true; + }); + }); + }); + + it('should call finish after microtasks are done', (done) => { + var finished = false; + + var testZoneSpec = new AsyncTestZoneSpec(() => { + expect(finished).toBe(true); + done(); + }, () => { + done.fail('async zone called failCallback unexpectedly'); + }, 'name'); + + var atz = Zone.current.fork(testZoneSpec); + + atz.run(function() { + Promise.resolve().then(() => { + finished = true; + }); + }); + }); + + it('should call finish after an event task is done', (done) => { + var finished = false; + + var testZoneSpec = new AsyncTestZoneSpec(() => { + expect(finished).toBe(true); + done(); + }, () => { + done.fail('async zone called failCallback unexpectedly'); + }, 'name'); + + var atz = Zone.current.fork(testZoneSpec); + + atz.run(function() { + var div = document.createElement('div'); + + div.addEventListener('click', () => { + finished = true; + }); + + div.click(); + }); + }); + + it('should fail if setInterval is used', (done) => { + var finished = false; + + var testZoneSpec = new AsyncTestZoneSpec(() => { + done.fail('expected failCallback to be called'); + }, (err) => { + expect(err).toEqual('Cannot use setInterval from within an async zone test.'); + done(); + }, 'name'); + + var atz = Zone.current.fork(testZoneSpec); + + atz.run(function() { + setInterval(() => { + }, 100); + }); + }); + + it('should fail if an error is thrown', (done) => { + var finished = false; + + var testZoneSpec = new AsyncTestZoneSpec(() => { + done.fail('expected failCallback to be called'); + }, (err) => { + expect(err).toEqual('my error'); + done(); + }, 'name'); + + var atz = Zone.current.fork(testZoneSpec); + + atz.run(function() { + setTimeout(() => { + throw new Error('my error'); + }, 10); + }); + }); + + it('should fail if a promise rejection is unhandled', (done) => { + var finished = false; + + var testZoneSpec = new AsyncTestZoneSpec(() => { + done.fail('expected failCallback to be called'); + }, (err) => { + expect(err).toEqual('Uncaught (in promise): my reason'); + done(); + }, 'name'); + + var atz = Zone.current.fork(testZoneSpec); + + atz.run(function() { + // Promise rejection does not cause onHandleError to trigger + // until the next Macro task, so that there's a chance + // for handlers to be added. + Promise.reject('my reason'); + }); + + }); +}); + +export var __something__; diff --git a/test/browser_entry_point.ts b/test/browser_entry_point.ts index ee3602760..064bd6060 100644 --- a/test/browser_entry_point.ts +++ b/test/browser_entry_point.ts @@ -12,6 +12,7 @@ import './test-env-setup'; // List all tests here: import './long-stack-trace-zone.spec'; +import './async-test.spec'; import './microtasks.spec'; import './zone.spec'; import './integration/brick.spec';