)
+ ```
+
+ The second feature will slow down the `Error` performance, so `zone.js` provide a flag to let you be able to control the behavior.
+ The flag is `__Zone_Error_BlacklistedStackFrames_policy`. And the available options is:
+
+ 1. default: this is the default one, if you load `zone.js/dist/zone-error` without
+ setting the flag, `default` will be used, and `BlackListStackFrames` will be available
+ when `new Error()`, you can get a `error.stack` which is `zone stack free`. But this
+ will slow down `new Error()` a little bit.
+
+ 2. disable: this will disable `BlackListZoneStackFrame` feature, and if you load
+ `zone.js/dist/zone-error`, you will only get a `wrapped Error` which can handle
+ `Error inherit` issue.
+
+ 3. lazy: this is a feature to let you be able to get `BlackListZoneStackFrame` feature,
+ but not impact performance. But as a trade off, you can't get the `zone free stack
+ frames` by access `error.stack`. You can only get it by access `error.zoneAwareStack`.
+
+
+- Angular(2+)
+
+Angular uses zone.js to manage async operations and decide when to perform change detection. Thus, in Angular,
+the following APIs should be patched, otherwise Angular may not work as expected.
+
+1. ZoneAwarePromise
+2. timer
+3. on_property
+4. EventTarget
+5. XHR
\ No newline at end of file
diff --git a/packages/zone.js/NON-STANDARD-APIS.md b/packages/zone.js/NON-STANDARD-APIS.md
new file mode 100644
index 0000000000..8cca25bdd3
--- /dev/null
+++ b/packages/zone.js/NON-STANDARD-APIS.md
@@ -0,0 +1,229 @@
+# Zone.js's support for non standard apis
+
+Zone.js patched most standard APIs such as DOM event listeners, XMLHttpRequest in Browser, EventEmitter and fs API in Node.js so they can be in zone.
+
+But there are still a lot of non standard APIs that are not patched by default, such as MediaQuery, Notification,
+ WebAudio and so on. We are adding support to those APIs, and our progress is updated here.
+
+## Currently supported non standard Web APIs
+
+* MediaQuery
+* Notification
+
+## Currently supported polyfills
+
+* webcomponents
+
+Usage:
+
+```
+
+
+
+```
+
+## Currently supported non standard node APIs
+
+## Currently supported non standard common APIs
+
+* bluebird promise
+
+Browser Usage:
+
+```
+
+
+
+
+```
+
+After those steps, window.Promise will become a ZoneAware Bluebird Promise.
+
+Node Sample Usage:
+
+```
+require('zone.js');
+const Bluebird = require('bluebird');
+require('zone.js/dist/zone-bluebird');
+Zone[Zone['__symbol__']('bluebird')](Bluebird);
+Zone.current.fork({
+ name: 'bluebird'
+}).run(() => {
+ Bluebird.resolve(1).then(r => {
+ console.log('result ', r, 'Zone', Zone.current.name);
+ });
+});
+```
+
+In NodeJS environment, you can choose to use Bluebird Promise as global.Promise
+or use ZoneAwarePromise as global.Promise.
+
+To run the jasmine test cases of bluebird
+
+```
+ npm install bluebird
+```
+
+then modify test/node_tests.ts
+remove the comment of the following line
+
+```
+//import './extra/bluebird.spec';
+```
+
+## Others
+
+* Cordova
+
+patch `cordova.exec` API
+
+`cordova.exec(success, error, service, action, args);`
+
+`success` and `error` will be patched with `Zone.wrap`.
+
+to load the patch, you should load in the following order.
+
+```
+
+
+
+```
+
+## Usage
+
+By default, those APIs' support will not be loaded in zone.js or zone-node.js,
+so if you want to load those API's support, you should load those files by yourself.
+
+For example, if you want to add MediaQuery patch, you should do like this:
+
+```
+
+
+```
+
+* rxjs
+
+`zone.js` also provide a `rxjs` patch to make sure rxjs Observable/Subscription/Operator run in correct zone.
+For details please refer to [pull request 843](https://github.com/angular/zone.js/pull/843). The following sample code describes the idea.
+
+```
+const constructorZone = Zone.current.fork({name: 'constructor'});
+const subscriptionZone = Zone.current.fork({name: 'subscription'});
+const operatorZone = Zone.current.fork({name: 'operator'});
+
+let observable;
+let subscriber;
+constructorZone.run(() => {
+ observable = new Observable((_subscriber) => {
+ subscriber = _subscriber;
+ console.log('current zone when construct observable:', Zone.current.name); // will output constructor.
+ return () => {
+ console.log('current zone when unsubscribe observable:', Zone.current.name); // will output constructor.
+ }
+ });
+});
+
+subscriptionZone.run(() => {
+ observable.subscribe(() => {
+ console.log('current zone when subscription next', Zone.current.name); // will output subscription.
+ }, () => {
+ console.log('current zone when subscription error', Zone.current.name); // will output subscription.
+ }, () => {
+ console.log('current zone when subscription complete', Zone.current.name); // will output subscription.
+ });
+});
+
+operatorZone.run(() => {
+ observable.map(() => {
+ console.log('current zone when map operator', Zone.current.name); // will output operator.
+ });
+});
+```
+
+Currently basically everything the `rxjs` API includes
+
+- Observable
+- Subscription
+- Subscriber
+- Operators
+- Scheduler
+
+is patched, so each asynchronous call will run in the correct zone.
+
+## Usage.
+
+For example, in an Angular application, you can load this patch in your `app.module.ts`.
+
+```
+import 'zone.js/dist/zone-patch-rxjs';
+```
+
+* electron
+
+In electron, we patched the following APIs with `zone.js`
+
+1. Browser API
+2. NodeJS
+3. Electorn Native API
+
+## Usage.
+
+add following line into `polyfill.ts` after loading zone-mix.
+
+```
+//import 'zone.js/dist/zone'; // originally added by angular-cli, comment it out
+import 'zone.js/dist/zone-mix'; // add zone-mix to patch both Browser and Nodejs
+import 'zone.js/dist/zone-patch-electron'; // add zone-patch-electron to patch Electron native API
+```
+
+there is a sampel repo [zone-electron](https://github.com/JiaLiPassion/zone-electron).
+
+* socket.io-client
+
+user need to patch `io` themselves just like following code.
+
+```javascript
+
+
+
+
+```
+
+please reference the sample repo [zone-socketio](https://github.com/JiaLiPassion/zone-socketio) about
+detail usage.
+
+* jsonp
+
+## Usage.
+
+provide a helper method to patch jsonp. Because jsonp has a lot of implementation, so
+user need to provide the information to let json `send` and `callback` in zone.
+
+there is a sampel repo [zone-jsonp](https://github.com/JiaLiPassion/test-zone-js-with-jsonp) here,
+sample usage is:
+
+```javascript
+import 'zone.js/dist/zone-patch-jsonp';
+Zone['__zone_symbol__jsonp']({
+ jsonp: getJSONP,
+ sendFuncName: 'send',
+ successFuncName: 'jsonpSuccessCallback',
+ failedFuncName: 'jsonpFailedCallback'
+});
+```
+* ResizeObserver
+
+Currently only `Chrome 64` native support this feature.
+you can add the following line into `polyfill.ts` after loading `zone.js`.
+
+```
+import 'zone.js/dist/zone';
+import 'zone.js/dist/zone-patch-resize-observer';
+```
+
+there is a sample repo [zone-resize-observer](https://github.com/JiaLiPassion/zone-resize-observer) here
diff --git a/packages/zone.js/README.md b/packages/zone.js/README.md
new file mode 100644
index 0000000000..a623701602
--- /dev/null
+++ b/packages/zone.js/README.md
@@ -0,0 +1,92 @@
+# Zone.js
+
+[](https://cdnjs.com/libraries/zone.js)
+
+Implements _Zones_ for JavaScript, inspired by [Dart](https://www.dartlang.org/articles/zones/).
+
+> If you're using zone.js via unpkg (i.e. using `https://unpkg.com/zone.js`)
+> and you're using any of the following libraries, make sure you import them first
+
+> * 'newrelic' as it patches global.Promise before zone.js does
+> * 'async-listener' as it patches global.setTimeout, global.setInterval before zone.js does
+> * 'continuation-local-storage' as it uses async-listener
+
+# NEW Zone.js POST-v0.6.0
+
+See the new API [here](./lib/zone.ts).
+
+Read up on [Zone Primer](https://docs.google.com/document/d/1F5Ug0jcrm031vhSMJEOgp1l-Is-Vf0UCNDY-LsQtAIY).
+
+## What's a Zone?
+
+A Zone is an execution context that persists across async tasks.
+You can think of it as [thread-local storage](http://en.wikipedia.org/wiki/Thread-local_storage) for JavaScript VMs.
+
+See this video from ng-conf 2014 for a detailed explanation:
+
+[](//www.youtube.com/watch?v=3IqtmUscE_U&t=150)
+
+## See also
+* [async-listener](https://github.com/othiym23/async-listener) - a similar library for node
+* [Async stack traces in Chrome](http://www.html5rocks.com/en/tutorials/developertools/async-call-stack/)
+* [strongloop/zone](https://github.com/strongloop/zone) (Deprecated)
+* [vizone](https://github.com/gilbox/vizone) - control flow visualizer that uses zone.js
+
+## Standard API support
+
+zone.js patched most standard web APIs (such as DOM events, `XMLHttpRequest`, ...) and nodejs APIs
+(`EventEmitter`, `fs`, ...), for more details, please see [STANDARD-APIS.md](STANDARD-APIS.md).
+
+## Nonstandard API support
+
+We are adding support to some nonstandard APIs, such as MediaQuery and
+Notification. Please see [NON-STANDARD-APIS.md](NON-STANDARD-APIS.md) for more details.
+
+## Examples
+
+You can find some samples to describe how to use zone.js in [SAMPLE.md](SAMPLE.md).
+
+## Modules
+
+zone.js patches the async APIs described above, but those patches will have some overhead.
+Starting from zone.js v0.8.9, you can choose which web API module you want to patch.
+For more details, please
+see [MODULE.md](MODULE.md).
+
+## Bundles
+There are several bundles under `dist` folder.
+
+|Bundle|Summary|
+|---|---|
+|zone.js|the default bundle, contains the most used APIs such as `setTimeout/Promise/EventTarget...`, also this bundle supports all evergreen and legacy (IE/Legacy Firefox/Legacy Safari) Browsers|
+|zone-evergreen.js|the bundle for evergreen browsers, doesn't include the `patch` for `legacy` browsers such as `IE` or old versions of `Firefox/Safari`|
+|zone-legacy.js|the patch bundle for legacy browsers, only includes the `patch` for `legacy` browsers such as `IE` or old versions of `Firefox/Safari`. This bundle must be loaded after `zone-evergreen.js`, **`zone.js`=`zone-evergreen.js` + `zone-legacy.js`**|
+|zone-testing.js|the bundle for zone testing support, including `jasmine/mocha` support and `async/fakeAsync/sync` test utilities|
+|zone-externs.js|the API definitions for `closure compiler`|
+
+And here are the additional optional patches not included in the main zone.js bundles
+
+|Patch|Summary|
+|---|---|
+|webapis-media-query.js|patch for `MediaQuery APIs`|
+|webapis-notification.js|patch for `Notification APIs`|
+|webapis-rtc-peer-connection.js|patch for `RTCPeerConnection APIs`|
+|webapis-shadydom.js|patch for `Shady DOM APIs`|
+|zone-bluebird.js|patch for `Bluebird APIs`|
+|zone-error.js|patch for `Error Global Object`, supports remove `Zone StackTrace`|
+|zone-patch-canvas.js|patch for `Canvas API`|
+|zone-patch-cordova.js|patch for `Cordova API`|
+|zone-patch-electron.js|patch for `Electron API`|
+|zone-patch-fetch.js|patch for `Fetch API`|
+|zone-patch-jsonp.js|utility for `jsonp API`|
+|zone-patch-resize-observer.js|patch for `ResizeObserver API`|
+|zone-patch-rxjs.js|patch for `rxjs API`|
+|zone-patch-rxjs-fake-async.js|patch for `rxjs fakeasync test`|
+|zone-patch-socket-io.js|patch for `socket-io`|
+|zone-patch-user-media.js|patch for `UserMedia API`|
+
+## Promise A+ test passed
+[](https://promisesaplus.com/)
+
+## License
+MIT
diff --git a/packages/zone.js/SAMPLE.md b/packages/zone.js/SAMPLE.md
new file mode 100644
index 0000000000..c4b9d463cd
--- /dev/null
+++ b/packages/zone.js/SAMPLE.md
@@ -0,0 +1,23 @@
+# Sample
+
+### Basic Sample
+
+use `zone.js` and `long-stack-trace-zone.js` to display longStackTrace information in html.
+[basic](https://stackblitz.com/edit/zonejs-basic?file=index.js)
+
+### Async Task Counting Sample
+
+use `zone.js` to monitor async tasks and print the count info.
+[counting](https://stackblitz.com/edit/zonejs-counting?file=index.js)
+
+### Profiling Sample
+
+use `zone.js` to profiling sort algorithm.
+[profiling](https://stackblitz.com/edit/zonejs-profiling?file=index.js)
+
+### Throttle with longStackTrace
+
+use `long-stack-trace-zone` to display full flow of complex async operations such as throttle XHR requests.
+[throttle](https://stackblitz.com/edit/zonejs-throttle?file=index.js)
+
+
diff --git a/packages/zone.js/STANDARD-APIS.md b/packages/zone.js/STANDARD-APIS.md
new file mode 100644
index 0000000000..ed5d9c6581
--- /dev/null
+++ b/packages/zone.js/STANDARD-APIS.md
@@ -0,0 +1,148 @@
+# Zone.js's support for standard apis
+
+Zone.js patched most standard APIs such as DOM event listeners, XMLHttpRequest in Browser, EventEmitter and fs API in Node.js so they can be in zone.
+
+In this document, all patched API are listed.
+
+For non-standard APIs, please see [NON-STANDARD-APIS.md](NON-STANDARD-APIS.md)
+
+## Patch Mechanisms
+
+There are several patch mechanisms
+
+- wrap: makes callbacks run in zones, and makes applications able to receive onInvoke and onIntercept callbacks
+- Task: just like in the JavaScript VM, applications can receive onScheduleTask, onInvokeTask, onCancelTask and onHasTask callbacks
+ 1. MacroTask
+ 2. MicroTask
+ 3. EventTask
+
+Some APIs which should be treated as Tasks, but are currently still patched in the wrap way. These will be patched as Tasks soon.
+
+## Browser
+
+Web APIs
+
+| API | Patch Mechanism | Others |
+| --- | --- | --- |
+| setTimeout/clearTimeout | MacroTask | app can get handlerId, interval, args, isPeriodic(false) through task.data |
+| setImmediate/clearImmediate | MacroTask | same with setTimeout |
+| setInterval/clearInterval | MacroTask | isPeriodic is true, so setInterval will not trigger onHasTask callback |
+| requestAnimationFrame/cancelAnimationFrame | MacroTask | |
+| mozRequestAnimationFrame/mozCancelAnimationFrame | MacroTask | |
+| webkitRequestAnimationFrame/webkitCancelAnimationFrame | MacroTask | |
+| alert | wrap | |
+| prompt | wrap | |
+| confirm | wrap | |
+| Promise | MicroTask | |
+| EventTarget | EventTask | see below Event Target for more details |
+| HTMLElement on properties | EventTask | see below on properties for more details |
+| XMLHttpRequest.send/abort | MacroTask | |
+| XMLHttpRequest on properties | EventTask | |
+| IDBIndex on properties | EventTask | |
+| IDBRequest on properties | EventTask | |
+| IDBOpenDBRequest on properties | EventTask | |
+| IDBDatabaseRequest on properties | EventTask | |
+| IDBTransaction on properties | EventTask | |
+| IDBCursor on properties | EventTask | |
+| WebSocket on properties | EventTask | |
+| MutationObserver | wrap | |
+| WebkitMutationObserver | wrap | |
+| FileReader | wrap | |
+| registerElement | wrap | |
+
+EventTarget
+
+- For browsers supporting EventTarget, Zone.js just patches EventTarget, so everything that inherits
+from EventTarget will also be patched.
+- For browsers that do not support EventTarget, Zone.js will patch the following APIs in the IDL
+ that inherit from EventTarget
+
+ |||||
+ |---|---|---|---|
+ |ApplicationCache|EventSource|FileReader|InputMethodContext|
+ |MediaController|MessagePort|Node|Performance|
+ |SVGElementInstance|SharedWorker|TextTrack|TextTrackCue|
+ |TextTrackList|WebKitNamedFlow|Window|Worker|
+ |WorkerGlobalScope|XMLHttpRequest|XMLHttpRequestEventTarget|XMLHttpRequestUpload|
+ |IDBRequest|IDBOpenDBRequest|IDBDatabase|IDBTransaction|
+ |IDBCursor|DBIndex|WebSocket|
+
+The following 'on' properties, such as onclick, onreadystatechange, are patched in Zone.js as EventTasks
+
+ |||||
+ |---|---|---|---|
+ |copy|cut|paste|abort|
+ |blur|focus|canplay|canplaythrough|
+ |change|click|contextmenu|dblclick|
+ |drag|dragend|dragenter|dragleave|
+ |dragover|dragstart|drop|durationchange|
+ |emptied|ended|input|invalid|
+ |keydown|keypress|keyup|load|
+ |loadeddata|loadedmetadata|loadstart|message|
+ |mousedown|mouseenter|mouseleave|mousemove|
+ |mouseout|mouseover|mouseup|pause|
+ |play|playing|progress|ratechange|
+ |reset|scroll|seeked|seeking|
+ |select|show|stalled|submit|
+ |suspend|timeupdate|volumechange|waiting|
+ |mozfullscreenchange|mozfullscreenerror|mozpointerlockchange|mozpointerlockerror|
+ |error|webglcontextrestored|webglcontextlost|webglcontextcreationerror|
+
+## NodeJS
+
+| API | Patch Mechanism | Others |
+| --- | --- | --- |
+| setTimeout/clearTimeout | MacroTask | app can get handlerId, interval, args, isPeriodic(false) through task.data |
+| setImmediate/clearImmediate | MacroTask | same with setTimeout |
+| setInterval/clearInterval | MacroTask | isPeriodic is true, so setInterval will not trigger onHasTask callback |
+| process.nextTick | Microtask | isPeriodic is true, so setInterval will not trigger onHasTask callback |
+| Promise | MicroTask | |
+| EventEmitter | EventTask | All APIs inherit EventEmitter are patched as EventTask |
+| crypto | MacroTask | |
+| fs | MacroTask | all async methods are patched |
+
+EventEmitter, addEventListener, prependEventListener and 'on' will be patched once as EventTasks, and removeEventListener and
+removeAllListeners will remove those EventTasks
+
+## Electron
+
+Zone.js does not patch the Electron API, although in Electron both browser APIs and node APIs are patched, so
+if you want to include Zone.js in Electron, please use dist/zone-mix.js
+
+## ZoneAwareError
+
+ZoneAwareError replaces global Error, and adds zone information to stack trace.
+ZoneAwareError also handles 'this' issue.
+This type of issue would happen when creating an error without `new`: `this` would be `undefined` in strict mode, and `global` in
+non-strict mode. It could cause some very difficult to detect issues.
+
+```javascript
+ const error = Error();
+```
+
+ZoneAwareError makes sure that `this` is ZoneAwareError even without new.
+
+## ZoneAwarePromise
+
+ZoneAwarePromise wraps the global Promise and makes it run in zones as a MicroTask.
+It also passes promise A+ tests.
+
+## BlackListEvents
+
+Sometimes we don't want some `event` to be patched by `zone.js`, we can blacklist events
+by following settings.
+
+```javascript
+ // disable on properties
+ var targets = [window, Document, HTMLBodyElement, HTMLElement];
+ __Zone_ignore_on_properties = [];
+ targets.forEach(function (target) {
+ __Zone_ignore_on_properties.push({
+ target: target,
+ ignoreProperties: ['scroll']
+ });
+ });
+
+ // disable addEventListener
+ global['__zone_symbol__BLACK_LISTED_EVENTS'] = ['scroll'];
+```
diff --git a/packages/zone.js/bundles.bzl b/packages/zone.js/bundles.bzl
new file mode 100644
index 0000000000..0c6e6c04e7
--- /dev/null
+++ b/packages/zone.js/bundles.bzl
@@ -0,0 +1,50 @@
+"""
+Describe all the output bundles in the zone.js npm package
+by mapping the bundle name to the source location.
+"""
+
+_DIR = "//packages/zone.js/lib:"
+
+ES5_GLOBAL_BUNDLES = {
+ "zone": _DIR + "browser/rollup-legacy-main",
+ "zone-mix": _DIR + "mix/rollup-mix",
+ "zone-node": _DIR + "node/rollup-main",
+ "zone-testing-node-bundle": _DIR + "node/rollup-test-main",
+}
+
+ES5_BUNDLES = {
+ "async-test": _DIR + "testing/async-testing",
+ "fake-async-test": _DIR + "testing/fake-async",
+ "long-stack-trace-zone": _DIR + "zone-spec/long-stack-trace",
+ "proxy": _DIR + "zone-spec/proxy",
+ "zone-patch-rxjs-fake-async": _DIR + "rxjs/rxjs-fake-async",
+ "sync-test": _DIR + "zone-spec/sync-test",
+ "task-tracking": _DIR + "zone-spec/task-tracking",
+ "wtf": _DIR + "zone-spec/wtf",
+ "zone-error": _DIR + "common/error-rewrite",
+ "zone-legacy": _DIR + "browser/browser-legacy",
+ "zone-bluebird": _DIR + "extra/bluebird",
+ "zone-patch-canvas": _DIR + "browser/canvas",
+ "zone-patch-cordova": _DIR + "extra/cordova",
+ "zone-patch-electron": _DIR + "extra/electron",
+ "zone-patch-fetch": _DIR + "common/fetch",
+ "jasmine-patch": _DIR + "jasmine/jasmine",
+ "zone-patch-jsonp": _DIR + "extra/jsonp",
+ "webapis-media-query": _DIR + "browser/webapis-media-query",
+ "mocha-patch": _DIR + "mocha/mocha",
+ "webapis-notification": _DIR + "browser/webapis-notification",
+ "zone-patch-promise-test": _DIR + "testing/promise-testing",
+ "zone-patch-resize-observer": _DIR + "browser/webapis-resize-observer",
+ "webapis-rtc-peer-connection": _DIR + "browser/webapis-rtc-peer-connection",
+ "zone-patch-rxjs": _DIR + "rxjs/rxjs",
+ "webapis-shadydom": _DIR + "browser/shadydom",
+ "zone-patch-socket-io": _DIR + "extra/socket-io",
+ "zone-patch-user-media": _DIR + "browser/webapis-user-media",
+ "zone-testing": _DIR + "testing/zone-testing",
+ "zone-testing-bundle": _DIR + "browser/rollup-legacy-test-main",
+}
+
+ES2015_BUNDLES = {
+ "zone-evergreen": _DIR + "browser/rollup-main",
+ "zone-evergreen-testing-bundle": _DIR + "browser/rollup-test-main",
+}
diff --git a/packages/zone.js/check-file-size.js b/packages/zone.js/check-file-size.js
new file mode 100644
index 0000000000..180882c22b
--- /dev/null
+++ b/packages/zone.js/check-file-size.js
@@ -0,0 +1,28 @@
+/**
+ * @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 fs = require('fs');
+
+module.exports = function(config) {
+ let chkResult = true;
+ config.targets.forEach(target => {
+ if (target.checkTarget) {
+ try {
+ const stats = fs.statSync(target.path);
+ if (stats.size > target.limit) {
+ console.error(
+ `file ${target.path} size over limit, limit is ${target.limit}, actual is ${stats.size}`);
+ chkResult = false;
+ }
+ } catch (err) {
+ console.error(`failed to get filesize: ${target.path}`);
+ chkResult = false;
+ }
+ }
+ });
+ return chkResult;
+};
diff --git a/packages/zone.js/dist/BUILD.bazel b/packages/zone.js/dist/BUILD.bazel
new file mode 100644
index 0000000000..6d45334460
--- /dev/null
+++ b/packages/zone.js/dist/BUILD.bazel
@@ -0,0 +1,200 @@
+load("@build_bazel_rules_nodejs//:defs.bzl", "rollup_bundle")
+load("//packages/zone.js:bundles.bzl", "ES2015_BUNDLES", "ES5_BUNDLES", "ES5_GLOBAL_BUNDLES")
+
+package(default_visibility = ["//packages/zone.js:__subpackages__"])
+
+# copy this file from //lib to //dist
+genrule(
+ name = "zone_externs",
+ srcs = ["//packages/zone.js/lib:closure/zone_externs.js"],
+ outs = ["zone_externs.js"],
+ cmd = "cp $< $@",
+)
+
+genrule(
+ name = "zone_d_ts",
+ srcs = ["//packages/zone.js/lib"],
+ outs = ["zone.js.d.ts"],
+ cmd = "find $(SRCS) -name \"zone.d.ts\" -exec cp {} $(@D)/zone.js.d.ts \;",
+)
+
+[
+ rollup_bundle(
+ name = b[0].replace("-", "_") + "_rollup",
+ entry_point = b[1] + ".ts",
+ globals = {
+ "electron": "electron",
+ },
+ license_banner = "//packages:license-banner.txt",
+ deps = [
+ "//packages/zone.js/lib",
+ ],
+ )
+ for b in ES5_BUNDLES.items()
+]
+
+[
+ rollup_bundle(
+ name = b[0].replace("-", "_") + "_rollup",
+ entry_point = b[1] + ".ts",
+ global_name = "Zone",
+ license_banner = "//packages:license-banner.txt",
+ deps = [
+ "//packages/zone.js/lib",
+ ],
+ )
+ for b in ES5_GLOBAL_BUNDLES.items() + ES2015_BUNDLES.items()
+]
+
+# the es5 filegroups
+[
+ filegroup(
+ name = b[0] + ".es5",
+ srcs = [":" + b[0].replace("-", "_") + "_rollup"],
+ output_group = "es5_umd",
+ )
+ for b in ES5_BUNDLES.items() + ES5_GLOBAL_BUNDLES.items()
+]
+
+# the es5.min filegroups
+[
+ filegroup(
+ name = b[0] + ".es5.min",
+ srcs = [":" + b[0].replace("-", "_") + "_rollup"],
+ output_group = "es5_umd_min",
+ )
+ for b in ES5_BUNDLES.items() + ES5_GLOBAL_BUNDLES.items()
+]
+
+# the es2015 filegroups
+[
+ filegroup(
+ name = b[0] + ".umd",
+ srcs = [":" + b[0].replace("-", "_") + "_rollup"],
+ output_group = "umd",
+ )
+ for b in ES2015_BUNDLES.items()
+]
+
+# the es2015.min filegroups
+[
+ filegroup(
+ name = b[0] + ".umd.min",
+ srcs = [":" + b[0].replace("-", "_") + "_rollup"],
+ output_group = "umd_min",
+ )
+ for b in ES2015_BUNDLES.items()
+]
+
+# Extract and rename each es5 bundle to a .js and .min.js in the dist/ dir
+[
+ genrule(
+ name = b[0] + "-dist",
+ srcs = [
+ b[0] + ".es5",
+ b[0] + ".es5.min",
+ ],
+ outs = [
+ b[0] + ".js",
+ b[0] + ".min.js",
+ ],
+ cmd = " && ".join([
+ "mkdir -p $(@D)",
+ "cp $(@D)/" + b[0].replace("-", "_") + "_rollup.es5umd.js $(@D)/" + b[0] + ".js",
+ "cp $(@D)/" + b[0].replace("-", "_") + "_rollup.min.es5umd.js $(@D)/" + b[0] + ".min.js",
+ ]),
+ )
+ for b in ES5_BUNDLES.items() + ES5_GLOBAL_BUNDLES.items()
+]
+
+# Extract and rename each es5 bundle to a .js and .min.js in the dist/ dir
+[
+ genrule(
+ name = b[0] + "-dist-dev-test",
+ srcs = [
+ b[0] + ".es5",
+ ],
+ outs = [
+ b[0] + ".dev.test.js",
+ ],
+ cmd = " && ".join([
+ "mkdir -p $(@D)",
+ "cp $(@D)/" + b[0].replace("-", "_") + "_rollup.es5umd.js $(@D)/" + b[0] + ".dev.test.js",
+ ]),
+ )
+ for b in ES5_BUNDLES.items() + ES5_GLOBAL_BUNDLES.items()
+]
+
+[
+ genrule(
+ name = b + "-dist-dev-test",
+ srcs = [
+ b + ".umd",
+ ],
+ outs = [
+ b + ".dev.test.js",
+ ],
+ cmd = " && ".join([
+ "mkdir -p $(@D)",
+ "cp $(@D)/" + b.replace("-", "_") + "_rollup.umd.js $(@D)/" + b + ".dev.test.js",
+ ]),
+ )
+ for b in ES2015_BUNDLES
+]
+
+# Extract and rename each es5 bundle to a .js and .min.js in the dist/ dir
+[
+ genrule(
+ name = b[0] + "-dist-test",
+ srcs = [
+ b[0] + ".es5.min",
+ ],
+ outs = [
+ b[0] + ".test.min.js",
+ ],
+ cmd = " && ".join([
+ "mkdir -p $(@D)",
+ "cp $(@D)/" + b[0].replace("-", "_") + "_rollup.min.es5umd.js $(@D)/" + b[0] + ".test.min.js",
+ ]),
+ )
+ for b in ES5_BUNDLES.items() + ES5_GLOBAL_BUNDLES.items()
+]
+
+# Extract and rename each es2015 bundle to a .js and .min.js in the dist/ dir
+[
+ genrule(
+ name = b + "-dist",
+ srcs = [
+ b + ".umd",
+ b + ".umd.min",
+ ],
+ outs = [
+ b + ".js",
+ b + ".min.js",
+ ],
+ cmd = " && ".join([
+ "mkdir -p $(@D)",
+ "cp $(@D)/" + b.replace("-", "_") + "_rollup.umd.js $(@D)/" + b + ".js",
+ "cp $(@D)/" + b.replace("-", "_") + "_rollup.min.umd.js $(@D)/" + b + ".min.js",
+ ]),
+ )
+ for b in ES2015_BUNDLES
+]
+
+# Extract and rename each es5 bundle to a .js and .min.js in the dist/ dir
+[
+ genrule(
+ name = b + "-dist-test",
+ srcs = [
+ b + ".umd.min",
+ ],
+ outs = [
+ b + ".test.min.js",
+ ],
+ cmd = " && ".join([
+ "mkdir -p $(@D)",
+ "cp $(@D)/" + b.replace("-", "_") + "_rollup.min.umd.js $(@D)/" + b + ".test.min.js",
+ ]),
+ )
+ for b in ES2015_BUNDLES
+]
diff --git a/packages/zone.js/doc/error.png b/packages/zone.js/doc/error.png
new file mode 100644
index 0000000000..e1344e25a1
Binary files /dev/null and b/packages/zone.js/doc/error.png differ
diff --git a/packages/zone.js/doc/error.puml b/packages/zone.js/doc/error.puml
new file mode 100644
index 0000000000..f3db461865
--- /dev/null
+++ b/packages/zone.js/doc/error.puml
@@ -0,0 +1,9 @@
+@startuml
+scheduling --> unknown: zoneSpec.onScheduleTask\nor task.scheduleFn\nthrow error
+running --> scheduled: error in \ntask.callback\nand task is\nperiodical\ntask
+running --> notScheduled: error in\ntask.callback\nand\ntask is not\nperiodical
+running: zoneSpec.onHandleError
+running --> throw: error in\n task.callback\n and \nzoneSpec.onHandleError\n return true
+canceling --> unknown: zoneSpec.onCancelTask\n or task.cancelFn\n throw error
+unknown --> throw
+@enduml
\ No newline at end of file
diff --git a/packages/zone.js/doc/eventtask.png b/packages/zone.js/doc/eventtask.png
new file mode 100644
index 0000000000..eb984aee38
Binary files /dev/null and b/packages/zone.js/doc/eventtask.png differ
diff --git a/packages/zone.js/doc/eventtask.puml b/packages/zone.js/doc/eventtask.puml
new file mode 100644
index 0000000000..6ba4fe3b2f
--- /dev/null
+++ b/packages/zone.js/doc/eventtask.puml
@@ -0,0 +1,21 @@
+@startuml
+[*] --> notScheduled: initialize
+notScheduled --> scheduling: addEventListener
+
+scheduling: zoneSpec.onScheduleTask
+scheduling: zoneSpec.onHasTask
+
+scheduling --> scheduled
+scheduled --> running: event\n triggered
+running: zoneSpec:onInvokeTask
+
+scheduled --> canceling: removeEventListener
+canceling: zoneSpec.onCancelTask
+canceling --> notScheduled
+canceling: zoneSpec.onHasTask
+
+running --> scheduled: callback\n finished
+running: zoneSpec.onHasTask
+running --> canceling: removeEventListener
+
+@enduml
\ No newline at end of file
diff --git a/packages/zone.js/doc/microtask.png b/packages/zone.js/doc/microtask.png
new file mode 100644
index 0000000000..330359490e
Binary files /dev/null and b/packages/zone.js/doc/microtask.png differ
diff --git a/packages/zone.js/doc/microtask.puml b/packages/zone.js/doc/microtask.puml
new file mode 100644
index 0000000000..0529d65032
--- /dev/null
+++ b/packages/zone.js/doc/microtask.puml
@@ -0,0 +1,14 @@
+@startuml
+[*] --> notScheduled: initialize
+notScheduled --> scheduling: promise.then/\nprocess.nextTick\nand so on
+
+scheduling: zoneSpec.onScheduleTask
+scheduling: zoneSpec.onHasTask
+
+scheduling --> scheduled
+scheduled --> running: callback
+running: zoneSpec:onInvokeTask
+
+running --> notScheduled
+running: zoneSpec.onHasTask
+@enduml
\ No newline at end of file
diff --git a/packages/zone.js/doc/non-periodical-macrotask.png b/packages/zone.js/doc/non-periodical-macrotask.png
new file mode 100644
index 0000000000..2fdcc1127b
Binary files /dev/null and b/packages/zone.js/doc/non-periodical-macrotask.png differ
diff --git a/packages/zone.js/doc/non-periodical-macrotask.puml b/packages/zone.js/doc/non-periodical-macrotask.puml
new file mode 100644
index 0000000000..a8d0456c45
--- /dev/null
+++ b/packages/zone.js/doc/non-periodical-macrotask.puml
@@ -0,0 +1,18 @@
+@startuml
+[*] --> notScheduled: initialize
+notScheduled --> scheduling: setTimeout/\nXMLHttpRequest.send\nand so on
+
+scheduling: zoneSpec.onScheduleTask
+scheduling: zoneSpec.onHasTask
+
+scheduling --> scheduled
+scheduled --> running: timeout callback\nreadystatechange\ncallback
+running: zoneSpec:onInvokeTask
+
+scheduled --> canceling: clearTimeout\n/abort request
+canceling: zoneSpec.onCancelTask
+canceling --> notScheduled
+canceling: zoneSpec.onHasTask
+running --> notScheduled
+running: zoneSpec.onHasTask
+@enduml
diff --git a/packages/zone.js/doc/override-task.png b/packages/zone.js/doc/override-task.png
new file mode 100644
index 0000000000..20136425e2
Binary files /dev/null and b/packages/zone.js/doc/override-task.png differ
diff --git a/packages/zone.js/doc/override-task.puml b/packages/zone.js/doc/override-task.puml
new file mode 100644
index 0000000000..d0e586539c
--- /dev/null
+++ b/packages/zone.js/doc/override-task.puml
@@ -0,0 +1,18 @@
+@startuml
+[*] --> notScheduled: initialize
+notScheduled --> scheduling: scheduleTask
+
+scheduling: zoneSpec.onScheduleTask
+scheduling: zoneSpec.onHasTask
+
+scheduling --> scheduled: override with\n anotherZone
+scheduled --> running: timeout callback\nreadystatechange\ncallback
+running: anotherZoneSpec:onInvokeTask
+
+scheduled --> canceling: clearTimeout\n/abort request
+canceling: anotherZoneSpec.onCancelTask
+canceling --> notScheduled
+canceling: zneSpec.onHasTask
+running --> notScheduled
+running: zoneSpec.onHasTask
+@enduml
\ No newline at end of file
diff --git a/packages/zone.js/doc/periodical-macrotask.png b/packages/zone.js/doc/periodical-macrotask.png
new file mode 100644
index 0000000000..e673d23ff3
Binary files /dev/null and b/packages/zone.js/doc/periodical-macrotask.png differ
diff --git a/packages/zone.js/doc/periodical-macrotask.puml b/packages/zone.js/doc/periodical-macrotask.puml
new file mode 100644
index 0000000000..905968df36
--- /dev/null
+++ b/packages/zone.js/doc/periodical-macrotask.puml
@@ -0,0 +1,18 @@
+@startuml
+[*] --> notScheduled: initialize
+notScheduled --> scheduling: setInterval
+
+scheduling: zoneSpec.onScheduleTask
+scheduling: zoneSpec.onHasTask
+
+scheduling --> scheduled
+scheduled --> running: interval\n callback
+running: zoneSpec:onInvokeTask
+
+scheduled --> canceling: clearInterval
+canceling: zoneSpec.onCancelTask
+canceling --> notScheduled
+canceling: zoneSpec.onHasTask
+running --> scheduled: callback\n finished
+running --> canceling: clearInterval
+@enduml
\ No newline at end of file
diff --git a/packages/zone.js/doc/reschedule-task.png b/packages/zone.js/doc/reschedule-task.png
new file mode 100644
index 0000000000..ba4cc71c6e
Binary files /dev/null and b/packages/zone.js/doc/reschedule-task.png differ
diff --git a/packages/zone.js/doc/reschedule-task.puml b/packages/zone.js/doc/reschedule-task.puml
new file mode 100644
index 0000000000..49d99e0cbd
--- /dev/null
+++ b/packages/zone.js/doc/reschedule-task.puml
@@ -0,0 +1,20 @@
+@startuml
+[*] --> notScheduled: initialize
+notScheduled --> scheduling: â‘ current zone\n scheduleTask
+notScheduled --> scheduling: â‘¢anotherZone\n scheduleTask
+
+scheduling: anotherZoneSpec.onScheduleTask
+scheduling: anotherZoneSpec.onHasTask
+
+scheduling --> notScheduled: â‘¡cancelScheduleRequest
+scheduling --> scheduled
+scheduled --> running: callback
+running: anotherZoneSpec:onInvokeTask
+
+scheduled --> canceling: cancelTask
+canceling: anotherZoneSpec.onCancelTask
+canceling --> notScheduled
+canceling: anotherZoneSpec.onHasTask
+running --> notScheduled
+running: anotherZoneSpec.onHasTask
+@enduml
\ No newline at end of file
diff --git a/packages/zone.js/doc/task.md b/packages/zone.js/doc/task.md
new file mode 100644
index 0000000000..4b08beb93d
--- /dev/null
+++ b/packages/zone.js/doc/task.md
@@ -0,0 +1,80 @@
+## Task lifecycle
+
+We handle several kinds of tasks in zone.js,
+
+- MacroTask
+- MicroTask
+- EventTask
+
+For details, please refer to [here](../dist/zone.js.d.ts)
+
+This document will explain the lifecycle (state-transition) of different types of tasks and also the triggering of various zonespec's callback during that cycle.
+
+The motivation to write this document has come from this [PR](https://github.com/angular/zone.js/pull/629) of @mhevery. This has made the task's state more clear. Also, tasks can now be cancelled and rescheduled in different zone.
+
+### MicroTask
+Such as Promise.then, process.nextTick, they are microTasks, the lifecycle(state transition)
+looks like this.
+
+
+
+ZoneSpec's onHasTask callback will be triggered when the first microTask were scheduled or the
+last microTask was invoked.
+
+### EventTask
+Such as EventTarget's EventListener, EventEmitter's EventListener, their lifecycle(state transition)
+looks like this.
+
+
+
+ZoneSpec's onHasTask callback will be triggered when the first eventTask were scheduled or the
+last eventTask was cancelled.
+
+EventTask will go back to scheduled state after invoked(running state), and will become notScheduled after cancelTask(such as removeEventListener)
+
+### MacroTask
+
+#### Non Periodical MacroTask
+Such as setTimeout/XMLHttpRequest, their lifecycle(state transition)
+looks like this.
+
+
+
+ZoneSpec's onHasTask callback will be triggered when the first macroTask were scheduled or the
+last macroTask was invoked or cancelled.
+
+Non periodical macroTask will become notScheduled after being invoked or being cancelled(such as clearTimeout)
+
+#### Periodical MacroTask
+Such as setInterval, their lifecycle(state transition)
+looks like this.
+
+
+
+ZoneSpec's onHasTask callback will be triggered when first macroTask was scheduled or last macroTask
+ was cancelled, it will not triggered after invoke, because it is periodical and become scheduled again.
+
+Periodical macroTask will go back to scheduled state after invoked(running state), and will become notScheduled after cancelTask(such as clearInterval)
+
+### Reschedule Task to a new zone
+Sometimes you may want to reschedule task into different zone, the lifecycle looks like
+
+
+
+the ZoneTask's cancelScheduleRequest method can be only called in onScheduleTask callback of ZoneSpec,
+because it is still under scheduling state.
+
+And after rescheduling, the task will be scheduled to new zone(the otherZoneSpec in the graph),
+and will have nothing todo with the original zone.
+
+### Override zone when scheduling
+Sometimes you may want to just override the zone when scheduling, the lifecycle looks like
+
+
+
+After overriding, the task will be invoked/cancelled in the new zone(the otherZoneSpec in the graph),
+but hasTask callback will still be invoked in original zone.
+
+### Error occurs in task lifecycle
+
+
diff --git a/packages/zone.js/example/basic.html b/packages/zone.js/example/basic.html
new file mode 100644
index 0000000000..7ea039f87f
--- /dev/null
+++ b/packages/zone.js/example/basic.html
@@ -0,0 +1,59 @@
+
+
+
+
+ Zone.js Basic Demo
+
+
+
+
+
+
+ Basic Example
+
+ Bind Error
+ Cause Error
+
+
+
+
\ No newline at end of file
diff --git a/packages/zone.js/example/benchmarks/addEventListener.html b/packages/zone.js/example/benchmarks/addEventListener.html
new file mode 100644
index 0000000000..db5d731cf2
--- /dev/null
+++ b/packages/zone.js/example/benchmarks/addEventListener.html
@@ -0,0 +1,65 @@
+
+
+
+
+ Zone.js addEventListenerBenchmark
+
+
+
+
+
+ addEventListener Benchmark
+
+ No Zone
+ Add/Remove same callback
+ Add/Remove different callback
+
+ With Zone
+ Add/Remove same callback
+ Add/Remove different callback
+
+
+
+
+
+
diff --git a/packages/zone.js/example/benchmarks/event_emitter.js b/packages/zone.js/example/benchmarks/event_emitter.js
new file mode 100644
index 0000000000..c6adb16a28
--- /dev/null
+++ b/packages/zone.js/example/benchmarks/event_emitter.js
@@ -0,0 +1,50 @@
+/**
+ * @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 events = require('events');
+const EventEmitter = events.EventEmitter;
+require('../../dist/zone-node');
+
+const emitters = [];
+const callbacks = [];
+const size = 100000;
+for (let i = 0; i < size; i++) {
+ const emitter = new EventEmitter();
+ const callback = (function(i) { return function() { console.log(i); }; })(i);
+ emitters[i] = emitter;
+ callbacks[i] = callback;
+}
+
+function addRemoveCallback(reuse, useZone) {
+ const start = new Date();
+ let callback = callbacks[0];
+ for (let i = 0; i < size; i++) {
+ const emitter = emitters[i];
+ if (!reuse) callback = callbacks[i];
+ if (useZone)
+ emitter.on('msg', callback);
+ else
+ emitter.__zone_symbol__addListener('msg', callback);
+ }
+
+ for (let i = 0; i < size; i++) {
+ const emitter = emitters[i];
+ if (!reuse) callback = callbacks[i];
+ if (useZone)
+ emitter.removeListener('msg', callback);
+ else
+ emitter.__zone_symbol__removeListener('msg', callback);
+ }
+ const end = new Date();
+ console.log(useZone ? 'use zone' : 'native', reuse ? 'reuse' : 'new');
+ console.log('Execution time: %dms', end - start);
+}
+
+addRemoveCallback(false, false);
+addRemoveCallback(false, true);
+addRemoveCallback(true, false);
+addRemoveCallback(true, true);
\ No newline at end of file
diff --git a/packages/zone.js/example/counting.html b/packages/zone.js/example/counting.html
new file mode 100644
index 0000000000..65d5ed0fcc
--- /dev/null
+++ b/packages/zone.js/example/counting.html
@@ -0,0 +1,95 @@
+
+
+
+
+ Counting Pending Tasks
+
+
+
+
+
+
+ Counting Pending Tasks
+
+ We want to know about just the events from a single mouse click
+ while a bunch of other stuff is happening on the page
+
+ This is useful in E2E testing. Because you know when there are
+ no async tasks, you avoid adding timeouts that wait for tasks that
+ run for an indeterminable amount of time.
+
+ Start
+
+
+
+
+
+
+
diff --git a/packages/zone.js/example/css/style.css b/packages/zone.js/example/css/style.css
new file mode 100644
index 0000000000..9453385b99
--- /dev/null
+++ b/packages/zone.js/example/css/style.css
@@ -0,0 +1,8 @@
+body {
+ padding: 50px;
+ font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
+}
+
+a {
+ color: #00B7FF;
+}
diff --git a/packages/zone.js/example/index.html b/packages/zone.js/example/index.html
new file mode 100644
index 0000000000..12f1cf8c50
--- /dev/null
+++ b/packages/zone.js/example/index.html
@@ -0,0 +1,21 @@
+
+
+
+
+ Zone.js Examples
+
+
+
+
+ Examples
+
+
+ Tracing user actions with long stack traces
+ Counting Tasks
+ Profiling Across Tasks
+ Throttle
+ WebSocket
+
+
+
+
diff --git a/packages/zone.js/example/js/counting-zone.js b/packages/zone.js/example/js/counting-zone.js
new file mode 100644
index 0000000000..5a3490e469
--- /dev/null
+++ b/packages/zone.js/example/js/counting-zone.js
@@ -0,0 +1,33 @@
+/*
+ * See example/counting.html
+ */
+
+Zone['countingZoneSpec'] = {
+ name: 'counterZone',
+ // setTimeout
+ onScheduleTask: function(delegate, current, target, task) {
+ this.data.count += 1;
+ delegate.scheduleTask(target, task);
+ },
+
+ // fires when...
+ // - clearTimeout
+ // - setTimeout finishes
+ onInvokeTask: function(delegate, current, target, task, applyThis, applyArgs) {
+ delegate.invokeTask(target, task, applyThis, applyArgs);
+ this.data.count -= 1;
+ },
+
+ onHasTask: function(delegate, current, target, hasTask) {
+ if (this.data.count === 0 && !this.data.flushed) {
+ this.data.flushed = true;
+ target.run(this.onFlush);
+ }
+ },
+
+ counter: function() { return this.data.count; },
+
+ data: {count: 0, flushed: false},
+
+ onFlush: function() {}
+};
diff --git a/packages/zone.js/example/profiling.html b/packages/zone.js/example/profiling.html
new file mode 100644
index 0000000000..5224c26151
--- /dev/null
+++ b/packages/zone.js/example/profiling.html
@@ -0,0 +1,126 @@
+
+
+
+
+ Zones Profiling
+
+
+
+
+
+
+
+ Profiling with Zones
+
+ Start Profiling
+
+
+
+
+
diff --git a/packages/zone.js/example/throttle.html b/packages/zone.js/example/throttle.html
new file mode 100644
index 0000000000..9dc4f61f08
--- /dev/null
+++ b/packages/zone.js/example/throttle.html
@@ -0,0 +1,91 @@
+
+
+
+
+ Zones throttle
+
+
+
+
+
+ Throttle Example
+
+ Error
+
+
+
+
+
diff --git a/packages/zone.js/example/web-socket.html b/packages/zone.js/example/web-socket.html
new file mode 100644
index 0000000000..933bab2569
--- /dev/null
+++ b/packages/zone.js/example/web-socket.html
@@ -0,0 +1,38 @@
+
+
+
+
+ WebSockets with Zones
+
+
+
+
+
+
+ Ensure that you started node test/ws-server.js
before loading
+ this page. Then check console output.
+
+
+
+
+
diff --git a/packages/zone.js/file-size-limit.json b/packages/zone.js/file-size-limit.json
new file mode 100644
index 0000000000..97c9f3a97b
--- /dev/null
+++ b/packages/zone.js/file-size-limit.json
@@ -0,0 +1,14 @@
+{
+ "targets": [
+ {
+ "path": "dist/zone-evergreen.min.js",
+ "checkTarget": true,
+ "limit": 43000
+ },
+ {
+ "path": "dist/zone.min.js",
+ "checkTarget": true,
+ "limit": 45000
+ }
+ ]
+}
diff --git a/packages/zone.js/karma-base.conf.js b/packages/zone.js/karma-base.conf.js
new file mode 100644
index 0000000000..35b218c97b
--- /dev/null
+++ b/packages/zone.js/karma-base.conf.js
@@ -0,0 +1,51 @@
+/**
+ * @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
+ */
+
+module.exports = function(config) {
+ config.set({
+ basePath: '',
+ client: {errorpolicy: config.errorpolicy},
+ files: [
+ 'node_modules/systemjs/dist/system-polyfills.js', 'node_modules/systemjs/dist/system.src.js',
+ 'node_modules/whatwg-fetch/fetch.js',
+ {pattern: 'node_modules/rxjs/**/**/*.js', included: false, watched: false},
+ {pattern: 'node_modules/rxjs/**/**/*.js.map', included: false, watched: false},
+ {pattern: 'node_modules/rxjs/**/*.js', included: false, watched: false},
+ {pattern: 'node_modules/es6-promise/**/*.js', included: false, watched: false},
+ {pattern: 'node_modules/core-js/**/*.js', included: false, watched: false},
+ {pattern: 'node_modules/rxjs/**/*.js.map', included: false, watched: false},
+ {pattern: 'test/assets/**/*.*', watched: true, served: true, included: false},
+ {pattern: 'build/**/*.js.map', watched: true, served: true, included: false},
+ {pattern: 'build/**/*.js', watched: true, served: true, included: false}
+ ],
+
+ plugins: [
+ require('karma-chrome-launcher'), require('karma-firefox-launcher'),
+ require('karma-sourcemap-loader')
+ ],
+
+ preprocessors: {'**/*.js': ['sourcemap']},
+
+ exclude: ['test/microtasks.spec.ts'],
+
+ reporters: ['progress'],
+
+ // port: 9876,
+ colors: true,
+
+ logLevel: config.LOG_INFO,
+
+ browsers: ['Chrome'],
+
+ captureTimeout: 60000,
+ retryLimit: 4,
+
+ autoWatch: true,
+ singleRun: false
+ });
+};
diff --git a/packages/zone.js/karma-build-jasmine-phantomjs.conf.js b/packages/zone.js/karma-build-jasmine-phantomjs.conf.js
new file mode 100644
index 0000000000..54989858d1
--- /dev/null
+++ b/packages/zone.js/karma-build-jasmine-phantomjs.conf.js
@@ -0,0 +1,9 @@
+
+module.exports = function(config) {
+ require('./karma-build.conf.js')(config);
+
+ config.plugins.push(require('karma-jasmine'));
+ config.plugins.push(require('karma-phantomjs-launcher'));
+ config.frameworks.push('jasmine');
+ config.browsers.splice(0, 1, ['PhantomJS']);
+};
diff --git a/packages/zone.js/karma-build-jasmine.conf.js b/packages/zone.js/karma-build-jasmine.conf.js
new file mode 100644
index 0000000000..432a215938
--- /dev/null
+++ b/packages/zone.js/karma-build-jasmine.conf.js
@@ -0,0 +1,7 @@
+
+module.exports = function(config) {
+ require('./karma-build.conf.js')(config);
+
+ config.plugins.push(require('karma-jasmine'));
+ config.frameworks.push('jasmine');
+};
diff --git a/packages/zone.js/karma-build-jasmine.es2015.conf.js b/packages/zone.js/karma-build-jasmine.es2015.conf.js
new file mode 100644
index 0000000000..6ab875d89d
--- /dev/null
+++ b/packages/zone.js/karma-build-jasmine.es2015.conf.js
@@ -0,0 +1,11 @@
+
+module.exports = function(config) {
+ require('./karma-build-jasmine.conf.js')(config);
+ for (let i = 0; i < config.files.length; i++) {
+ if (config.files[i] === 'node_modules/core-js-bundle/index.js') {
+ config.files.splice(i, 1);
+ break;
+ }
+ }
+ config.client.entrypoint = 'browser_es2015_entry_point';
+};
diff --git a/packages/zone.js/karma-build-mocha.conf.js b/packages/zone.js/karma-build-mocha.conf.js
new file mode 100644
index 0000000000..44ff081948
--- /dev/null
+++ b/packages/zone.js/karma-build-mocha.conf.js
@@ -0,0 +1,11 @@
+
+module.exports = function(config) {
+ require('./karma-build.conf.js')(config);
+
+ config.plugins.push(require('karma-mocha'));
+ config.frameworks.push('mocha');
+ config.client.mocha = {
+ timeout: 5000 // copied timeout for Jasmine in WebSocket.spec (otherwise Mochas default timeout
+ // at 2 sec is to low for the tests)
+ };
+};
diff --git a/packages/zone.js/karma-build-sauce-mocha.conf.js b/packages/zone.js/karma-build-sauce-mocha.conf.js
new file mode 100644
index 0000000000..7e44d3bd9c
--- /dev/null
+++ b/packages/zone.js/karma-build-sauce-mocha.conf.js
@@ -0,0 +1,12 @@
+/**
+ * @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
+ */
+
+module.exports = function(config) {
+ require('./karma-dist-mocha.conf.js')(config);
+ require('./sauce.conf')(config);
+};
diff --git a/packages/zone.js/karma-build-sauce-selenium3-mocha.conf.js b/packages/zone.js/karma-build-sauce-selenium3-mocha.conf.js
new file mode 100644
index 0000000000..73ddbf40f5
--- /dev/null
+++ b/packages/zone.js/karma-build-sauce-selenium3-mocha.conf.js
@@ -0,0 +1,12 @@
+/**
+ * @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
+ */
+
+module.exports = function(config) {
+ require('./karma-dist-mocha.conf.js')(config);
+ require('./sauce-selenium3.conf')(config, ['SL_IE9']);
+};
diff --git a/packages/zone.js/karma-build.conf.js b/packages/zone.js/karma-build.conf.js
new file mode 100644
index 0000000000..a3e2d2503e
--- /dev/null
+++ b/packages/zone.js/karma-build.conf.js
@@ -0,0 +1,18 @@
+/**
+ * @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
+ */
+
+module.exports = function(config) {
+ require('./karma-base.conf.js')(config);
+ config.files.push('node_modules/core-js-bundle/index.js');
+ config.files.push('build/test/browser-env-setup.js');
+ config.files.push('build/test/wtf_mock.js');
+ config.files.push('build/test/test_fake_polyfill.js');
+ config.files.push('build/lib/zone.js');
+ config.files.push('build/lib/common/promise.js');
+ config.files.push('build/test/main.js');
+};
diff --git a/packages/zone.js/karma-dist-jasmine.conf.js b/packages/zone.js/karma-dist-jasmine.conf.js
new file mode 100644
index 0000000000..32627d46ea
--- /dev/null
+++ b/packages/zone.js/karma-dist-jasmine.conf.js
@@ -0,0 +1,7 @@
+
+module.exports = function(config) {
+ require('./karma-dist.conf.js')(config);
+
+ config.plugins.push(require('karma-jasmine'));
+ config.frameworks.push('jasmine');
+};
diff --git a/packages/zone.js/karma-dist-mocha.conf.js b/packages/zone.js/karma-dist-mocha.conf.js
new file mode 100644
index 0000000000..a7dcf6c7ea
--- /dev/null
+++ b/packages/zone.js/karma-dist-mocha.conf.js
@@ -0,0 +1,23 @@
+
+module.exports = function(config) {
+ require('./karma-dist.conf.js')(config);
+
+ for (let i = 0; i < config.files.length; i++) {
+ if (config.files[i] === 'dist/zone-testing.js') {
+ config.files.splice(i, 1);
+ break;
+ }
+ }
+ config.files.push('dist/long-stack-trace-zone.js');
+ config.files.push('dist/proxy.js');
+ config.files.push('dist/sync-test.js');
+ config.files.push('dist/async-test.js');
+ config.files.push('dist/fake-async-test.js');
+ config.files.push('dist/zone-patch-promise-test.js');
+ config.plugins.push(require('karma-mocha'));
+ config.frameworks.push('mocha');
+ config.client.mocha = {
+ timeout: 5000 // copied timeout for Jasmine in WebSocket.spec (otherwise Mochas default timeout
+ // at 2 sec is to low for the tests)
+ };
+};
diff --git a/packages/zone.js/karma-dist-sauce-jasmine.conf.js b/packages/zone.js/karma-dist-sauce-jasmine.conf.js
new file mode 100644
index 0000000000..3e4d24b620
--- /dev/null
+++ b/packages/zone.js/karma-dist-sauce-jasmine.conf.js
@@ -0,0 +1,12 @@
+/**
+ * @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
+ */
+
+module.exports = function(config) {
+ require('./karma-dist-jasmine.conf.js')(config);
+ require('./sauce.conf')(config, ['SL_IOS9']);
+};
diff --git a/packages/zone.js/karma-dist-sauce-jasmine.es2015.conf.js b/packages/zone.js/karma-dist-sauce-jasmine.es2015.conf.js
new file mode 100644
index 0000000000..874076ad58
--- /dev/null
+++ b/packages/zone.js/karma-dist-sauce-jasmine.es2015.conf.js
@@ -0,0 +1,28 @@
+
+module.exports = function(config) {
+ require('./karma-dist-jasmine.conf.js')(config);
+ require('./sauce.es2015.conf')(config);
+ const files = config.files;
+ config.files = [];
+ for (let i = 0; i < files.length; i++) {
+ if (files[i] !== 'node_modules/core-js-bundle/index.js' || files[i] === 'build/test/main.js') {
+ config.files.push(files[i]);
+ }
+ }
+ 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({pattern: 'dist/zone-evergreen.js', type: 'module'});
+ config.files.push('dist/zone-patch-canvas.js');
+ config.files.push('dist/zone-patch-fetch.js');
+ config.files.push('dist/webapis-media-query.js');
+ config.files.push('dist/webapis-notification.js');
+ config.files.push('dist/zone-patch-user-media.js');
+ config.files.push('dist/zone-patch-resize-observer.js');
+ config.files.push('dist/task-tracking.js');
+ config.files.push('dist/wtf.js');
+ config.files.push('dist/zone-testing.js');
+ config.files.push('build/test/test-env-setup-jasmine.js');
+ config.files.push('build/lib/common/error-rewrite.js');
+ config.files.push('build/test/browser/custom-element.spec.js');
+};
diff --git a/packages/zone.js/karma-dist-sauce-jasmine3.conf.js b/packages/zone.js/karma-dist-sauce-jasmine3.conf.js
new file mode 100644
index 0000000000..61559bf6fe
--- /dev/null
+++ b/packages/zone.js/karma-dist-sauce-jasmine3.conf.js
@@ -0,0 +1,16 @@
+/**
+ * @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
+ */
+
+module.exports = function(config) {
+ require('./karma-dist-jasmine.conf.js')(config);
+ require('./sauce.conf')(config, [
+ 'SL_IOS9', 'SL_CHROME', 'SL_FIREFOX_54', 'SL_SAFARI8', 'SL_SAFARI9', 'SL_SAFARI10', 'SL_IOS8',
+ 'SL_IOS9', 'SL_IOS10', 'SL_IE9', 'SL_IE10', 'SL_IE11', 'SL_MSEDGE15', 'SL_ANDROID4.4',
+ 'SL_ANDROID5.1'
+ ])
+};
diff --git a/packages/zone.js/karma-dist-sauce-selenium3-jasmine.conf.js b/packages/zone.js/karma-dist-sauce-selenium3-jasmine.conf.js
new file mode 100644
index 0000000000..9acc12022e
--- /dev/null
+++ b/packages/zone.js/karma-dist-sauce-selenium3-jasmine.conf.js
@@ -0,0 +1,12 @@
+/**
+ * @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
+ */
+
+module.exports = function(config) {
+ require('./karma-dist-jasmine.conf.js')(config);
+ require('./sauce-selenium3.conf')(config);
+};
diff --git a/packages/zone.js/karma-dist.conf.js b/packages/zone.js/karma-dist.conf.js
new file mode 100644
index 0000000000..f592296ca1
--- /dev/null
+++ b/packages/zone.js/karma-dist.conf.js
@@ -0,0 +1,27 @@
+/**
+ * @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
+ */
+
+module.exports = function(config) {
+ require('./karma-base.conf.js')(config);
+ config.files.push('node_modules/core-js-bundle/index.js');
+ config.files.push('build/test/browser-env-setup.js');
+ 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-fetch.js');
+ config.files.push('dist/zone-patch-canvas.js');
+ config.files.push('dist/webapis-media-query.js');
+ config.files.push('dist/webapis-notification.js');
+ config.files.push('dist/zone-patch-user-media.js');
+ config.files.push('dist/zone-patch-resize-observer.js');
+ config.files.push('dist/task-tracking.js');
+ config.files.push('dist/wtf.js');
+ config.files.push('dist/zone-testing.js');
+ config.files.push('build/test/main.js');
+};
diff --git a/packages/zone.js/karma-evergreen-dist-jasmine.conf.js b/packages/zone.js/karma-evergreen-dist-jasmine.conf.js
new file mode 100644
index 0000000000..f7df41e676
--- /dev/null
+++ b/packages/zone.js/karma-evergreen-dist-jasmine.conf.js
@@ -0,0 +1,7 @@
+
+module.exports = function(config) {
+ require('./karma-evergreen-dist.conf.js')(config);
+
+ config.plugins.push(require('karma-jasmine'));
+ config.frameworks.push('jasmine');
+};
diff --git a/packages/zone.js/karma-evergreen-dist-sauce-jasmine.conf.js b/packages/zone.js/karma-evergreen-dist-sauce-jasmine.conf.js
new file mode 100644
index 0000000000..855dfef35a
--- /dev/null
+++ b/packages/zone.js/karma-evergreen-dist-sauce-jasmine.conf.js
@@ -0,0 +1,12 @@
+/**
+ * @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
+ */
+
+module.exports = function(config) {
+ require('./karma-evergreen-dist-jasmine.conf.js')(config);
+ require('./sauce-evergreen.conf')(config);
+};
diff --git a/packages/zone.js/karma-evergreen-dist.conf.js b/packages/zone.js/karma-evergreen-dist.conf.js
new file mode 100644
index 0000000000..d1f4d22677
--- /dev/null
+++ b/packages/zone.js/karma-evergreen-dist.conf.js
@@ -0,0 +1,35 @@
+/**
+ * @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
+ */
+
+module.exports = function(config) {
+ require('./karma-base.conf.js')(config);
+ const files = config.files;
+ config.files = [];
+ for (let i = 0; i < files.length; i++) {
+ if (files[i] !== 'node_modules/core-js-bundle/index.js') {
+ config.files.push(files[i]);
+ }
+ }
+
+ config.files.push('build/test/browser-env-setup.js');
+ 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({pattern: 'dist/zone-evergreen.js', type: 'module'});
+ config.files.push('dist/zone-patch-canvas.js');
+ config.files.push('dist/zone-patch-fetch.js');
+ config.files.push('dist/webapis-media-query.js');
+ config.files.push('dist/webapis-notification.js');
+ config.files.push('dist/zone-patch-user-media.js');
+ config.files.push('dist/zone-patch-resize-observer.js');
+ config.files.push('dist/task-tracking.js');
+ config.files.push('dist/wtf.js');
+ config.files.push('dist/zone-testing.js');
+ config.files.push({pattern: 'build/test/browser/custom-element.spec.js', type: 'module'});
+ config.files.push('build/test/main.js');
+};
diff --git a/packages/zone.js/lib/BUILD.bazel b/packages/zone.js/lib/BUILD.bazel
new file mode 100644
index 0000000000..9ad6945583
--- /dev/null
+++ b/packages/zone.js/lib/BUILD.bazel
@@ -0,0 +1,18 @@
+load("@npm_bazel_typescript//:defs.bzl", "ts_library")
+
+package(default_visibility = ["//packages/zone.js:__pkg__"])
+
+exports_files(glob([
+ "**/*",
+]))
+
+ts_library(
+ name = "lib",
+ srcs = glob(["**/*.ts"]),
+ visibility = ["//packages/zone.js:__subpackages__"],
+ deps = [
+ "@npm//@types/jasmine",
+ "@npm//@types/node",
+ "@npm//rxjs",
+ ],
+)
diff --git a/packages/zone.js/lib/browser/api-util.ts b/packages/zone.js/lib/browser/api-util.ts
new file mode 100644
index 0000000000..6c3e90fcb3
--- /dev/null
+++ b/packages/zone.js/lib/browser/api-util.ts
@@ -0,0 +1,52 @@
+/**
+ * @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
+ */
+
+import {globalSources, patchEventPrototype, patchEventTarget, zoneSymbolEventNames} from '../common/events';
+import {ADD_EVENT_LISTENER_STR, ArraySlice, FALSE_STR, ObjectCreate, ObjectDefineProperty, ObjectGetOwnPropertyDescriptor, REMOVE_EVENT_LISTENER_STR, TRUE_STR, ZONE_SYMBOL_PREFIX, attachOriginToPatched, bindArguments, isBrowser, isIEOrEdge, isMix, isNode, patchClass, patchMacroTask, patchMethod, patchOnProperties, wrapWithCurrentZone} from '../common/utils';
+
+import {patchCallbacks} from './browser-util';
+import {_redefineProperty} from './define-property';
+import {eventNames, filterProperties} from './property-descriptor';
+
+Zone.__load_patch('util', (global: any, Zone: ZoneType, api: _ZonePrivate) => {
+ api.patchOnProperties = patchOnProperties;
+ api.patchMethod = patchMethod;
+ api.bindArguments = bindArguments;
+ api.patchMacroTask = patchMacroTask;
+ // In earlier version of zone.js (<0.9.0), we use env name `__zone_symbol__BLACK_LISTED_EVENTS` to
+ // define which events will not be patched by `Zone.js`.
+ // In newer version (>=0.9.0), we change the env name to `__zone_symbol__UNPATCHED_EVENTS` to keep
+ // the name consistent with angular repo.
+ // The `__zone_symbol__BLACK_LISTED_EVENTS` is deprecated, but it is still be supported for
+ // backwards compatibility.
+ const SYMBOL_BLACK_LISTED_EVENTS = Zone.__symbol__('BLACK_LISTED_EVENTS');
+ const SYMBOL_UNPATCHED_EVENTS = Zone.__symbol__('UNPATCHED_EVENTS');
+ if (global[SYMBOL_UNPATCHED_EVENTS]) {
+ global[SYMBOL_BLACK_LISTED_EVENTS] = global[SYMBOL_UNPATCHED_EVENTS];
+ }
+ if (global[SYMBOL_BLACK_LISTED_EVENTS]) {
+ (Zone as any)[SYMBOL_BLACK_LISTED_EVENTS] = (Zone as any)[SYMBOL_UNPATCHED_EVENTS] =
+ global[SYMBOL_BLACK_LISTED_EVENTS];
+ }
+ api.patchEventPrototype = patchEventPrototype;
+ api.patchEventTarget = patchEventTarget;
+ api.isIEOrEdge = isIEOrEdge;
+ api.ObjectDefineProperty = ObjectDefineProperty;
+ api.ObjectGetOwnPropertyDescriptor = ObjectGetOwnPropertyDescriptor;
+ api.ObjectCreate = ObjectCreate;
+ api.ArraySlice = ArraySlice;
+ api.patchClass = patchClass;
+ api.wrapWithCurrentZone = wrapWithCurrentZone;
+ api.filterProperties = filterProperties;
+ api.attachOriginToPatched = attachOriginToPatched;
+ api._redefineProperty = _redefineProperty;
+ api.patchCallbacks = patchCallbacks;
+ api.getGlobalObjects = () =>
+ ({globalSources, zoneSymbolEventNames, eventNames, isBrowser, isMix, isNode, TRUE_STR,
+ FALSE_STR, ZONE_SYMBOL_PREFIX, ADD_EVENT_LISTENER_STR, REMOVE_EVENT_LISTENER_STR});
+});
diff --git a/packages/zone.js/lib/browser/browser-legacy.ts b/packages/zone.js/lib/browser/browser-legacy.ts
new file mode 100644
index 0000000000..c01c6b80db
--- /dev/null
+++ b/packages/zone.js/lib/browser/browser-legacy.ts
@@ -0,0 +1,31 @@
+/**
+ * @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
+ */
+/**
+ * @fileoverview
+ * @suppress {missingRequire}
+ */
+
+import {eventTargetLegacyPatch} from './event-target-legacy';
+import {propertyDescriptorLegacyPatch} from './property-descriptor-legacy';
+import {registerElementPatch} from './register-element';
+
+(function(_global: any) {
+ _global[Zone.__symbol__('legacyPatch')] = function() {
+ const Zone = _global['Zone'];
+ Zone.__load_patch('registerElement', (global: any, Zone: ZoneType, api: _ZonePrivate) => {
+ registerElementPatch(global, api);
+ });
+
+ Zone.__load_patch('EventTargetLegacy', (global: any, Zone: ZoneType, api: _ZonePrivate) => {
+ eventTargetLegacyPatch(global, api);
+ propertyDescriptorLegacyPatch(api, global);
+ });
+ };
+})(typeof window !== 'undefined' ?
+ window :
+ typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {});
diff --git a/packages/zone.js/lib/browser/browser-util.ts b/packages/zone.js/lib/browser/browser-util.ts
new file mode 100644
index 0000000000..99c453030a
--- /dev/null
+++ b/packages/zone.js/lib/browser/browser-util.ts
@@ -0,0 +1,38 @@
+/**
+ * @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
+ */
+export function patchCallbacks(
+ api: _ZonePrivate, target: any, targetName: string, method: string, callbacks: string[]) {
+ const symbol = Zone.__symbol__(method);
+ if (target[symbol]) {
+ return;
+ }
+ const nativeDelegate = target[symbol] = target[method];
+ target[method] = function(name: any, opts: any, options?: any) {
+ if (opts && opts.prototype) {
+ callbacks.forEach(function(callback) {
+ const source = `${targetName}.${method}::` + callback;
+ const prototype = opts.prototype;
+ if (prototype.hasOwnProperty(callback)) {
+ const descriptor = api.ObjectGetOwnPropertyDescriptor(prototype, callback);
+ if (descriptor && descriptor.value) {
+ descriptor.value = api.wrapWithCurrentZone(descriptor.value, source);
+ api._redefineProperty(opts.prototype, callback, descriptor);
+ } else if (prototype[callback]) {
+ prototype[callback] = api.wrapWithCurrentZone(prototype[callback], source);
+ }
+ } else if (prototype[callback]) {
+ prototype[callback] = api.wrapWithCurrentZone(prototype[callback], source);
+ }
+ });
+ }
+
+ return nativeDelegate.call(target, name, opts, options);
+ };
+
+ api.attachOriginToPatched(target[method], nativeDelegate);
+}
diff --git a/packages/zone.js/lib/browser/browser.ts b/packages/zone.js/lib/browser/browser.ts
new file mode 100644
index 0000000000..337f685b8e
--- /dev/null
+++ b/packages/zone.js/lib/browser/browser.ts
@@ -0,0 +1,280 @@
+/**
+ * @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
+ */
+/**
+ * @fileoverview
+ * @suppress {missingRequire}
+ */
+
+import {findEventTasks} from '../common/events';
+import {patchTimer} from '../common/timers';
+import {ZONE_SYMBOL_ADD_EVENT_LISTENER, ZONE_SYMBOL_REMOVE_EVENT_LISTENER, patchClass, patchMethod, patchPrototype, scheduleMacroTaskWithCurrentZone, zoneSymbol} from '../common/utils';
+
+import {patchCustomElements} from './custom-elements';
+import {propertyPatch} from './define-property';
+import {eventTargetPatch, patchEvent} from './event-target';
+import {propertyDescriptorPatch} from './property-descriptor';
+
+Zone.__load_patch('legacy', (global: any) => {
+ const legacyPatch = global[Zone.__symbol__('legacyPatch')];
+ if (legacyPatch) {
+ legacyPatch();
+ }
+});
+
+Zone.__load_patch('timers', (global: any) => {
+ const set = 'set';
+ const clear = 'clear';
+ patchTimer(global, set, clear, 'Timeout');
+ patchTimer(global, set, clear, 'Interval');
+ patchTimer(global, set, clear, 'Immediate');
+});
+
+Zone.__load_patch('requestAnimationFrame', (global: any) => {
+ patchTimer(global, 'request', 'cancel', 'AnimationFrame');
+ patchTimer(global, 'mozRequest', 'mozCancel', 'AnimationFrame');
+ patchTimer(global, 'webkitRequest', 'webkitCancel', 'AnimationFrame');
+});
+
+Zone.__load_patch('blocking', (global: any, Zone: ZoneType) => {
+ const blockingMethods = ['alert', 'prompt', 'confirm'];
+ for (let i = 0; i < blockingMethods.length; i++) {
+ const name = blockingMethods[i];
+ patchMethod(global, name, (delegate, symbol, name) => {
+ return function(s: any, args: any[]) {
+ return Zone.current.run(delegate, global, args, name);
+ };
+ });
+ }
+});
+
+Zone.__load_patch('EventTarget', (global: any, Zone: ZoneType, api: _ZonePrivate) => {
+ patchEvent(global, api);
+ eventTargetPatch(global, api);
+ // patch XMLHttpRequestEventTarget's addEventListener/removeEventListener
+ const XMLHttpRequestEventTarget = (global as any)['XMLHttpRequestEventTarget'];
+ if (XMLHttpRequestEventTarget && XMLHttpRequestEventTarget.prototype) {
+ api.patchEventTarget(global, [XMLHttpRequestEventTarget.prototype]);
+ }
+ patchClass('MutationObserver');
+ patchClass('WebKitMutationObserver');
+ patchClass('IntersectionObserver');
+ patchClass('FileReader');
+});
+
+Zone.__load_patch('on_property', (global: any, Zone: ZoneType, api: _ZonePrivate) => {
+ propertyDescriptorPatch(api, global);
+ propertyPatch();
+});
+
+Zone.__load_patch('customElements', (global: any, Zone: ZoneType, api: _ZonePrivate) => {
+ patchCustomElements(global, api);
+});
+
+Zone.__load_patch('XHR', (global: any, Zone: ZoneType) => {
+ // Treat XMLHttpRequest as a macrotask.
+ patchXHR(global);
+
+ const XHR_TASK = zoneSymbol('xhrTask');
+ const XHR_SYNC = zoneSymbol('xhrSync');
+ const XHR_LISTENER = zoneSymbol('xhrListener');
+ const XHR_SCHEDULED = zoneSymbol('xhrScheduled');
+ const XHR_URL = zoneSymbol('xhrURL');
+ const XHR_ERROR_BEFORE_SCHEDULED = zoneSymbol('xhrErrorBeforeScheduled');
+
+ interface XHROptions extends TaskData {
+ target: any;
+ url: string;
+ args: any[];
+ aborted: boolean;
+ }
+
+ function patchXHR(window: any) {
+ const XMLHttpRequest = window['XMLHttpRequest'];
+ if (!XMLHttpRequest) {
+ // XMLHttpRequest is not available in service worker
+ return;
+ }
+ const XMLHttpRequestPrototype: any = XMLHttpRequest.prototype;
+
+ function findPendingTask(target: any) { return target[XHR_TASK]; }
+
+ let oriAddListener = XMLHttpRequestPrototype[ZONE_SYMBOL_ADD_EVENT_LISTENER];
+ let oriRemoveListener = XMLHttpRequestPrototype[ZONE_SYMBOL_REMOVE_EVENT_LISTENER];
+ if (!oriAddListener) {
+ const XMLHttpRequestEventTarget = window['XMLHttpRequestEventTarget'];
+ if (XMLHttpRequestEventTarget) {
+ const XMLHttpRequestEventTargetPrototype = XMLHttpRequestEventTarget.prototype;
+ oriAddListener = XMLHttpRequestEventTargetPrototype[ZONE_SYMBOL_ADD_EVENT_LISTENER];
+ oriRemoveListener = XMLHttpRequestEventTargetPrototype[ZONE_SYMBOL_REMOVE_EVENT_LISTENER];
+ }
+ }
+
+ const READY_STATE_CHANGE = 'readystatechange';
+ const SCHEDULED = 'scheduled';
+
+ function scheduleTask(task: Task) {
+ const data = task.data;
+ const target = data.target;
+ target[XHR_SCHEDULED] = false;
+ target[XHR_ERROR_BEFORE_SCHEDULED] = false;
+ // remove existing event listener
+ const listener = target[XHR_LISTENER];
+ if (!oriAddListener) {
+ oriAddListener = target[ZONE_SYMBOL_ADD_EVENT_LISTENER];
+ oriRemoveListener = target[ZONE_SYMBOL_REMOVE_EVENT_LISTENER];
+ }
+
+ if (listener) {
+ oriRemoveListener.call(target, READY_STATE_CHANGE, listener);
+ }
+ const newListener = target[XHR_LISTENER] = () => {
+ if (target.readyState === target.DONE) {
+ // sometimes on some browsers XMLHttpRequest will fire onreadystatechange with
+ // readyState=4 multiple times, so we need to check task state here
+ if (!data.aborted && target[XHR_SCHEDULED] && task.state === SCHEDULED) {
+ // check whether the xhr has registered onload listener
+ // if that is the case, the task should invoke after all
+ // onload listeners finish.
+ const loadTasks = target[Zone.__symbol__('loadfalse')];
+ if (loadTasks && loadTasks.length > 0) {
+ const oriInvoke = task.invoke;
+ task.invoke = function() {
+ // need to load the tasks again, because in other
+ // load listener, they may remove themselves
+ const loadTasks = target[Zone.__symbol__('loadfalse')];
+ for (let i = 0; i < loadTasks.length; i++) {
+ if (loadTasks[i] === task) {
+ loadTasks.splice(i, 1);
+ }
+ }
+ if (!data.aborted && task.state === SCHEDULED) {
+ oriInvoke.call(task);
+ }
+ };
+ loadTasks.push(task);
+ } else {
+ task.invoke();
+ }
+ } else if (!data.aborted && target[XHR_SCHEDULED] === false) {
+ // error occurs when xhr.send()
+ target[XHR_ERROR_BEFORE_SCHEDULED] = true;
+ }
+ }
+ };
+ oriAddListener.call(target, READY_STATE_CHANGE, newListener);
+
+ const storedTask: Task = target[XHR_TASK];
+ if (!storedTask) {
+ target[XHR_TASK] = task;
+ }
+ sendNative !.apply(target, data.args);
+ target[XHR_SCHEDULED] = true;
+ return task;
+ }
+
+ function placeholderCallback() {}
+
+ function clearTask(task: Task) {
+ const data = task.data;
+ // Note - ideally, we would call data.target.removeEventListener here, but it's too late
+ // to prevent it from firing. So instead, we store info for the event listener.
+ data.aborted = true;
+ return abortNative !.apply(data.target, data.args);
+ }
+
+ const openNative =
+ patchMethod(XMLHttpRequestPrototype, 'open', () => function(self: any, args: any[]) {
+ self[XHR_SYNC] = args[2] == false;
+ self[XHR_URL] = args[1];
+ return openNative !.apply(self, args);
+ });
+
+ const XMLHTTPREQUEST_SOURCE = 'XMLHttpRequest.send';
+ const fetchTaskAborting = zoneSymbol('fetchTaskAborting');
+ const fetchTaskScheduling = zoneSymbol('fetchTaskScheduling');
+ const sendNative: Function|null =
+ patchMethod(XMLHttpRequestPrototype, 'send', () => function(self: any, args: any[]) {
+ if ((Zone.current as any)[fetchTaskScheduling] === true) {
+ // a fetch is scheduling, so we are using xhr to polyfill fetch
+ // and because we already schedule macroTask for fetch, we should
+ // not schedule a macroTask for xhr again
+ return sendNative !.apply(self, args);
+ }
+ if (self[XHR_SYNC]) {
+ // if the XHR is sync there is no task to schedule, just execute the code.
+ return sendNative !.apply(self, args);
+ } else {
+ const options: XHROptions =
+ {target: self, url: self[XHR_URL], isPeriodic: false, args: args, aborted: false};
+ const task = scheduleMacroTaskWithCurrentZone(
+ XMLHTTPREQUEST_SOURCE, placeholderCallback, options, scheduleTask, clearTask);
+ if (self && self[XHR_ERROR_BEFORE_SCHEDULED] === true && !options.aborted &&
+ task.state === SCHEDULED) {
+ // xhr request throw error when send
+ // we should invoke task instead of leaving a scheduled
+ // pending macroTask
+ task.invoke();
+ }
+ }
+ });
+
+ const abortNative =
+ patchMethod(XMLHttpRequestPrototype, 'abort', () => function(self: any, args: any[]) {
+ const task: Task = findPendingTask(self);
+ if (task && typeof task.type == 'string') {
+ // If the XHR has already completed, do nothing.
+ // If the XHR has already been aborted, do nothing.
+ // Fix #569, call abort multiple times before done will cause
+ // macroTask task count be negative number
+ if (task.cancelFn == null || (task.data && (task.data).aborted)) {
+ return;
+ }
+ task.zone.cancelTask(task);
+ } else if ((Zone.current as any)[fetchTaskAborting] === true) {
+ // the abort is called from fetch polyfill, we need to call native abort of XHR.
+ return abortNative !.apply(self, args);
+ }
+ // Otherwise, we are trying to abort an XHR which has not yet been sent, so there is no
+ // task
+ // to cancel. Do nothing.
+ });
+ }
+});
+
+Zone.__load_patch('geolocation', (global: any) => {
+ /// GEO_LOCATION
+ if (global['navigator'] && global['navigator'].geolocation) {
+ patchPrototype(global['navigator'].geolocation, ['getCurrentPosition', 'watchPosition']);
+ }
+});
+
+Zone.__load_patch('PromiseRejectionEvent', (global: any, Zone: ZoneType) => {
+ // handle unhandled promise rejection
+ function findPromiseRejectionHandler(evtName: string) {
+ return function(e: any) {
+ const eventTasks = findEventTasks(global, evtName);
+ eventTasks.forEach(eventTask => {
+ // windows has added unhandledrejection event listener
+ // trigger the event listener
+ const PromiseRejectionEvent = global['PromiseRejectionEvent'];
+ if (PromiseRejectionEvent) {
+ const evt = new PromiseRejectionEvent(evtName, {promise: e.promise, reason: e.rejection});
+ eventTask.invoke(evt);
+ }
+ });
+ };
+ }
+
+ if (global['PromiseRejectionEvent']) {
+ (Zone as any)[zoneSymbol('unhandledPromiseRejectionHandler')] =
+ findPromiseRejectionHandler('unhandledrejection');
+
+ (Zone as any)[zoneSymbol('rejectionHandledHandler')] =
+ findPromiseRejectionHandler('rejectionhandled');
+ }
+});
diff --git a/packages/zone.js/lib/browser/canvas.ts b/packages/zone.js/lib/browser/canvas.ts
new file mode 100644
index 0000000000..527e09817c
--- /dev/null
+++ b/packages/zone.js/lib/browser/canvas.ts
@@ -0,0 +1,16 @@
+/**
+ * @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
+ */
+Zone.__load_patch('canvas', (global: any, Zone: ZoneType, api: _ZonePrivate) => {
+ const HTMLCanvasElement = global['HTMLCanvasElement'];
+ if (typeof HTMLCanvasElement !== 'undefined' && HTMLCanvasElement.prototype &&
+ HTMLCanvasElement.prototype.toBlob) {
+ api.patchMacroTask(HTMLCanvasElement.prototype, 'toBlob', (self: any, args: any[]) => {
+ return {name: 'HTMLCanvasElement.toBlob', target: self, cbIdx: 0, args: args};
+ });
+ }
+});
diff --git a/packages/zone.js/lib/browser/custom-elements.ts b/packages/zone.js/lib/browser/custom-elements.ts
new file mode 100644
index 0000000000..703090f5ec
--- /dev/null
+++ b/packages/zone.js/lib/browser/custom-elements.ts
@@ -0,0 +1,19 @@
+/**
+ * @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
+ */
+
+export function patchCustomElements(_global: any, api: _ZonePrivate) {
+ const {isBrowser, isMix} = api.getGlobalObjects() !;
+ if ((!isBrowser && !isMix) || !_global['customElements'] || !('customElements' in _global)) {
+ return;
+ }
+
+ const callbacks =
+ ['connectedCallback', 'disconnectedCallback', 'adoptedCallback', 'attributeChangedCallback'];
+
+ api.patchCallbacks(api, _global.customElements, 'customElements', 'define', callbacks);
+}
diff --git a/packages/zone.js/lib/browser/define-property.ts b/packages/zone.js/lib/browser/define-property.ts
new file mode 100644
index 0000000000..7c00a68e12
--- /dev/null
+++ b/packages/zone.js/lib/browser/define-property.ts
@@ -0,0 +1,111 @@
+/**
+ * @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
+ */
+
+/*
+ * This is necessary for Chrome and Chrome mobile, to enable
+ * things like redefining `createdCallback` on an element.
+ */
+
+const zoneSymbol = Zone.__symbol__;
+const _defineProperty = (Object as any)[zoneSymbol('defineProperty')] = Object.defineProperty;
+const _getOwnPropertyDescriptor = (Object as any)[zoneSymbol('getOwnPropertyDescriptor')] =
+ Object.getOwnPropertyDescriptor;
+const _create = Object.create;
+const unconfigurablesKey = zoneSymbol('unconfigurables');
+
+export function propertyPatch() {
+ Object.defineProperty = function(obj: any, prop: string, desc: any) {
+ if (isUnconfigurable(obj, prop)) {
+ throw new TypeError('Cannot assign to read only property \'' + prop + '\' of ' + obj);
+ }
+ const originalConfigurableFlag = desc.configurable;
+ if (prop !== 'prototype') {
+ desc = rewriteDescriptor(obj, prop, desc);
+ }
+ return _tryDefineProperty(obj, prop, desc, originalConfigurableFlag);
+ };
+
+ Object.defineProperties = function(obj, props) {
+ Object.keys(props).forEach(function(prop) { Object.defineProperty(obj, prop, props[prop]); });
+ return obj;
+ };
+
+ Object.create = function(obj: any, proto: any) {
+ if (typeof proto === 'object' && !Object.isFrozen(proto)) {
+ Object.keys(proto).forEach(function(prop) {
+ proto[prop] = rewriteDescriptor(obj, prop, proto[prop]);
+ });
+ }
+ return _create(obj, proto);
+ };
+
+ Object.getOwnPropertyDescriptor = function(obj, prop) {
+ const desc = _getOwnPropertyDescriptor(obj, prop);
+ if (desc && isUnconfigurable(obj, prop)) {
+ desc.configurable = false;
+ }
+ return desc;
+ };
+}
+
+export function _redefineProperty(obj: any, prop: string, desc: any) {
+ const originalConfigurableFlag = desc.configurable;
+ desc = rewriteDescriptor(obj, prop, desc);
+ return _tryDefineProperty(obj, prop, desc, originalConfigurableFlag);
+}
+
+function isUnconfigurable(obj: any, prop: any) {
+ return obj && obj[unconfigurablesKey] && obj[unconfigurablesKey][prop];
+}
+
+function rewriteDescriptor(obj: any, prop: string, desc: any) {
+ // issue-927, if the desc is frozen, don't try to change the desc
+ if (!Object.isFrozen(desc)) {
+ desc.configurable = true;
+ }
+ if (!desc.configurable) {
+ // issue-927, if the obj is frozen, don't try to set the desc to obj
+ if (!obj[unconfigurablesKey] && !Object.isFrozen(obj)) {
+ _defineProperty(obj, unconfigurablesKey, {writable: true, value: {}});
+ }
+ if (obj[unconfigurablesKey]) {
+ obj[unconfigurablesKey][prop] = true;
+ }
+ }
+ return desc;
+}
+
+function _tryDefineProperty(obj: any, prop: string, desc: any, originalConfigurableFlag: any) {
+ try {
+ return _defineProperty(obj, prop, desc);
+ } catch (error) {
+ if (desc.configurable) {
+ // In case of errors, when the configurable flag was likely set by rewriteDescriptor(), let's
+ // retry with the original flag value
+ if (typeof originalConfigurableFlag == 'undefined') {
+ delete desc.configurable;
+ } else {
+ desc.configurable = originalConfigurableFlag;
+ }
+ try {
+ return _defineProperty(obj, prop, desc);
+ } catch (error) {
+ let descJson: string|null = null;
+ try {
+ descJson = JSON.stringify(desc);
+ } catch (error) {
+ descJson = desc.toString();
+ }
+ console.log(`Attempting to configure '${prop}' with descriptor '${descJson}' on object '${
+ obj}' and got error, giving up: ${error}`);
+ }
+ } else {
+ throw error;
+ }
+ }
+}
diff --git a/packages/zone.js/lib/browser/event-target-legacy.ts b/packages/zone.js/lib/browser/event-target-legacy.ts
new file mode 100644
index 0000000000..96540c72c8
--- /dev/null
+++ b/packages/zone.js/lib/browser/event-target-legacy.ts
@@ -0,0 +1,110 @@
+/**
+ * @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
+ */
+
+export function eventTargetLegacyPatch(_global: any, api: _ZonePrivate) {
+ const {eventNames, globalSources, zoneSymbolEventNames, TRUE_STR, FALSE_STR, ZONE_SYMBOL_PREFIX} =
+ api.getGlobalObjects() !;
+ const WTF_ISSUE_555 =
+ 'Anchor,Area,Audio,BR,Base,BaseFont,Body,Button,Canvas,Content,DList,Directory,Div,Embed,FieldSet,Font,Form,Frame,FrameSet,HR,Head,Heading,Html,IFrame,Image,Input,Keygen,LI,Label,Legend,Link,Map,Marquee,Media,Menu,Meta,Meter,Mod,OList,Object,OptGroup,Option,Output,Paragraph,Pre,Progress,Quote,Script,Select,Source,Span,Style,TableCaption,TableCell,TableCol,Table,TableRow,TableSection,TextArea,Title,Track,UList,Unknown,Video';
+ const NO_EVENT_TARGET =
+ 'ApplicationCache,EventSource,FileReader,InputMethodContext,MediaController,MessagePort,Node,Performance,SVGElementInstance,SharedWorker,TextTrack,TextTrackCue,TextTrackList,WebKitNamedFlow,Window,Worker,WorkerGlobalScope,XMLHttpRequest,XMLHttpRequestEventTarget,XMLHttpRequestUpload,IDBRequest,IDBOpenDBRequest,IDBDatabase,IDBTransaction,IDBCursor,DBIndex,WebSocket'
+ .split(',');
+ const EVENT_TARGET = 'EventTarget';
+
+ let apis: any[] = [];
+ const isWtf = _global['wtf'];
+ const WTF_ISSUE_555_ARRAY = WTF_ISSUE_555.split(',');
+
+ if (isWtf) {
+ // Workaround for: https://github.com/google/tracing-framework/issues/555
+ apis = WTF_ISSUE_555_ARRAY.map((v) => 'HTML' + v + 'Element').concat(NO_EVENT_TARGET);
+ } else if (_global[EVENT_TARGET]) {
+ apis.push(EVENT_TARGET);
+ } else {
+ // Note: EventTarget is not available in all browsers,
+ // if it's not available, we instead patch the APIs in the IDL that inherit from EventTarget
+ apis = NO_EVENT_TARGET;
+ }
+
+ const isDisableIECheck = _global['__Zone_disable_IE_check'] || false;
+ const isEnableCrossContextCheck = _global['__Zone_enable_cross_context_check'] || false;
+ const ieOrEdge = api.isIEOrEdge();
+
+ const ADD_EVENT_LISTENER_SOURCE = '.addEventListener:';
+ const FUNCTION_WRAPPER = '[object FunctionWrapper]';
+ const BROWSER_TOOLS = 'function __BROWSERTOOLS_CONSOLE_SAFEFUNC() { [native code] }';
+
+ // predefine all __zone_symbol__ + eventName + true/false string
+ for (let i = 0; i < eventNames.length; i++) {
+ const eventName = eventNames[i];
+ const falseEventName = eventName + FALSE_STR;
+ const trueEventName = eventName + TRUE_STR;
+ const symbol = ZONE_SYMBOL_PREFIX + falseEventName;
+ const symbolCapture = ZONE_SYMBOL_PREFIX + trueEventName;
+ zoneSymbolEventNames[eventName] = {};
+ zoneSymbolEventNames[eventName][FALSE_STR] = symbol;
+ zoneSymbolEventNames[eventName][TRUE_STR] = symbolCapture;
+ }
+
+ // predefine all task.source string
+ for (let i = 0; i < WTF_ISSUE_555.length; i++) {
+ const target: any = WTF_ISSUE_555_ARRAY[i];
+ const targets: any = globalSources[target] = {};
+ for (let j = 0; j < eventNames.length; j++) {
+ const eventName = eventNames[j];
+ targets[eventName] = target + ADD_EVENT_LISTENER_SOURCE + eventName;
+ }
+ }
+
+ const checkIEAndCrossContext = function(
+ nativeDelegate: any, delegate: any, target: any, args: any) {
+ if (!isDisableIECheck && ieOrEdge) {
+ if (isEnableCrossContextCheck) {
+ try {
+ const testString = delegate.toString();
+ if ((testString === FUNCTION_WRAPPER || testString == BROWSER_TOOLS)) {
+ nativeDelegate.apply(target, args);
+ return false;
+ }
+ } catch (error) {
+ nativeDelegate.apply(target, args);
+ return false;
+ }
+ } else {
+ const testString = delegate.toString();
+ if ((testString === FUNCTION_WRAPPER || testString == BROWSER_TOOLS)) {
+ nativeDelegate.apply(target, args);
+ return false;
+ }
+ }
+ } else if (isEnableCrossContextCheck) {
+ try {
+ delegate.toString();
+ } catch (error) {
+ nativeDelegate.apply(target, args);
+ return false;
+ }
+ }
+ return true;
+ };
+
+ const apiTypes: any[] = [];
+ for (let i = 0; i < apis.length; i++) {
+ const type = _global[apis[i]];
+ apiTypes.push(type && type.prototype);
+ }
+ // vh is validateHandler to check event handler
+ // is valid or not(for security check)
+ api.patchEventTarget(_global, apiTypes, {vh: checkIEAndCrossContext});
+ (Zone as any)[api.symbol('patchEventTarget')] = !!_global[EVENT_TARGET];
+ return true;
+}
+
+export function patchEvent(global: any, api: _ZonePrivate) {
+ api.patchEventPrototype(global, api);
+}
diff --git a/packages/zone.js/lib/browser/event-target.ts b/packages/zone.js/lib/browser/event-target.ts
new file mode 100644
index 0000000000..2a41ba6290
--- /dev/null
+++ b/packages/zone.js/lib/browser/event-target.ts
@@ -0,0 +1,39 @@
+/**
+ * @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
+ */
+
+export function eventTargetPatch(_global: any, api: _ZonePrivate) {
+ if ((Zone as any)[api.symbol('patchEventTarget')]) {
+ // EventTarget is already patched.
+ return;
+ }
+ const {eventNames, zoneSymbolEventNames, TRUE_STR, FALSE_STR, ZONE_SYMBOL_PREFIX} =
+ api.getGlobalObjects() !;
+ // predefine all __zone_symbol__ + eventName + true/false string
+ for (let i = 0; i < eventNames.length; i++) {
+ const eventName = eventNames[i];
+ const falseEventName = eventName + FALSE_STR;
+ const trueEventName = eventName + TRUE_STR;
+ const symbol = ZONE_SYMBOL_PREFIX + falseEventName;
+ const symbolCapture = ZONE_SYMBOL_PREFIX + trueEventName;
+ zoneSymbolEventNames[eventName] = {};
+ zoneSymbolEventNames[eventName][FALSE_STR] = symbol;
+ zoneSymbolEventNames[eventName][TRUE_STR] = symbolCapture;
+ }
+
+ const EVENT_TARGET = _global['EventTarget'];
+ if (!EVENT_TARGET || !EVENT_TARGET.prototype) {
+ return;
+ }
+ api.patchEventTarget(_global, [EVENT_TARGET && EVENT_TARGET.prototype]);
+
+ return true;
+}
+
+export function patchEvent(global: any, api: _ZonePrivate) {
+ api.patchEventPrototype(global, api);
+}
diff --git a/packages/zone.js/lib/browser/property-descriptor-legacy.ts b/packages/zone.js/lib/browser/property-descriptor-legacy.ts
new file mode 100644
index 0000000000..737e701e62
--- /dev/null
+++ b/packages/zone.js/lib/browser/property-descriptor-legacy.ts
@@ -0,0 +1,124 @@
+/**
+ * @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
+ */
+/**
+ * @fileoverview
+ * @suppress {globalThis}
+ */
+
+import * as webSocketPatch from './websocket';
+
+export function propertyDescriptorLegacyPatch(api: _ZonePrivate, _global: any) {
+ const {isNode, isMix} = api.getGlobalObjects() !;
+ if (isNode && !isMix) {
+ return;
+ }
+
+ if (!canPatchViaPropertyDescriptor(api, _global)) {
+ const supportsWebSocket = typeof WebSocket !== 'undefined';
+ // Safari, Android browsers (Jelly Bean)
+ patchViaCapturingAllTheEvents(api);
+ api.patchClass('XMLHttpRequest');
+ if (supportsWebSocket) {
+ webSocketPatch.apply(api, _global);
+ }
+ (Zone as any)[api.symbol('patchEvents')] = true;
+ }
+}
+
+function canPatchViaPropertyDescriptor(api: _ZonePrivate, _global: any) {
+ const {isBrowser, isMix} = api.getGlobalObjects() !;
+ if ((isBrowser || isMix) &&
+ !api.ObjectGetOwnPropertyDescriptor(HTMLElement.prototype, 'onclick') &&
+ typeof Element !== 'undefined') {
+ // WebKit https://bugs.webkit.org/show_bug.cgi?id=134364
+ // IDL interface attributes are not configurable
+ const desc = api.ObjectGetOwnPropertyDescriptor(Element.prototype, 'onclick');
+ if (desc && !desc.configurable) return false;
+ // try to use onclick to detect whether we can patch via propertyDescriptor
+ // because XMLHttpRequest is not available in service worker
+ if (desc) {
+ api.ObjectDefineProperty(
+ Element.prototype, 'onclick',
+ {enumerable: true, configurable: true, get: function() { return true; }});
+ const div = document.createElement('div');
+ const result = !!div.onclick;
+ api.ObjectDefineProperty(Element.prototype, 'onclick', desc);
+ return result;
+ }
+ }
+
+ const XMLHttpRequest = _global['XMLHttpRequest'];
+ if (!XMLHttpRequest) {
+ // XMLHttpRequest is not available in service worker
+ return false;
+ }
+ const ON_READY_STATE_CHANGE = 'onreadystatechange';
+ const XMLHttpRequestPrototype = XMLHttpRequest.prototype;
+
+ const xhrDesc =
+ api.ObjectGetOwnPropertyDescriptor(XMLHttpRequestPrototype, ON_READY_STATE_CHANGE);
+
+ // add enumerable and configurable here because in opera
+ // by default XMLHttpRequest.prototype.onreadystatechange is undefined
+ // without adding enumerable and configurable will cause onreadystatechange
+ // non-configurable
+ // and if XMLHttpRequest.prototype.onreadystatechange is undefined,
+ // we should set a real desc instead a fake one
+ if (xhrDesc) {
+ api.ObjectDefineProperty(
+ XMLHttpRequestPrototype, ON_READY_STATE_CHANGE,
+ {enumerable: true, configurable: true, get: function() { return true; }});
+ const req = new XMLHttpRequest();
+ const result = !!req.onreadystatechange;
+ // restore original desc
+ api.ObjectDefineProperty(XMLHttpRequestPrototype, ON_READY_STATE_CHANGE, xhrDesc || {});
+ return result;
+ } else {
+ const SYMBOL_FAKE_ONREADYSTATECHANGE = api.symbol('fake');
+ api.ObjectDefineProperty(XMLHttpRequestPrototype, ON_READY_STATE_CHANGE, {
+ enumerable: true,
+ configurable: true,
+ get: function() { return this[SYMBOL_FAKE_ONREADYSTATECHANGE]; },
+ set: function(value) { this[SYMBOL_FAKE_ONREADYSTATECHANGE] = value; }
+ });
+ const req = new XMLHttpRequest();
+ const detectFunc = () => {};
+ req.onreadystatechange = detectFunc;
+ const result = (req as any)[SYMBOL_FAKE_ONREADYSTATECHANGE] === detectFunc;
+ req.onreadystatechange = null as any;
+ return result;
+ }
+}
+
+// Whenever any eventListener fires, we check the eventListener target and all parents
+// for `onwhatever` properties and replace them with zone-bound functions
+// - Chrome (for now)
+function patchViaCapturingAllTheEvents(api: _ZonePrivate) {
+ const {eventNames} = api.getGlobalObjects() !;
+ const unboundKey = api.symbol('unbound');
+ for (let i = 0; i < eventNames.length; i++) {
+ const property = eventNames[i];
+ const onproperty = 'on' + property;
+ self.addEventListener(property, function(event) {
+ let elt: any = event.target, bound, source;
+ if (elt) {
+ source = elt.constructor['name'] + '.' + onproperty;
+ } else {
+ source = 'unknown.' + onproperty;
+ }
+ while (elt) {
+ if (elt[onproperty] && !elt[onproperty][unboundKey]) {
+ bound = api.wrapWithCurrentZone(elt[onproperty], source);
+ bound[unboundKey] = elt[onproperty];
+ elt[onproperty] = bound;
+ }
+ elt = elt.parentElement;
+ }
+ }, true);
+ }
+}
diff --git a/packages/zone.js/lib/browser/property-descriptor.ts b/packages/zone.js/lib/browser/property-descriptor.ts
new file mode 100644
index 0000000000..dfaa208b8c
--- /dev/null
+++ b/packages/zone.js/lib/browser/property-descriptor.ts
@@ -0,0 +1,334 @@
+/**
+ * @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
+ */
+/**
+ * @fileoverview
+ * @suppress {globalThis}
+ */
+
+import {ObjectGetPrototypeOf, isBrowser, isIE, isMix, isNode, patchOnProperties} from '../common/utils';
+
+const globalEventHandlersEventNames = [
+ 'abort',
+ 'animationcancel',
+ 'animationend',
+ 'animationiteration',
+ 'auxclick',
+ 'beforeinput',
+ 'blur',
+ 'cancel',
+ 'canplay',
+ 'canplaythrough',
+ 'change',
+ 'compositionstart',
+ 'compositionupdate',
+ 'compositionend',
+ 'cuechange',
+ 'click',
+ 'close',
+ 'contextmenu',
+ 'curechange',
+ 'dblclick',
+ 'drag',
+ 'dragend',
+ 'dragenter',
+ 'dragexit',
+ 'dragleave',
+ 'dragover',
+ 'drop',
+ 'durationchange',
+ 'emptied',
+ 'ended',
+ 'error',
+ 'focus',
+ 'focusin',
+ 'focusout',
+ 'gotpointercapture',
+ 'input',
+ 'invalid',
+ 'keydown',
+ 'keypress',
+ 'keyup',
+ 'load',
+ 'loadstart',
+ 'loadeddata',
+ 'loadedmetadata',
+ 'lostpointercapture',
+ 'mousedown',
+ 'mouseenter',
+ 'mouseleave',
+ 'mousemove',
+ 'mouseout',
+ 'mouseover',
+ 'mouseup',
+ 'mousewheel',
+ 'orientationchange',
+ 'pause',
+ 'play',
+ 'playing',
+ 'pointercancel',
+ 'pointerdown',
+ 'pointerenter',
+ 'pointerleave',
+ 'pointerlockchange',
+ 'mozpointerlockchange',
+ 'webkitpointerlockerchange',
+ 'pointerlockerror',
+ 'mozpointerlockerror',
+ 'webkitpointerlockerror',
+ 'pointermove',
+ 'pointout',
+ 'pointerover',
+ 'pointerup',
+ 'progress',
+ 'ratechange',
+ 'reset',
+ 'resize',
+ 'scroll',
+ 'seeked',
+ 'seeking',
+ 'select',
+ 'selectionchange',
+ 'selectstart',
+ 'show',
+ 'sort',
+ 'stalled',
+ 'submit',
+ 'suspend',
+ 'timeupdate',
+ 'volumechange',
+ 'touchcancel',
+ 'touchmove',
+ 'touchstart',
+ 'touchend',
+ 'transitioncancel',
+ 'transitionend',
+ 'waiting',
+ 'wheel'
+];
+const documentEventNames = [
+ 'afterscriptexecute', 'beforescriptexecute', 'DOMContentLoaded', 'freeze', 'fullscreenchange',
+ 'mozfullscreenchange', 'webkitfullscreenchange', 'msfullscreenchange', 'fullscreenerror',
+ 'mozfullscreenerror', 'webkitfullscreenerror', 'msfullscreenerror', 'readystatechange',
+ 'visibilitychange', 'resume'
+];
+const windowEventNames = [
+ 'absolutedeviceorientation',
+ 'afterinput',
+ 'afterprint',
+ 'appinstalled',
+ 'beforeinstallprompt',
+ 'beforeprint',
+ 'beforeunload',
+ 'devicelight',
+ 'devicemotion',
+ 'deviceorientation',
+ 'deviceorientationabsolute',
+ 'deviceproximity',
+ 'hashchange',
+ 'languagechange',
+ 'message',
+ 'mozbeforepaint',
+ 'offline',
+ 'online',
+ 'paint',
+ 'pageshow',
+ 'pagehide',
+ 'popstate',
+ 'rejectionhandled',
+ 'storage',
+ 'unhandledrejection',
+ 'unload',
+ 'userproximity',
+ 'vrdisplyconnected',
+ 'vrdisplaydisconnected',
+ 'vrdisplaypresentchange'
+];
+const htmlElementEventNames = [
+ 'beforecopy', 'beforecut', 'beforepaste', 'copy', 'cut', 'paste', 'dragstart', 'loadend',
+ 'animationstart', 'search', 'transitionrun', 'transitionstart', 'webkitanimationend',
+ 'webkitanimationiteration', 'webkitanimationstart', 'webkittransitionend'
+];
+const mediaElementEventNames =
+ ['encrypted', 'waitingforkey', 'msneedkey', 'mozinterruptbegin', 'mozinterruptend'];
+const ieElementEventNames = [
+ 'activate',
+ 'afterupdate',
+ 'ariarequest',
+ 'beforeactivate',
+ 'beforedeactivate',
+ 'beforeeditfocus',
+ 'beforeupdate',
+ 'cellchange',
+ 'controlselect',
+ 'dataavailable',
+ 'datasetchanged',
+ 'datasetcomplete',
+ 'errorupdate',
+ 'filterchange',
+ 'layoutcomplete',
+ 'losecapture',
+ 'move',
+ 'moveend',
+ 'movestart',
+ 'propertychange',
+ 'resizeend',
+ 'resizestart',
+ 'rowenter',
+ 'rowexit',
+ 'rowsdelete',
+ 'rowsinserted',
+ 'command',
+ 'compassneedscalibration',
+ 'deactivate',
+ 'help',
+ 'mscontentzoom',
+ 'msmanipulationstatechanged',
+ 'msgesturechange',
+ 'msgesturedoubletap',
+ 'msgestureend',
+ 'msgesturehold',
+ 'msgesturestart',
+ 'msgesturetap',
+ 'msgotpointercapture',
+ 'msinertiastart',
+ 'mslostpointercapture',
+ 'mspointercancel',
+ 'mspointerdown',
+ 'mspointerenter',
+ 'mspointerhover',
+ 'mspointerleave',
+ 'mspointermove',
+ 'mspointerout',
+ 'mspointerover',
+ 'mspointerup',
+ 'pointerout',
+ 'mssitemodejumplistitemremoved',
+ 'msthumbnailclick',
+ 'stop',
+ 'storagecommit'
+];
+const webglEventNames = ['webglcontextrestored', 'webglcontextlost', 'webglcontextcreationerror'];
+const formEventNames = ['autocomplete', 'autocompleteerror'];
+const detailEventNames = ['toggle'];
+const frameEventNames = ['load'];
+const frameSetEventNames = ['blur', 'error', 'focus', 'load', 'resize', 'scroll', 'messageerror'];
+const marqueeEventNames = ['bounce', 'finish', 'start'];
+
+const XMLHttpRequestEventNames = [
+ 'loadstart', 'progress', 'abort', 'error', 'load', 'progress', 'timeout', 'loadend',
+ 'readystatechange'
+];
+const IDBIndexEventNames =
+ ['upgradeneeded', 'complete', 'abort', 'success', 'error', 'blocked', 'versionchange', 'close'];
+const websocketEventNames = ['close', 'error', 'open', 'message'];
+const workerEventNames = ['error', 'message'];
+
+export const eventNames = globalEventHandlersEventNames.concat(
+ webglEventNames, formEventNames, detailEventNames, documentEventNames, windowEventNames,
+ htmlElementEventNames, ieElementEventNames);
+
+export interface IgnoreProperty {
+ target: any;
+ ignoreProperties: string[];
+}
+
+export function filterProperties(
+ target: any, onProperties: string[], ignoreProperties: IgnoreProperty[]): string[] {
+ if (!ignoreProperties || ignoreProperties.length === 0) {
+ return onProperties;
+ }
+
+ const tip: IgnoreProperty[] = ignoreProperties.filter(ip => ip.target === target);
+ if (!tip || tip.length === 0) {
+ return onProperties;
+ }
+
+ const targetIgnoreProperties: string[] = tip[0].ignoreProperties;
+ return onProperties.filter(op => targetIgnoreProperties.indexOf(op) === -1);
+}
+
+export function patchFilteredProperties(
+ target: any, onProperties: string[], ignoreProperties: IgnoreProperty[], prototype?: any) {
+ // check whether target is available, sometimes target will be undefined
+ // because different browser or some 3rd party plugin.
+ if (!target) {
+ return;
+ }
+ const filteredProperties: string[] = filterProperties(target, onProperties, ignoreProperties);
+ patchOnProperties(target, filteredProperties, prototype);
+}
+
+export function propertyDescriptorPatch(api: _ZonePrivate, _global: any) {
+ if (isNode && !isMix) {
+ return;
+ }
+ if ((Zone as any)[api.symbol('patchEvents')]) {
+ // events are already been patched by legacy patch.
+ return;
+ }
+ const supportsWebSocket = typeof WebSocket !== 'undefined';
+ const ignoreProperties: IgnoreProperty[] = _global['__Zone_ignore_on_properties'];
+ // for browsers that we can patch the descriptor: Chrome & Firefox
+ if (isBrowser) {
+ const internalWindow: any = window;
+ const ignoreErrorProperties =
+ isIE ? [{target: internalWindow, ignoreProperties: ['error']}] : [];
+ // in IE/Edge, onProp not exist in window object, but in WindowPrototype
+ // so we need to pass WindowPrototype to check onProp exist or not
+ patchFilteredProperties(
+ internalWindow, eventNames.concat(['messageerror']),
+ ignoreProperties ? ignoreProperties.concat(ignoreErrorProperties) : ignoreProperties,
+ ObjectGetPrototypeOf(internalWindow));
+ patchFilteredProperties(Document.prototype, eventNames, ignoreProperties);
+
+ if (typeof internalWindow['SVGElement'] !== 'undefined') {
+ patchFilteredProperties(internalWindow['SVGElement'].prototype, eventNames, ignoreProperties);
+ }
+ patchFilteredProperties(Element.prototype, eventNames, ignoreProperties);
+ patchFilteredProperties(HTMLElement.prototype, eventNames, ignoreProperties);
+ patchFilteredProperties(HTMLMediaElement.prototype, mediaElementEventNames, ignoreProperties);
+ patchFilteredProperties(
+ HTMLFrameSetElement.prototype, windowEventNames.concat(frameSetEventNames),
+ ignoreProperties);
+ patchFilteredProperties(
+ HTMLBodyElement.prototype, windowEventNames.concat(frameSetEventNames), ignoreProperties);
+ patchFilteredProperties(HTMLFrameElement.prototype, frameEventNames, ignoreProperties);
+ patchFilteredProperties(HTMLIFrameElement.prototype, frameEventNames, ignoreProperties);
+
+ const HTMLMarqueeElement = internalWindow['HTMLMarqueeElement'];
+ if (HTMLMarqueeElement) {
+ patchFilteredProperties(HTMLMarqueeElement.prototype, marqueeEventNames, ignoreProperties);
+ }
+ const Worker = internalWindow['Worker'];
+ if (Worker) {
+ patchFilteredProperties(Worker.prototype, workerEventNames, ignoreProperties);
+ }
+ }
+ const XMLHttpRequest = _global['XMLHttpRequest'];
+ if (XMLHttpRequest) {
+ // XMLHttpRequest is not available in ServiceWorker, so we need to check here
+ patchFilteredProperties(XMLHttpRequest.prototype, XMLHttpRequestEventNames, ignoreProperties);
+ }
+ const XMLHttpRequestEventTarget = _global['XMLHttpRequestEventTarget'];
+ if (XMLHttpRequestEventTarget) {
+ patchFilteredProperties(
+ XMLHttpRequestEventTarget && XMLHttpRequestEventTarget.prototype, XMLHttpRequestEventNames,
+ ignoreProperties);
+ }
+ if (typeof IDBIndex !== 'undefined') {
+ patchFilteredProperties(IDBIndex.prototype, IDBIndexEventNames, ignoreProperties);
+ patchFilteredProperties(IDBRequest.prototype, IDBIndexEventNames, ignoreProperties);
+ patchFilteredProperties(IDBOpenDBRequest.prototype, IDBIndexEventNames, ignoreProperties);
+ patchFilteredProperties(IDBDatabase.prototype, IDBIndexEventNames, ignoreProperties);
+ patchFilteredProperties(IDBTransaction.prototype, IDBIndexEventNames, ignoreProperties);
+ patchFilteredProperties(IDBCursor.prototype, IDBIndexEventNames, ignoreProperties);
+ }
+ if (supportsWebSocket) {
+ patchFilteredProperties(WebSocket.prototype, websocketEventNames, ignoreProperties);
+ }
+}
diff --git a/packages/zone.js/lib/browser/register-element.ts b/packages/zone.js/lib/browser/register-element.ts
new file mode 100644
index 0000000000..e760d36c0d
--- /dev/null
+++ b/packages/zone.js/lib/browser/register-element.ts
@@ -0,0 +1,19 @@
+/**
+ * @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
+ */
+
+export function registerElementPatch(_global: any, api: _ZonePrivate) {
+ const {isBrowser, isMix} = api.getGlobalObjects() !;
+ if ((!isBrowser && !isMix) || !('registerElement' in (_global).document)) {
+ return;
+ }
+
+ const callbacks =
+ ['createdCallback', 'attachedCallback', 'detachedCallback', 'attributeChangedCallback'];
+
+ api.patchCallbacks(api, document, 'Document', 'registerElement', callbacks);
+}
diff --git a/packages/zone.js/lib/browser/rollup-common.ts b/packages/zone.js/lib/browser/rollup-common.ts
new file mode 100644
index 0000000000..006ede27ef
--- /dev/null
+++ b/packages/zone.js/lib/browser/rollup-common.ts
@@ -0,0 +1,12 @@
+/**
+ * @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
+ */
+
+import '../zone';
+import '../common/promise';
+import '../common/to-string';
+import './api-util';
diff --git a/packages/zone.js/lib/browser/rollup-legacy-main.ts b/packages/zone.js/lib/browser/rollup-legacy-main.ts
new file mode 100644
index 0000000000..68034c61c6
--- /dev/null
+++ b/packages/zone.js/lib/browser/rollup-legacy-main.ts
@@ -0,0 +1,11 @@
+/**
+ * @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
+ */
+
+import './rollup-common';
+import './browser-legacy';
+import './browser';
diff --git a/packages/zone.js/lib/browser/rollup-legacy-test-main.ts b/packages/zone.js/lib/browser/rollup-legacy-test-main.ts
new file mode 100644
index 0000000000..4c2a374aef
--- /dev/null
+++ b/packages/zone.js/lib/browser/rollup-legacy-test-main.ts
@@ -0,0 +1,12 @@
+/**
+ * @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
+ */
+
+import './rollup-legacy-main';
+
+// load test related files into bundle
+import '../testing/zone-testing';
diff --git a/packages/zone.js/lib/browser/rollup-main.ts b/packages/zone.js/lib/browser/rollup-main.ts
new file mode 100644
index 0000000000..ee94dde97a
--- /dev/null
+++ b/packages/zone.js/lib/browser/rollup-main.ts
@@ -0,0 +1,10 @@
+/**
+ * @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
+ */
+
+import './rollup-common';
+import './browser';
diff --git a/packages/zone.js/lib/browser/rollup-test-main.ts b/packages/zone.js/lib/browser/rollup-test-main.ts
new file mode 100644
index 0000000000..91a951b244
--- /dev/null
+++ b/packages/zone.js/lib/browser/rollup-test-main.ts
@@ -0,0 +1,12 @@
+/**
+ * @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
+ */
+
+import './rollup-main';
+
+// load test related files into bundle
+import '../testing/zone-testing';
diff --git a/packages/zone.js/lib/browser/shadydom.ts b/packages/zone.js/lib/browser/shadydom.ts
new file mode 100644
index 0000000000..f308308cd8
--- /dev/null
+++ b/packages/zone.js/lib/browser/shadydom.ts
@@ -0,0 +1,24 @@
+/**
+ * @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
+ */
+Zone.__load_patch('shadydom', (global: any, Zone: ZoneType, api: _ZonePrivate) => {
+ // https://github.com/angular/zone.js/issues/782
+ // in web components, shadydom will patch addEventListener/removeEventListener of
+ // Node.prototype and WindowPrototype, this will have conflict with zone.js
+ // so zone.js need to patch them again.
+ const windowPrototype = Object.getPrototypeOf(window);
+ if (windowPrototype && windowPrototype.hasOwnProperty('addEventListener')) {
+ (windowPrototype as any)[Zone.__symbol__('addEventListener')] = null;
+ (windowPrototype as any)[Zone.__symbol__('removeEventListener')] = null;
+ api.patchEventTarget(global, [windowPrototype]);
+ }
+ if (Node.prototype.hasOwnProperty('addEventListener')) {
+ (Node.prototype as any)[Zone.__symbol__('addEventListener')] = null;
+ (Node.prototype as any)[Zone.__symbol__('removeEventListener')] = null;
+ api.patchEventTarget(global, [Node.prototype]);
+ }
+});
diff --git a/packages/zone.js/lib/browser/webapis-media-query.ts b/packages/zone.js/lib/browser/webapis-media-query.ts
new file mode 100644
index 0000000000..7ca46e1719
--- /dev/null
+++ b/packages/zone.js/lib/browser/webapis-media-query.ts
@@ -0,0 +1,65 @@
+/**
+ * @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
+ */
+Zone.__load_patch('mediaQuery', (global: any, Zone: ZoneType, api: _ZonePrivate) => {
+ function patchAddListener(proto: any) {
+ api.patchMethod(proto, 'addListener', (delegate: Function) => (self: any, args: any[]) => {
+ const callback = args.length > 0 ? args[0] : null;
+ if (typeof callback === 'function') {
+ const wrapperedCallback = Zone.current.wrap(callback, 'MediaQuery');
+ callback[api.symbol('mediaQueryCallback')] = wrapperedCallback;
+ return delegate.call(self, wrapperedCallback);
+ } else {
+ return delegate.apply(self, args);
+ }
+ });
+ }
+
+ function patchRemoveListener(proto: any) {
+ api.patchMethod(proto, 'removeListener', (delegate: Function) => (self: any, args: any[]) => {
+ const callback = args.length > 0 ? args[0] : null;
+ if (typeof callback === 'function') {
+ const wrapperedCallback = callback[api.symbol('mediaQueryCallback')];
+ if (wrapperedCallback) {
+ return delegate.call(self, wrapperedCallback);
+ } else {
+ return delegate.apply(self, args);
+ }
+ } else {
+ return delegate.apply(self, args);
+ }
+ });
+ }
+
+ if (global['MediaQueryList']) {
+ const proto = global['MediaQueryList'].prototype;
+ patchAddListener(proto);
+ patchRemoveListener(proto);
+ } else if (global['matchMedia']) {
+ api.patchMethod(global, 'matchMedia', (delegate: Function) => (self: any, args: any[]) => {
+ const mql = delegate.apply(self, args);
+ if (mql) {
+ // try to patch MediaQueryList.prototype
+ const proto = Object.getPrototypeOf(mql);
+ if (proto && proto['addListener']) {
+ // try to patch proto, don't need to worry about patch
+ // multiple times, because, api.patchEventTarget will check it
+ patchAddListener(proto);
+ patchRemoveListener(proto);
+ patchAddListener(mql);
+ patchRemoveListener(mql);
+ } else if (mql['addListener']) {
+ // proto not exists, or proto has no addListener method
+ // try to patch mql instance
+ patchAddListener(mql);
+ patchRemoveListener(mql);
+ }
+ }
+ return mql;
+ });
+ }
+});
diff --git a/packages/zone.js/lib/browser/webapis-notification.ts b/packages/zone.js/lib/browser/webapis-notification.ts
new file mode 100644
index 0000000000..2d663e73cb
--- /dev/null
+++ b/packages/zone.js/lib/browser/webapis-notification.ts
@@ -0,0 +1,18 @@
+/**
+ * @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
+ */
+Zone.__load_patch('notification', (global: any, Zone: ZoneType, api: _ZonePrivate) => {
+ const Notification = global['Notification'];
+ if (!Notification || !Notification.prototype) {
+ return;
+ }
+ const desc = Object.getOwnPropertyDescriptor(Notification.prototype, 'onerror');
+ if (!desc || !desc.configurable) {
+ return;
+ }
+ api.patchOnProperties(Notification.prototype, null);
+});
diff --git a/packages/zone.js/lib/browser/webapis-resize-observer.ts b/packages/zone.js/lib/browser/webapis-resize-observer.ts
new file mode 100644
index 0000000000..d2965a6ad2
--- /dev/null
+++ b/packages/zone.js/lib/browser/webapis-resize-observer.ts
@@ -0,0 +1,91 @@
+/**
+ * @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
+ */
+Zone.__load_patch('ResizeObserver', (global: any, Zone: any, api: _ZonePrivate) => {
+ const ResizeObserver = global['ResizeObserver'];
+ if (!ResizeObserver) {
+ return;
+ }
+
+ const resizeObserverSymbol = api.symbol('ResizeObserver');
+
+ api.patchMethod(global, 'ResizeObserver', (delegate: Function) => (self: any, args: any[]) => {
+ const callback = args.length > 0 ? args[0] : null;
+ if (callback) {
+ args[0] = function(entries: any, observer: any) {
+ const zones: {[zoneName: string]: any} = {};
+ const currZone = Zone.current;
+ for (let entry of entries) {
+ let zone = entry.target[resizeObserverSymbol];
+ if (!zone) {
+ zone = currZone;
+ }
+ let zoneEntriesInfo = zones[zone.name];
+ if (!zoneEntriesInfo) {
+ zones[zone.name] = zoneEntriesInfo = {entries: [], zone: zone};
+ }
+ zoneEntriesInfo.entries.push(entry);
+ }
+
+ Object.keys(zones).forEach(zoneName => {
+ const zoneEntriesInfo = zones[zoneName];
+ if (zoneEntriesInfo.zone !== Zone.current) {
+ zoneEntriesInfo.zone.run(
+ callback, this, [zoneEntriesInfo.entries, observer], 'ResizeObserver');
+ } else {
+ callback.call(this, zoneEntriesInfo.entries, observer);
+ }
+ });
+ };
+ }
+ return args.length > 0 ? new ResizeObserver(args[0]) : new ResizeObserver();
+ });
+
+ api.patchMethod(
+ ResizeObserver.prototype, 'observe', (delegate: Function) => (self: any, args: any[]) => {
+ const target = args.length > 0 ? args[0] : null;
+ if (!target) {
+ return delegate.apply(self, args);
+ }
+ let targets = self[resizeObserverSymbol];
+ if (!targets) {
+ targets = self[resizeObserverSymbol] = [];
+ }
+ targets.push(target);
+ target[resizeObserverSymbol] = Zone.current;
+ return delegate.apply(self, args);
+ });
+
+ api.patchMethod(
+ ResizeObserver.prototype, 'unobserve', (delegate: Function) => (self: any, args: any[]) => {
+ const target = args.length > 0 ? args[0] : null;
+ if (!target) {
+ return delegate.apply(self, args);
+ }
+ let targets = self[resizeObserverSymbol];
+ if (targets) {
+ for (let i = 0; i < targets.length; i++) {
+ if (targets[i] === target) {
+ targets.splice(i, 1);
+ break;
+ }
+ }
+ }
+ target[resizeObserverSymbol] = undefined;
+ return delegate.apply(self, args);
+ });
+
+ api.patchMethod(
+ ResizeObserver.prototype, 'disconnect', (delegate: Function) => (self: any, args: any[]) => {
+ const targets = self[resizeObserverSymbol];
+ if (targets) {
+ targets.forEach((target: any) => { target[resizeObserverSymbol] = undefined; });
+ self[resizeObserverSymbol] = undefined;
+ }
+ return delegate.apply(self, args);
+ });
+});
diff --git a/packages/zone.js/lib/browser/webapis-rtc-peer-connection.ts b/packages/zone.js/lib/browser/webapis-rtc-peer-connection.ts
new file mode 100644
index 0000000000..5930445efd
--- /dev/null
+++ b/packages/zone.js/lib/browser/webapis-rtc-peer-connection.ts
@@ -0,0 +1,26 @@
+/**
+ * @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
+ */
+Zone.__load_patch('RTCPeerConnection', (global: any, Zone: ZoneType, api: _ZonePrivate) => {
+ const RTCPeerConnection = global['RTCPeerConnection'];
+ if (!RTCPeerConnection) {
+ return;
+ }
+
+ const addSymbol = api.symbol('addEventListener');
+ const removeSymbol = api.symbol('removeEventListener');
+
+ RTCPeerConnection.prototype.addEventListener = RTCPeerConnection.prototype[addSymbol];
+ RTCPeerConnection.prototype.removeEventListener = RTCPeerConnection.prototype[removeSymbol];
+
+ // RTCPeerConnection extends EventTarget, so we must clear the symbol
+ // to allow patch RTCPeerConnection.prototype.addEventListener again
+ RTCPeerConnection.prototype[addSymbol] = null;
+ RTCPeerConnection.prototype[removeSymbol] = null;
+
+ api.patchEventTarget(global, [RTCPeerConnection.prototype], {useG: false});
+});
diff --git a/packages/zone.js/lib/browser/webapis-user-media.ts b/packages/zone.js/lib/browser/webapis-user-media.ts
new file mode 100644
index 0000000000..6d2cf5b986
--- /dev/null
+++ b/packages/zone.js/lib/browser/webapis-user-media.ts
@@ -0,0 +1,20 @@
+/**
+ * @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
+ */
+Zone.__load_patch('getUserMedia', (global: any, Zone: any, api: _ZonePrivate) => {
+ function wrapFunctionArgs(func: Function, source?: string): Function {
+ return function() {
+ const args = Array.prototype.slice.call(arguments);
+ const wrappedArgs = api.bindArguments(args, source ? source : (func as any).name);
+ return func.apply(this, wrappedArgs);
+ };
+ }
+ let navigator = global['navigator'];
+ if (navigator && navigator.getUserMedia) {
+ navigator.getUserMedia = wrapFunctionArgs(navigator.getUserMedia);
+ }
+});
diff --git a/packages/zone.js/lib/browser/websocket.ts b/packages/zone.js/lib/browser/websocket.ts
new file mode 100644
index 0000000000..cde4ded9bc
--- /dev/null
+++ b/packages/zone.js/lib/browser/websocket.ts
@@ -0,0 +1,59 @@
+/**
+ * @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
+ */
+
+// we have to patch the instance since the proto is non-configurable
+export function apply(api: _ZonePrivate, _global: any) {
+ const {ADD_EVENT_LISTENER_STR, REMOVE_EVENT_LISTENER_STR} = api.getGlobalObjects() !;
+ const WS = (_global).WebSocket;
+ // On Safari window.EventTarget doesn't exist so need to patch WS add/removeEventListener
+ // On older Chrome, no need since EventTarget was already patched
+ if (!(_global).EventTarget) {
+ api.patchEventTarget(_global, [WS.prototype]);
+ }
+ (_global).WebSocket = function(x: any, y: any) {
+ const socket = arguments.length > 1 ? new WS(x, y) : new WS(x);
+ let proxySocket: any;
+
+ let proxySocketProto: any;
+
+ // Safari 7.0 has non-configurable own 'onmessage' and friends properties on the socket instance
+ const onmessageDesc = api.ObjectGetOwnPropertyDescriptor(socket, 'onmessage');
+ if (onmessageDesc && onmessageDesc.configurable === false) {
+ proxySocket = api.ObjectCreate(socket);
+ // socket have own property descriptor 'onopen', 'onmessage', 'onclose', 'onerror'
+ // but proxySocket not, so we will keep socket as prototype and pass it to
+ // patchOnProperties method
+ proxySocketProto = socket;
+ [ADD_EVENT_LISTENER_STR, REMOVE_EVENT_LISTENER_STR, 'send', 'close'].forEach(function(
+ propName) {
+ proxySocket[propName] = function() {
+ const args = api.ArraySlice.call(arguments);
+ if (propName === ADD_EVENT_LISTENER_STR || propName === REMOVE_EVENT_LISTENER_STR) {
+ const eventName = args.length > 0 ? args[0] : undefined;
+ if (eventName) {
+ const propertySymbol = Zone.__symbol__('ON_PROPERTY' + eventName);
+ socket[propertySymbol] = proxySocket[propertySymbol];
+ }
+ }
+ return socket[propName].apply(socket, args);
+ };
+ });
+ } else {
+ // we can patch the real socket
+ proxySocket = socket;
+ }
+
+ api.patchOnProperties(proxySocket, ['close', 'error', 'message', 'open'], proxySocketProto);
+ return proxySocket;
+ };
+
+ const globalWebSocket = _global['WebSocket'];
+ for (const prop in WS) {
+ globalWebSocket[prop] = WS[prop];
+ }
+}
diff --git a/packages/zone.js/lib/closure/zone_externs.js b/packages/zone.js/lib/closure/zone_externs.js
new file mode 100644
index 0000000000..10c8e621f3
--- /dev/null
+++ b/packages/zone.js/lib/closure/zone_externs.js
@@ -0,0 +1,445 @@
+/**
+* @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
+*/
+
+/**
+ * @fileoverview Externs for zone.js
+ * @see https://github.com/angular/zone.js
+ * @externs
+ */
+
+/**
+ * @interface
+ */
+var Zone = function() {};
+/**
+ * @type {!Zone} The parent Zone.
+ */
+Zone.prototype.parent;
+/**
+ * @type {!string} The Zone name (useful for debugging)
+ */
+Zone.prototype.name;
+
+Zone.assertZonePatched = function() {};
+
+/**
+ * @type {!Zone} Returns the current [Zone]. Returns the current zone. The only way to change
+ * the current zone is by invoking a run() method, which will update the current zone for the
+ * duration of the run method callback.
+ */
+Zone.current;
+
+/**
+ * @type {Task} The task associated with the current execution.
+ */
+Zone.currentTask;
+
+/**
+ * @type {!Zone} Return the root zone.
+ */
+Zone.root;
+
+/**
+ * Returns a value associated with the `key`.
+ *
+ * If the current zone does not have a key, the request is delegated to the parent zone. Use
+ * [ZoneSpec.properties] to configure the set of properties associated with the current zone.
+ *
+ * @param {!string} key The key to retrieve.
+ * @returns {?} The value for the key, or `undefined` if not found.
+ */
+Zone.prototype.get = function(key) {};
+
+/**
+ * Returns a Zone which defines a `key`.
+ *
+ * Recursively search the parent Zone until a Zone which has a property `key` is found.
+ *
+ * @param {!string} key The key to use for identification of the returned zone.
+ * @returns {?Zone} The Zone which defines the `key`, `null` if not found.
+ */
+Zone.prototype.getZoneWith = function(key) {};
+
+/**
+ * Used to create a child zone.
+ *
+ * @param {!ZoneSpec} zoneSpec A set of rules which the child zone should follow.
+ * @returns {!Zone} A new child zone.
+ */
+Zone.prototype.fork = function(zoneSpec) {};
+
+/**
+ * Wraps a callback function in a new function which will properly restore the current zone upon
+ * invocation.
+ *
+ * The wrapped function will properly forward `this` as well as `arguments` to the `callback`.
+ *
+ * Before the function is wrapped the zone can intercept the `callback` by declaring
+ * [ZoneSpec.onIntercept].
+ *
+ * @param {!Function} callback the function which will be wrapped in the zone.
+ * @param {!string=} source A unique debug location of the API being wrapped.
+ * @returns {!Function} A function which will invoke the `callback` through [Zone.runGuarded].
+ */
+Zone.prototype.wrap = function(callback, source) {};
+
+/**
+ * Invokes a function in a given zone.
+ *
+ * The invocation of `callback` can be intercepted be declaring [ZoneSpec.onInvoke].
+ *
+ * @param {!Function} callback The function to invoke.
+ * @param {?Object=} applyThis
+ * @param {?Array=} applyArgs
+ * @param {?string=} source A unique debug location of the API being invoked.
+ * @returns {*} Value from the `callback` function.
+ */
+Zone.prototype.run = function(callback, applyThis, applyArgs, source) {};
+
+/**
+ * Invokes a function in a given zone and catches any exceptions.
+ *
+ * Any exceptions thrown will be forwarded to [Zone.HandleError].
+ *
+ * The invocation of `callback` can be intercepted be declaring [ZoneSpec.onInvoke]. The
+ * handling of exceptions can intercepted by declaring [ZoneSpec.handleError].
+ *
+ * @param {!Function} callback The function to invoke.
+ * @param {?Object=} applyThis
+ * @param {?Array=} applyArgs
+ * @param {?string=} source A unique debug location of the API being invoked.
+ * @returns {*} Value from the `callback` function.
+ */
+Zone.prototype.runGuarded = function(callback, applyThis, applyArgs, source) {};
+
+/**
+ * Execute the Task by restoring the [Zone.currentTask] in the Task's zone.
+ *
+ * @param {!Task} task
+ * @param {?Object=} applyThis
+ * @param {?Array=} applyArgs
+ * @returns {*}
+ */
+Zone.prototype.runTask = function(task, applyThis, applyArgs) {};
+
+/**
+ * @param {string} source
+ * @param {!Function} callback
+ * @param {?TaskData=} data
+ * @param {?function(!Task)=} customSchedule
+ * @return {!MicroTask} microTask
+ */
+Zone.prototype.scheduleMicroTask = function(source, callback, data, customSchedule) {};
+
+/**
+ * @param {string} source
+ * @param {!Function} callback
+ * @param {?TaskData=} data
+ * @param {?function(!Task)=} customSchedule
+ * @param {?function(!Task)=} customCancel
+ * @return {!MacroTask} macroTask
+ */
+Zone.prototype.scheduleMacroTask = function(source, callback, data, customSchedule, customCancel) {
+};
+
+/**
+ * @param {string} source
+ * @param {!Function} callback
+ * @param {?TaskData=} data
+ * @param {?function(!Task)=} customSchedule
+ * @param {?function(!Task)=} customCancel
+ * @return {!EventTask} eventTask
+ */
+Zone.prototype.scheduleEventTask = function(source, callback, data, customSchedule, customCancel) {
+};
+
+/**
+ * @param {!Task} task
+ * @return {!Task} task
+ */
+Zone.prototype.scheduleTask = function(task) {};
+
+/**
+ * @param {!Task} task
+ * @return {!Task} task
+ */
+Zone.prototype.cancelTask = function(task) {};
+
+/**
+ * @record
+ */
+var ZoneSpec = function() {};
+/**
+ * @type {!string} The name of the zone. Usefull when debugging Zones.
+ */
+ZoneSpec.prototype.name;
+
+/**
+ * @type {Object|undefined} A set of properties to be associated with Zone. Use
+ * [Zone.get] to retrieve them.
+ */
+ZoneSpec.prototype.properties;
+
+/**
+ * Allows the interception of zone forking.
+ *
+ * When the zone is being forked, the request is forwarded to this method for interception.
+ *
+ * @type {
+ * undefined|?function(ZoneDelegate, Zone, Zone, ZoneSpec): Zone
+ * }
+ */
+ZoneSpec.prototype.onFork;
+
+/**
+ * Allows the interception of the wrapping of the callback.
+ *
+ * When the zone is being forked, the request is forwarded to this method for interception.
+ *
+ * @type {
+ * undefined|?function(ZoneDelegate, Zone, Zone, Function, string): Function
+ * }
+ */
+ZoneSpec.prototype.onIntercept;
+
+/**
+ * Allows interception of the callback invocation.
+ *
+ * @type {
+ * undefined|?function(ZoneDelegate, Zone, Zone, Function, Object, Array, string): *
+ * }
+ */
+ZoneSpec.prototype.onInvoke;
+
+/**
+ * Allows interception of the error handling.
+ *
+ * @type {
+ * undefined|?function(ZoneDelegate, Zone, Zone, Object): boolean
+ * }
+ */
+ZoneSpec.prototype.onHandleError;
+
+/**
+ * Allows interception of task scheduling.
+ *
+ * @type {
+ * undefined|?function(ZoneDelegate, Zone, Zone, Task): Task
+ * }
+ */
+ZoneSpec.prototype.onScheduleTask;
+
+/**
+ * Allows interception of task invoke.
+ *
+ * @type {
+ * undefined|?function(ZoneDelegate, Zone, Zone, Task, Object, Array): *
+ * }
+ */
+ZoneSpec.prototype.onInvokeTask;
+
+/**
+ * Allows interception of task cancelation.
+ *
+ * @type {
+ * undefined|?function(ZoneDelegate, Zone, Zone, Task): *
+ * }
+ */
+ZoneSpec.prototype.onCancelTask;
+/**
+ * Notifies of changes to the task queue empty status.
+ *
+ * @type {
+ * undefined|?function(ZoneDelegate, Zone, Zone, HasTaskState)
+ * }
+ */
+ZoneSpec.prototype.onHasTask;
+
+/**
+ * @interface
+ */
+var ZoneDelegate = function() {};
+/**
+ * @type {!Zone} zone
+ */
+ZoneDelegate.prototype.zone;
+/**
+ * @param {!Zone} targetZone the [Zone] which originally received the request.
+ * @param {!ZoneSpec} zoneSpec the argument passed into the `fork` method.
+ * @returns {!Zone} the new forked zone
+ */
+ZoneDelegate.prototype.fork = function(targetZone, zoneSpec) {};
+/**
+ * @param {!Zone} targetZone the [Zone] which originally received the request.
+ * @param {!Function} callback the callback function passed into `wrap` function
+ * @param {string=} source the argument passed into the `wrap` method.
+ * @returns {!Function}
+ */
+ZoneDelegate.prototype.intercept = function(targetZone, callback, source) {};
+
+/**
+ * @param {Zone} targetZone the [Zone] which originally received the request.
+ * @param {!Function} callback the callback which will be invoked.
+ * @param {?Object=} applyThis the argument passed into the `run` method.
+ * @param {?Array=} applyArgs the argument passed into the `run` method.
+ * @param {?string=} source the argument passed into the `run` method.
+ * @returns {*}
+ */
+ZoneDelegate.prototype.invoke = function(targetZone, callback, applyThis, applyArgs, source) {};
+/**
+ * @param {!Zone} targetZone the [Zone] which originally received the request.
+ * @param {!Object} error the argument passed into the `handleError` method.
+ * @returns {boolean}
+ */
+ZoneDelegate.prototype.handleError = function(targetZone, error) {};
+/**
+ * @param {!Zone} targetZone the [Zone] which originally received the request.
+ * @param {!Task} task the argument passed into the `scheduleTask` method.
+ * @returns {!Task} task
+ */
+ZoneDelegate.prototype.scheduleTask = function(targetZone, task) {};
+/**
+ * @param {!Zone} targetZone The [Zone] which originally received the request.
+ * @param {!Task} task The argument passed into the `scheduleTask` method.
+ * @param {?Object=} applyThis The argument passed into the `run` method.
+ * @param {?Array=} applyArgs The argument passed into the `run` method.
+ * @returns {*}
+ */
+ZoneDelegate.prototype.invokeTask = function(targetZone, task, applyThis, applyArgs) {};
+/**
+ * @param {!Zone} targetZone The [Zone] which originally received the request.
+ * @param {!Task} task The argument passed into the `cancelTask` method.
+ * @returns {*}
+ */
+ZoneDelegate.prototype.cancelTask = function(targetZone, task) {};
+/**
+ * @param {!Zone} targetZone The [Zone] which originally received the request.
+ * @param {!HasTaskState} hasTaskState
+ */
+ZoneDelegate.prototype.hasTask = function(targetZone, hasTaskState) {};
+
+/**
+ * @interface
+ */
+var HasTaskState = function() {};
+
+/**
+ * @type {boolean}
+ */
+HasTaskState.prototype.microTask;
+/**
+ * @type {boolean}
+ */
+HasTaskState.prototype.macroTask;
+/**
+ * @type {boolean}
+ */
+HasTaskState.prototype.eventTask;
+/**
+ * @type {TaskType}
+ */
+HasTaskState.prototype.change;
+
+/**
+ * @interface
+ */
+var TaskType = function() {};
+
+/**
+ * @interface
+ */
+var TaskState = function() {};
+
+/**
+ * @interface
+ */
+var TaskData = function() {};
+/**
+ * @type {boolean|undefined}
+ */
+TaskData.prototype.isPeriodic;
+/**
+ * @type {number|undefined}
+ */
+TaskData.prototype.delay;
+/**
+ * @type {number|undefined}
+ */
+TaskData.prototype.handleId;
+
+/**
+ * @interface
+ */
+var Task = function() {};
+/**
+ * @type {TaskType}
+ */
+Task.prototype.type;
+/**
+ * @type {TaskState}
+ */
+Task.prototype.state;
+/**
+ * @type {string}
+ */
+Task.prototype.source;
+/**
+ * @type {Function}
+ */
+Task.prototype.invoke;
+/**
+ * @type {Function}
+ */
+Task.prototype.callback;
+/**
+ * @type {TaskData}
+ */
+Task.prototype.data;
+/**
+ * @param {!Task} task
+ */
+Task.prototype.scheduleFn = function(task) {};
+/**
+ * @param {!Task} task
+ */
+Task.prototype.cancelFn = function(task) {};
+/**
+ * @type {Zone}
+ */
+Task.prototype.zone;
+/**
+ * @type {number}
+ */
+Task.prototype.runCount;
+Task.prototype.cancelSchduleRequest = function() {};
+
+/**
+ * @interface
+ * @extends {Task}
+ */
+var MicroTask = function() {};
+/**
+ * @interface
+ * @extends {Task}
+ */
+var MacroTask = function() {};
+/**
+ * @interface
+ * @extends {Task}
+ */
+var EventTask = function() {};
+
+/**
+ * @type {?string}
+ */
+Error.prototype.zoneAwareStack;
+
+/**
+ * @type {?string}
+ */
+Error.prototype.originalStack;
diff --git a/packages/zone.js/lib/common/error-rewrite.ts b/packages/zone.js/lib/common/error-rewrite.ts
new file mode 100644
index 0000000000..f88271318a
--- /dev/null
+++ b/packages/zone.js/lib/common/error-rewrite.ts
@@ -0,0 +1,378 @@
+/**
+ * @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
+ */
+/**
+ * @fileoverview
+ * @suppress {globalThis,undefinedVars}
+ */
+
+/**
+ * Extend the Error with additional fields for rewritten stack frames
+ */
+interface Error {
+ /**
+ * Stack trace where extra frames have been removed and zone names added.
+ */
+ zoneAwareStack?: string;
+
+ /**
+ * Original stack trace with no modifications
+ */
+ originalStack?: string;
+}
+
+Zone.__load_patch('Error', (global: any, Zone: ZoneType, api: _ZonePrivate) => {
+ /*
+ * This code patches Error so that:
+ * - It ignores un-needed stack frames.
+ * - It Shows the associated Zone for reach frame.
+ */
+
+ const enum FrameType {
+ /// Skip this frame when printing out stack
+ blackList,
+ /// This frame marks zone transition
+ transition
+ }
+
+ const blacklistedStackFramesSymbol = api.symbol('blacklistedStackFrames');
+ const NativeError = global[api.symbol('Error')] = global['Error'];
+ // Store the frames which should be removed from the stack frames
+ const blackListedStackFrames: {[frame: string]: FrameType} = {};
+ // We must find the frame where Error was created, otherwise we assume we don't understand stack
+ let zoneAwareFrame1: string;
+ let zoneAwareFrame2: string;
+ let zoneAwareFrame1WithoutNew: string;
+ let zoneAwareFrame2WithoutNew: string;
+ let zoneAwareFrame3WithoutNew: string;
+
+ global['Error'] = ZoneAwareError;
+ const stackRewrite = 'stackRewrite';
+
+ type BlackListedStackFramesPolicy = 'default' | 'disable' | 'lazy';
+ const blackListedStackFramesPolicy: BlackListedStackFramesPolicy =
+ global['__Zone_Error_BlacklistedStackFrames_policy'] || 'default';
+
+ interface ZoneFrameName {
+ zoneName: string;
+ parent?: ZoneFrameName;
+ }
+
+ function buildZoneFrameNames(zoneFrame: _ZoneFrame) {
+ let zoneFrameName: ZoneFrameName = {zoneName: zoneFrame.zone.name};
+ let result = zoneFrameName;
+ while (zoneFrame.parent) {
+ zoneFrame = zoneFrame.parent;
+ const parentZoneFrameName = {zoneName: zoneFrame.zone.name};
+ zoneFrameName.parent = parentZoneFrameName;
+ zoneFrameName = parentZoneFrameName;
+ }
+ return result;
+ }
+
+ function buildZoneAwareStackFrames(
+ originalStack: string, zoneFrame: _ZoneFrame | ZoneFrameName | null, isZoneFrame = true) {
+ let frames: string[] = originalStack.split('\n');
+ let i = 0;
+ // Find the first frame
+ while (!(frames[i] === zoneAwareFrame1 || frames[i] === zoneAwareFrame2 ||
+ frames[i] === zoneAwareFrame1WithoutNew || frames[i] === zoneAwareFrame2WithoutNew ||
+ frames[i] === zoneAwareFrame3WithoutNew) &&
+ i < frames.length) {
+ i++;
+ }
+ for (; i < frames.length && zoneFrame; i++) {
+ let frame = frames[i];
+ if (frame.trim()) {
+ switch (blackListedStackFrames[frame]) {
+ case FrameType.blackList:
+ frames.splice(i, 1);
+ i--;
+ break;
+ case FrameType.transition:
+ if (zoneFrame.parent) {
+ // This is the special frame where zone changed. Print and process it accordingly
+ zoneFrame = zoneFrame.parent;
+ } else {
+ zoneFrame = null;
+ }
+ frames.splice(i, 1);
+ i--;
+ break;
+ default:
+ frames[i] += isZoneFrame ? ` [${(zoneFrame as _ZoneFrame).zone.name}]` :
+ ` [${(zoneFrame as ZoneFrameName).zoneName}]`;
+ }
+ }
+ }
+ return frames.join('\n');
+ }
+ /**
+ * This is ZoneAwareError which processes the stack frame and cleans up extra frames as well as
+ * adds zone information to it.
+ */
+ function ZoneAwareError(): Error {
+ // We always have to return native error otherwise the browser console will not work.
+ let error: Error = NativeError.apply(this, arguments);
+ // Save original stack trace
+ const originalStack = (error as any)['originalStack'] = error.stack;
+
+ // Process the stack trace and rewrite the frames.
+ if ((ZoneAwareError as any)[stackRewrite] && originalStack) {
+ let zoneFrame = api.currentZoneFrame();
+ if (blackListedStackFramesPolicy === 'lazy') {
+ // don't handle stack trace now
+ (error as any)[api.symbol('zoneFrameNames')] = buildZoneFrameNames(zoneFrame);
+ } else if (blackListedStackFramesPolicy === 'default') {
+ try {
+ error.stack = error.zoneAwareStack = buildZoneAwareStackFrames(originalStack, zoneFrame);
+ } catch (e) {
+ // ignore as some browsers don't allow overriding of stack
+ }
+ }
+ }
+
+ if (this instanceof NativeError && this.constructor != NativeError) {
+ // We got called with a `new` operator AND we are subclass of ZoneAwareError
+ // in that case we have to copy all of our properties to `this`.
+ Object.keys(error).concat('stack', 'message').forEach((key) => {
+ const value = (error as any)[key];
+ if (value !== undefined) {
+ try {
+ this[key] = value;
+ } catch (e) {
+ // ignore the assignment in case it is a setter and it throws.
+ }
+ }
+ });
+ return this;
+ }
+ return error;
+ }
+
+ // Copy the prototype so that instanceof operator works as expected
+ ZoneAwareError.prototype = NativeError.prototype;
+ (ZoneAwareError as any)[blacklistedStackFramesSymbol] = blackListedStackFrames;
+ (ZoneAwareError as any)[stackRewrite] = false;
+
+ const zoneAwareStackSymbol = api.symbol('zoneAwareStack');
+
+ // try to define zoneAwareStack property when blackListed
+ // policy is delay
+ if (blackListedStackFramesPolicy === 'lazy') {
+ Object.defineProperty(ZoneAwareError.prototype, 'zoneAwareStack', {
+ configurable: true,
+ enumerable: true,
+ get: function() {
+ if (!this[zoneAwareStackSymbol]) {
+ this[zoneAwareStackSymbol] = buildZoneAwareStackFrames(
+ this.originalStack, this[api.symbol('zoneFrameNames')], false);
+ }
+ return this[zoneAwareStackSymbol];
+ },
+ set: function(newStack: string) {
+ this.originalStack = newStack;
+ this[zoneAwareStackSymbol] = buildZoneAwareStackFrames(
+ this.originalStack, this[api.symbol('zoneFrameNames')], false);
+ }
+ });
+ }
+
+ // those properties need special handling
+ const specialPropertyNames = ['stackTraceLimit', 'captureStackTrace', 'prepareStackTrace'];
+ // those properties of NativeError should be set to ZoneAwareError
+ const nativeErrorProperties = Object.keys(NativeError);
+ if (nativeErrorProperties) {
+ nativeErrorProperties.forEach(prop => {
+ if (specialPropertyNames.filter(sp => sp === prop).length === 0) {
+ Object.defineProperty(ZoneAwareError, prop, {
+ get: function() { return NativeError[prop]; },
+ set: function(value) { NativeError[prop] = value; }
+ });
+ }
+ });
+ }
+
+ if (NativeError.hasOwnProperty('stackTraceLimit')) {
+ // Extend default stack limit as we will be removing few frames.
+ NativeError.stackTraceLimit = Math.max(NativeError.stackTraceLimit, 15);
+
+ // make sure that ZoneAwareError has the same property which forwards to NativeError.
+ Object.defineProperty(ZoneAwareError, 'stackTraceLimit', {
+ get: function() { return NativeError.stackTraceLimit; },
+ set: function(value) { return NativeError.stackTraceLimit = value; }
+ });
+ }
+
+ if (NativeError.hasOwnProperty('captureStackTrace')) {
+ Object.defineProperty(ZoneAwareError, 'captureStackTrace', {
+ // add named function here because we need to remove this
+ // stack frame when prepareStackTrace below
+ value: function zoneCaptureStackTrace(targetObject: Object, constructorOpt?: Function) {
+ NativeError.captureStackTrace(targetObject, constructorOpt);
+ }
+ });
+ }
+
+ const ZONE_CAPTURESTACKTRACE = 'zoneCaptureStackTrace';
+ Object.defineProperty(ZoneAwareError, 'prepareStackTrace', {
+ get: function() { return NativeError.prepareStackTrace; },
+ set: function(value) {
+ if (!value || typeof value !== 'function') {
+ return NativeError.prepareStackTrace = value;
+ }
+ return NativeError.prepareStackTrace = function(
+ error: Error, structuredStackTrace: {getFunctionName: Function}[]) {
+ // remove additional stack information from ZoneAwareError.captureStackTrace
+ if (structuredStackTrace) {
+ for (let i = 0; i < structuredStackTrace.length; i++) {
+ const st = structuredStackTrace[i];
+ // remove the first function which name is zoneCaptureStackTrace
+ if (st.getFunctionName() === ZONE_CAPTURESTACKTRACE) {
+ structuredStackTrace.splice(i, 1);
+ break;
+ }
+ }
+ }
+ return value.call(this, error, structuredStackTrace);
+ };
+ }
+ });
+
+ if (blackListedStackFramesPolicy === 'disable') {
+ // don't need to run detectZone to populate
+ // blacklisted stack frames
+ return;
+ }
+ // Now we need to populate the `blacklistedStackFrames` as well as find the
+ // run/runGuarded/runTask frames. This is done by creating a detect zone and then threading
+ // the execution through all of the above methods so that we can look at the stack trace and
+ // find the frames of interest.
+
+ let detectZone: Zone = Zone.current.fork({
+ name: 'detect',
+ onHandleError: function(
+ parentZD: ZoneDelegate, current: Zone, target: Zone, error: any): boolean {
+ if (error.originalStack && Error === ZoneAwareError) {
+ let frames = error.originalStack.split(/\n/);
+ let runFrame = false, runGuardedFrame = false, runTaskFrame = false;
+ while (frames.length) {
+ let frame = frames.shift();
+ // On safari it is possible to have stack frame with no line number.
+ // This check makes sure that we don't filter frames on name only (must have
+ // line number or exact equals to `ZoneAwareError`)
+ if (/:\d+:\d+/.test(frame) || frame === 'ZoneAwareError') {
+ // Get rid of the path so that we don't accidentally find function name in path.
+ // In chrome the separator is `(` and `@` in FF and safari
+ // Chrome: at Zone.run (zone.js:100)
+ // Chrome: at Zone.run (http://localhost:9876/base/build/lib/zone.js:100:24)
+ // FireFox: Zone.prototype.run@http://localhost:9876/base/build/lib/zone.js:101:24
+ // Safari: run@http://localhost:9876/base/build/lib/zone.js:101:24
+ let fnName: string = frame.split('(')[0].split('@')[0];
+ let frameType = FrameType.transition;
+ if (fnName.indexOf('ZoneAwareError') !== -1) {
+ if (fnName.indexOf('new ZoneAwareError') !== -1) {
+ zoneAwareFrame1 = frame;
+ zoneAwareFrame2 = frame.replace('new ZoneAwareError', 'new Error.ZoneAwareError');
+ } else {
+ zoneAwareFrame1WithoutNew = frame;
+ zoneAwareFrame2WithoutNew = frame.replace('Error.', '');
+ if (frame.indexOf('Error.ZoneAwareError') === -1) {
+ zoneAwareFrame3WithoutNew =
+ frame.replace('ZoneAwareError', 'Error.ZoneAwareError');
+ }
+ }
+ blackListedStackFrames[zoneAwareFrame2] = FrameType.blackList;
+ }
+ if (fnName.indexOf('runGuarded') !== -1) {
+ runGuardedFrame = true;
+ } else if (fnName.indexOf('runTask') !== -1) {
+ runTaskFrame = true;
+ } else if (fnName.indexOf('run') !== -1) {
+ runFrame = true;
+ } else {
+ frameType = FrameType.blackList;
+ }
+ blackListedStackFrames[frame] = frameType;
+ // Once we find all of the frames we can stop looking.
+ if (runFrame && runGuardedFrame && runTaskFrame) {
+ (ZoneAwareError as any)[stackRewrite] = true;
+ break;
+ }
+ }
+ }
+ }
+ return false;
+ }
+ }) as Zone;
+ // carefully constructor a stack frame which contains all of the frames of interest which
+ // need to be detected and blacklisted.
+
+ const childDetectZone = detectZone.fork({
+ name: 'child',
+ onScheduleTask: function(delegate, curr, target, task) {
+ return delegate.scheduleTask(target, task);
+ },
+ onInvokeTask: function(delegate, curr, target, task, applyThis, applyArgs) {
+ return delegate.invokeTask(target, task, applyThis, applyArgs);
+ },
+ onCancelTask: function(delegate, curr, target, task) {
+ return delegate.cancelTask(target, task);
+ },
+ onInvoke: function(delegate, curr, target, callback, applyThis, applyArgs, source) {
+ return delegate.invoke(target, callback, applyThis, applyArgs, source);
+ }
+ });
+
+ // we need to detect all zone related frames, it will
+ // exceed default stackTraceLimit, so we set it to
+ // larger number here, and restore it after detect finish.
+ const originalStackTraceLimit = Error.stackTraceLimit;
+ Error.stackTraceLimit = 100;
+ // we schedule event/micro/macro task, and invoke them
+ // when onSchedule, so we can get all stack traces for
+ // all kinds of tasks with one error thrown.
+ childDetectZone.run(() => {
+ childDetectZone.runGuarded(() => {
+ const fakeTransitionTo = () => {};
+ childDetectZone.scheduleEventTask(
+ blacklistedStackFramesSymbol,
+ () => {
+ childDetectZone.scheduleMacroTask(
+ blacklistedStackFramesSymbol,
+ () => {
+ childDetectZone.scheduleMicroTask(
+ blacklistedStackFramesSymbol, () => { throw new Error(); }, undefined,
+ (t: Task) => {
+ (t as any)._transitionTo = fakeTransitionTo;
+ t.invoke();
+ });
+ childDetectZone.scheduleMicroTask(
+ blacklistedStackFramesSymbol, () => { throw Error(); }, undefined,
+ (t: Task) => {
+ (t as any)._transitionTo = fakeTransitionTo;
+ t.invoke();
+ });
+ },
+ undefined,
+ (t) => {
+ (t as any)._transitionTo = fakeTransitionTo;
+ t.invoke();
+ },
+ () => {});
+ },
+ undefined,
+ (t) => {
+ (t as any)._transitionTo = fakeTransitionTo;
+ t.invoke();
+ },
+ () => {});
+ });
+ });
+
+ Error.stackTraceLimit = originalStackTraceLimit;
+});
diff --git a/packages/zone.js/lib/common/events.ts b/packages/zone.js/lib/common/events.ts
new file mode 100644
index 0000000000..41ee5e8370
--- /dev/null
+++ b/packages/zone.js/lib/common/events.ts
@@ -0,0 +1,679 @@
+/**
+ * @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
+ */
+/**
+ * @fileoverview
+ * @suppress {missingRequire}
+ */
+
+import {ADD_EVENT_LISTENER_STR, FALSE_STR, ObjectGetPrototypeOf, REMOVE_EVENT_LISTENER_STR, TRUE_STR, ZONE_SYMBOL_PREFIX, attachOriginToPatched, isNode, zoneSymbol} from './utils';
+
+
+/** @internal **/
+interface EventTaskData extends TaskData {
+ // use global callback or not
+ readonly useG?: boolean;
+}
+
+let passiveSupported = false;
+
+if (typeof window !== 'undefined') {
+ try {
+ const options =
+ Object.defineProperty({}, 'passive', {get: function() { passiveSupported = true; }});
+
+ window.addEventListener('test', options, options);
+ window.removeEventListener('test', options, options);
+ } catch (err) {
+ passiveSupported = false;
+ }
+}
+
+// an identifier to tell ZoneTask do not create a new invoke closure
+const OPTIMIZED_ZONE_EVENT_TASK_DATA: EventTaskData = {
+ useG: true
+};
+
+export const zoneSymbolEventNames: any = {};
+export const globalSources: any = {};
+
+const EVENT_NAME_SYMBOL_REGX = new RegExp('^' + ZONE_SYMBOL_PREFIX + '(\\w+)(true|false)$');
+const IMMEDIATE_PROPAGATION_SYMBOL = zoneSymbol('propagationStopped');
+
+export interface PatchEventTargetOptions {
+ // validateHandler
+ vh?: (nativeDelegate: any, delegate: any, target: any, args: any) => boolean;
+ // addEventListener function name
+ add?: string;
+ // removeEventListener function name
+ rm?: string;
+ // prependEventListener function name
+ prepend?: string;
+ // listeners function name
+ listeners?: string;
+ // removeAllListeners function name
+ rmAll?: string;
+ // useGlobalCallback flag
+ useG?: boolean;
+ // check duplicate flag when addEventListener
+ chkDup?: boolean;
+ // return target flag when addEventListener
+ rt?: boolean;
+ // event compare handler
+ diff?: (task: any, delegate: any) => boolean;
+ // support passive or not
+ supportPassive?: boolean;
+ // get string from eventName (in nodejs, eventName maybe Symbol)
+ eventNameToString?: (eventName: any) => string;
+}
+
+export function patchEventTarget(
+ _global: any, apis: any[], patchOptions?: PatchEventTargetOptions) {
+ const ADD_EVENT_LISTENER = (patchOptions && patchOptions.add) || ADD_EVENT_LISTENER_STR;
+ const REMOVE_EVENT_LISTENER = (patchOptions && patchOptions.rm) || REMOVE_EVENT_LISTENER_STR;
+
+ const LISTENERS_EVENT_LISTENER = (patchOptions && patchOptions.listeners) || 'eventListeners';
+ const REMOVE_ALL_LISTENERS_EVENT_LISTENER =
+ (patchOptions && patchOptions.rmAll) || 'removeAllListeners';
+
+ const zoneSymbolAddEventListener = zoneSymbol(ADD_EVENT_LISTENER);
+
+ const ADD_EVENT_LISTENER_SOURCE = '.' + ADD_EVENT_LISTENER + ':';
+
+ const PREPEND_EVENT_LISTENER = 'prependListener';
+ const PREPEND_EVENT_LISTENER_SOURCE = '.' + PREPEND_EVENT_LISTENER + ':';
+
+ const invokeTask = function(task: any, target: any, event: Event) {
+ // for better performance, check isRemoved which is set
+ // by removeEventListener
+ if (task.isRemoved) {
+ return;
+ }
+ const delegate = task.callback;
+ if (typeof delegate === 'object' && delegate.handleEvent) {
+ // create the bind version of handleEvent when invoke
+ task.callback = (event: Event) => delegate.handleEvent(event);
+ task.originalDelegate = delegate;
+ }
+ // invoke static task.invoke
+ task.invoke(task, target, [event]);
+ const options = task.options;
+ if (options && typeof options === 'object' && options.once) {
+ // if options.once is true, after invoke once remove listener here
+ // only browser need to do this, nodejs eventEmitter will cal removeListener
+ // inside EventEmitter.once
+ const delegate = task.originalDelegate ? task.originalDelegate : task.callback;
+ target[REMOVE_EVENT_LISTENER].call(target, event.type, delegate, options);
+ }
+ };
+
+ // global shared zoneAwareCallback to handle all event callback with capture = false
+ const globalZoneAwareCallback = function(event: Event) {
+ // https://github.com/angular/zone.js/issues/911, in IE, sometimes
+ // event will be undefined, so we need to use window.event
+ event = event || _global.event;
+ if (!event) {
+ return;
+ }
+ // event.target is needed for Samsung TV and SourceBuffer
+ // || global is needed https://github.com/angular/zone.js/issues/190
+ const target: any = this || event.target || _global;
+ const tasks = target[zoneSymbolEventNames[event.type][FALSE_STR]];
+ if (tasks) {
+ // invoke all tasks which attached to current target with given event.type and capture = false
+ // for performance concern, if task.length === 1, just invoke
+ if (tasks.length === 1) {
+ invokeTask(tasks[0], target, event);
+ } else {
+ // https://github.com/angular/zone.js/issues/836
+ // copy the tasks array before invoke, to avoid
+ // the callback will remove itself or other listener
+ const copyTasks = tasks.slice();
+ for (let i = 0; i < copyTasks.length; i++) {
+ if (event && (event as any)[IMMEDIATE_PROPAGATION_SYMBOL] === true) {
+ break;
+ }
+ invokeTask(copyTasks[i], target, event);
+ }
+ }
+ }
+ };
+
+ // global shared zoneAwareCallback to handle all event callback with capture = true
+ const globalZoneAwareCaptureCallback = function(event: Event) {
+ // https://github.com/angular/zone.js/issues/911, in IE, sometimes
+ // event will be undefined, so we need to use window.event
+ event = event || _global.event;
+ if (!event) {
+ return;
+ }
+ // event.target is needed for Samsung TV and SourceBuffer
+ // || global is needed https://github.com/angular/zone.js/issues/190
+ const target: any = this || event.target || _global;
+ const tasks = target[zoneSymbolEventNames[event.type][TRUE_STR]];
+ if (tasks) {
+ // invoke all tasks which attached to current target with given event.type and capture = false
+ // for performance concern, if task.length === 1, just invoke
+ if (tasks.length === 1) {
+ invokeTask(tasks[0], target, event);
+ } else {
+ // https://github.com/angular/zone.js/issues/836
+ // copy the tasks array before invoke, to avoid
+ // the callback will remove itself or other listener
+ const copyTasks = tasks.slice();
+ for (let i = 0; i < copyTasks.length; i++) {
+ if (event && (event as any)[IMMEDIATE_PROPAGATION_SYMBOL] === true) {
+ break;
+ }
+ invokeTask(copyTasks[i], target, event);
+ }
+ }
+ }
+ };
+
+ function patchEventTargetMethods(obj: any, patchOptions?: PatchEventTargetOptions) {
+ if (!obj) {
+ return false;
+ }
+
+ let useGlobalCallback = true;
+ if (patchOptions && patchOptions.useG !== undefined) {
+ useGlobalCallback = patchOptions.useG;
+ }
+ const validateHandler = patchOptions && patchOptions.vh;
+
+ let checkDuplicate = true;
+ if (patchOptions && patchOptions.chkDup !== undefined) {
+ checkDuplicate = patchOptions.chkDup;
+ }
+
+ let returnTarget = false;
+ if (patchOptions && patchOptions.rt !== undefined) {
+ returnTarget = patchOptions.rt;
+ }
+
+ let proto = obj;
+ while (proto && !proto.hasOwnProperty(ADD_EVENT_LISTENER)) {
+ proto = ObjectGetPrototypeOf(proto);
+ }
+ if (!proto && obj[ADD_EVENT_LISTENER]) {
+ // somehow we did not find it, but we can see it. This happens on IE for Window properties.
+ proto = obj;
+ }
+
+ if (!proto) {
+ return false;
+ }
+ if (proto[zoneSymbolAddEventListener]) {
+ return false;
+ }
+
+ const eventNameToString = patchOptions && patchOptions.eventNameToString;
+
+ // a shared global taskData to pass data for scheduleEventTask
+ // so we do not need to create a new object just for pass some data
+ const taskData: any = {};
+
+ const nativeAddEventListener = proto[zoneSymbolAddEventListener] = proto[ADD_EVENT_LISTENER];
+ const nativeRemoveEventListener = proto[zoneSymbol(REMOVE_EVENT_LISTENER)] =
+ proto[REMOVE_EVENT_LISTENER];
+
+ const nativeListeners = proto[zoneSymbol(LISTENERS_EVENT_LISTENER)] =
+ proto[LISTENERS_EVENT_LISTENER];
+ const nativeRemoveAllListeners = proto[zoneSymbol(REMOVE_ALL_LISTENERS_EVENT_LISTENER)] =
+ proto[REMOVE_ALL_LISTENERS_EVENT_LISTENER];
+
+ let nativePrependEventListener: any;
+ if (patchOptions && patchOptions.prepend) {
+ nativePrependEventListener = proto[zoneSymbol(patchOptions.prepend)] =
+ proto[patchOptions.prepend];
+ }
+
+ function checkIsPassive(task: Task) {
+ if (!passiveSupported && typeof taskData.options !== 'boolean' &&
+ typeof taskData.options !== 'undefined' && taskData.options !== null) {
+ // options is a non-null non-undefined object
+ // passive is not supported
+ // don't pass options as object
+ // just pass capture as a boolean
+ (task as any).options = !!taskData.options.capture;
+ taskData.options = (task as any).options;
+ }
+ }
+
+ const customScheduleGlobal = function(task: Task) {
+ // if there is already a task for the eventName + capture,
+ // just return, because we use the shared globalZoneAwareCallback here.
+ if (taskData.isExisting) {
+ return;
+ }
+ checkIsPassive(task);
+ return nativeAddEventListener.call(
+ taskData.target, taskData.eventName,
+ taskData.capture ? globalZoneAwareCaptureCallback : globalZoneAwareCallback,
+ taskData.options);
+ };
+
+ const customCancelGlobal = function(task: any) {
+ // if task is not marked as isRemoved, this call is directly
+ // from Zone.prototype.cancelTask, we should remove the task
+ // from tasksList of target first
+ if (!task.isRemoved) {
+ const symbolEventNames = zoneSymbolEventNames[task.eventName];
+ let symbolEventName;
+ if (symbolEventNames) {
+ symbolEventName = symbolEventNames[task.capture ? TRUE_STR : FALSE_STR];
+ }
+ const existingTasks = symbolEventName && task.target[symbolEventName];
+ if (existingTasks) {
+ for (let i = 0; i < existingTasks.length; i++) {
+ const existingTask = existingTasks[i];
+ if (existingTask === task) {
+ existingTasks.splice(i, 1);
+ // set isRemoved to data for faster invokeTask check
+ task.isRemoved = true;
+ if (existingTasks.length === 0) {
+ // all tasks for the eventName + capture have gone,
+ // remove globalZoneAwareCallback and remove the task cache from target
+ task.allRemoved = true;
+ task.target[symbolEventName] = null;
+ }
+ break;
+ }
+ }
+ }
+ }
+ // if all tasks for the eventName + capture have gone,
+ // we will really remove the global event callback,
+ // if not, return
+ if (!task.allRemoved) {
+ return;
+ }
+ return nativeRemoveEventListener.call(
+ task.target, task.eventName,
+ task.capture ? globalZoneAwareCaptureCallback : globalZoneAwareCallback, task.options);
+ };
+
+ const customScheduleNonGlobal = function(task: Task) {
+ checkIsPassive(task);
+ return nativeAddEventListener.call(
+ taskData.target, taskData.eventName, task.invoke, taskData.options);
+ };
+
+ const customSchedulePrepend = function(task: Task) {
+ return nativePrependEventListener.call(
+ taskData.target, taskData.eventName, task.invoke, taskData.options);
+ };
+
+ const customCancelNonGlobal = function(task: any) {
+ return nativeRemoveEventListener.call(task.target, task.eventName, task.invoke, task.options);
+ };
+
+ const customSchedule = useGlobalCallback ? customScheduleGlobal : customScheduleNonGlobal;
+ const customCancel = useGlobalCallback ? customCancelGlobal : customCancelNonGlobal;
+
+ const compareTaskCallbackVsDelegate = function(task: any, delegate: any) {
+ const typeOfDelegate = typeof delegate;
+ return (typeOfDelegate === 'function' && task.callback === delegate) ||
+ (typeOfDelegate === 'object' && task.originalDelegate === delegate);
+ };
+
+ const compare =
+ (patchOptions && patchOptions.diff) ? patchOptions.diff : compareTaskCallbackVsDelegate;
+
+ const blackListedEvents: string[] = (Zone as any)[zoneSymbol('BLACK_LISTED_EVENTS')];
+
+ const makeAddListener = function(
+ nativeListener: any, addSource: string, customScheduleFn: any, customCancelFn: any,
+ returnTarget = false, prepend = false) {
+ return function() {
+ const target = this || _global;
+ const eventName = arguments[0];
+ let delegate = arguments[1];
+ if (!delegate) {
+ return nativeListener.apply(this, arguments);
+ }
+ if (isNode && eventName === 'uncaughtException') {
+ // don't patch uncaughtException of nodejs to prevent endless loop
+ return nativeListener.apply(this, arguments);
+ }
+
+ // don't create the bind delegate function for handleEvent
+ // case here to improve addEventListener performance
+ // we will create the bind delegate when invoke
+ let isHandleEvent = false;
+ if (typeof delegate !== 'function') {
+ if (!delegate.handleEvent) {
+ return nativeListener.apply(this, arguments);
+ }
+ isHandleEvent = true;
+ }
+
+ if (validateHandler && !validateHandler(nativeListener, delegate, target, arguments)) {
+ return;
+ }
+
+ const options = arguments[2];
+
+ if (blackListedEvents) {
+ // check black list
+ for (let i = 0; i < blackListedEvents.length; i++) {
+ if (eventName === blackListedEvents[i]) {
+ return nativeListener.apply(this, arguments);
+ }
+ }
+ }
+
+ let capture;
+ let once = false;
+ if (options === undefined) {
+ capture = false;
+ } else if (options === true) {
+ capture = true;
+ } else if (options === false) {
+ capture = false;
+ } else {
+ capture = options ? !!options.capture : false;
+ once = options ? !!options.once : false;
+ }
+
+ const zone = Zone.current;
+ const symbolEventNames = zoneSymbolEventNames[eventName];
+ let symbolEventName;
+ if (!symbolEventNames) {
+ // the code is duplicate, but I just want to get some better performance
+ const falseEventName =
+ (eventNameToString ? eventNameToString(eventName) : eventName) + FALSE_STR;
+ const trueEventName =
+ (eventNameToString ? eventNameToString(eventName) : eventName) + TRUE_STR;
+ const symbol = ZONE_SYMBOL_PREFIX + falseEventName;
+ const symbolCapture = ZONE_SYMBOL_PREFIX + trueEventName;
+ zoneSymbolEventNames[eventName] = {};
+ zoneSymbolEventNames[eventName][FALSE_STR] = symbol;
+ zoneSymbolEventNames[eventName][TRUE_STR] = symbolCapture;
+ symbolEventName = capture ? symbolCapture : symbol;
+ } else {
+ symbolEventName = symbolEventNames[capture ? TRUE_STR : FALSE_STR];
+ }
+ let existingTasks = target[symbolEventName];
+ let isExisting = false;
+ if (existingTasks) {
+ // already have task registered
+ isExisting = true;
+ if (checkDuplicate) {
+ for (let i = 0; i < existingTasks.length; i++) {
+ if (compare(existingTasks[i], delegate)) {
+ // same callback, same capture, same event name, just return
+ return;
+ }
+ }
+ }
+ } else {
+ existingTasks = target[symbolEventName] = [];
+ }
+ let source;
+ const constructorName = target.constructor['name'];
+ const targetSource = globalSources[constructorName];
+ if (targetSource) {
+ source = targetSource[eventName];
+ }
+ if (!source) {
+ source = constructorName + addSource +
+ (eventNameToString ? eventNameToString(eventName) : eventName);
+ }
+ // do not create a new object as task.data to pass those things
+ // just use the global shared one
+ taskData.options = options;
+ if (once) {
+ // if addEventListener with once options, we don't pass it to
+ // native addEventListener, instead we keep the once setting
+ // and handle ourselves.
+ taskData.options.once = false;
+ }
+ taskData.target = target;
+ taskData.capture = capture;
+ taskData.eventName = eventName;
+ taskData.isExisting = isExisting;
+
+ const data = useGlobalCallback ? OPTIMIZED_ZONE_EVENT_TASK_DATA : undefined;
+
+ // keep taskData into data to allow onScheduleEventTask to access the task information
+ if (data) {
+ (data as any).taskData = taskData;
+ }
+
+ const task: any =
+ zone.scheduleEventTask(source, delegate, data, customScheduleFn, customCancelFn);
+
+ // should clear taskData.target to avoid memory leak
+ // issue, https://github.com/angular/angular/issues/20442
+ taskData.target = null;
+
+ // need to clear up taskData because it is a global object
+ if (data) {
+ (data as any).taskData = null;
+ }
+
+ // have to save those information to task in case
+ // application may call task.zone.cancelTask() directly
+ if (once) {
+ options.once = true;
+ }
+ if (!(!passiveSupported && typeof task.options === 'boolean')) {
+ // if not support passive, and we pass an option object
+ // to addEventListener, we should save the options to task
+ task.options = options;
+ }
+ task.target = target;
+ task.capture = capture;
+ task.eventName = eventName;
+ if (isHandleEvent) {
+ // save original delegate for compare to check duplicate
+ (task as any).originalDelegate = delegate;
+ }
+ if (!prepend) {
+ existingTasks.push(task);
+ } else {
+ existingTasks.unshift(task);
+ }
+
+ if (returnTarget) {
+ return target;
+ }
+ };
+ };
+
+ proto[ADD_EVENT_LISTENER] = makeAddListener(
+ nativeAddEventListener, ADD_EVENT_LISTENER_SOURCE, customSchedule, customCancel,
+ returnTarget);
+ if (nativePrependEventListener) {
+ proto[PREPEND_EVENT_LISTENER] = makeAddListener(
+ nativePrependEventListener, PREPEND_EVENT_LISTENER_SOURCE, customSchedulePrepend,
+ customCancel, returnTarget, true);
+ }
+
+ proto[REMOVE_EVENT_LISTENER] = function() {
+ const target = this || _global;
+ const eventName = arguments[0];
+ const options = arguments[2];
+
+ let capture;
+ if (options === undefined) {
+ capture = false;
+ } else if (options === true) {
+ capture = true;
+ } else if (options === false) {
+ capture = false;
+ } else {
+ capture = options ? !!options.capture : false;
+ }
+
+ const delegate = arguments[1];
+ if (!delegate) {
+ return nativeRemoveEventListener.apply(this, arguments);
+ }
+
+ if (validateHandler &&
+ !validateHandler(nativeRemoveEventListener, delegate, target, arguments)) {
+ return;
+ }
+
+ const symbolEventNames = zoneSymbolEventNames[eventName];
+ let symbolEventName;
+ if (symbolEventNames) {
+ symbolEventName = symbolEventNames[capture ? TRUE_STR : FALSE_STR];
+ }
+ const existingTasks = symbolEventName && target[symbolEventName];
+ if (existingTasks) {
+ for (let i = 0; i < existingTasks.length; i++) {
+ const existingTask = existingTasks[i];
+ if (compare(existingTask, delegate)) {
+ existingTasks.splice(i, 1);
+ // set isRemoved to data for faster invokeTask check
+ (existingTask as any).isRemoved = true;
+ if (existingTasks.length === 0) {
+ // all tasks for the eventName + capture have gone,
+ // remove globalZoneAwareCallback and remove the task cache from target
+ (existingTask as any).allRemoved = true;
+ target[symbolEventName] = null;
+ }
+ existingTask.zone.cancelTask(existingTask);
+ if (returnTarget) {
+ return target;
+ }
+ return;
+ }
+ }
+ }
+ // issue 930, didn't find the event name or callback
+ // from zone kept existingTasks, the callback maybe
+ // added outside of zone, we need to call native removeEventListener
+ // to try to remove it.
+ return nativeRemoveEventListener.apply(this, arguments);
+ };
+
+ proto[LISTENERS_EVENT_LISTENER] = function() {
+ const target = this || _global;
+ const eventName = arguments[0];
+
+ const listeners: any[] = [];
+ const tasks =
+ findEventTasks(target, eventNameToString ? eventNameToString(eventName) : eventName);
+
+ for (let i = 0; i < tasks.length; i++) {
+ const task: any = tasks[i];
+ let delegate = task.originalDelegate ? task.originalDelegate : task.callback;
+ listeners.push(delegate);
+ }
+ return listeners;
+ };
+
+ proto[REMOVE_ALL_LISTENERS_EVENT_LISTENER] = function() {
+ const target = this || _global;
+
+ const eventName = arguments[0];
+ if (!eventName) {
+ const keys = Object.keys(target);
+ for (let i = 0; i < keys.length; i++) {
+ const prop = keys[i];
+ const match = EVENT_NAME_SYMBOL_REGX.exec(prop);
+ let evtName = match && match[1];
+ // in nodejs EventEmitter, removeListener event is
+ // used for monitoring the removeListener call,
+ // so just keep removeListener eventListener until
+ // all other eventListeners are removed
+ if (evtName && evtName !== 'removeListener') {
+ this[REMOVE_ALL_LISTENERS_EVENT_LISTENER].call(this, evtName);
+ }
+ }
+ // remove removeListener listener finally
+ this[REMOVE_ALL_LISTENERS_EVENT_LISTENER].call(this, 'removeListener');
+ } else {
+ const symbolEventNames = zoneSymbolEventNames[eventName];
+ if (symbolEventNames) {
+ const symbolEventName = symbolEventNames[FALSE_STR];
+ const symbolCaptureEventName = symbolEventNames[TRUE_STR];
+
+ const tasks = target[symbolEventName];
+ const captureTasks = target[symbolCaptureEventName];
+
+ if (tasks) {
+ const removeTasks = tasks.slice();
+ for (let i = 0; i < removeTasks.length; i++) {
+ const task = removeTasks[i];
+ let delegate = task.originalDelegate ? task.originalDelegate : task.callback;
+ this[REMOVE_EVENT_LISTENER].call(this, eventName, delegate, task.options);
+ }
+ }
+
+ if (captureTasks) {
+ const removeTasks = captureTasks.slice();
+ for (let i = 0; i < removeTasks.length; i++) {
+ const task = removeTasks[i];
+ let delegate = task.originalDelegate ? task.originalDelegate : task.callback;
+ this[REMOVE_EVENT_LISTENER].call(this, eventName, delegate, task.options);
+ }
+ }
+ }
+ }
+
+ if (returnTarget) {
+ return this;
+ }
+ };
+
+ // for native toString patch
+ attachOriginToPatched(proto[ADD_EVENT_LISTENER], nativeAddEventListener);
+ attachOriginToPatched(proto[REMOVE_EVENT_LISTENER], nativeRemoveEventListener);
+ if (nativeRemoveAllListeners) {
+ attachOriginToPatched(proto[REMOVE_ALL_LISTENERS_EVENT_LISTENER], nativeRemoveAllListeners);
+ }
+ if (nativeListeners) {
+ attachOriginToPatched(proto[LISTENERS_EVENT_LISTENER], nativeListeners);
+ }
+ return true;
+ }
+
+ let results: any[] = [];
+ for (let i = 0; i < apis.length; i++) {
+ results[i] = patchEventTargetMethods(apis[i], patchOptions);
+ }
+
+ return results;
+}
+
+export function findEventTasks(target: any, eventName: string): Task[] {
+ const foundTasks: any[] = [];
+ for (let prop in target) {
+ const match = EVENT_NAME_SYMBOL_REGX.exec(prop);
+ let evtName = match && match[1];
+ if (evtName && (!eventName || evtName === eventName)) {
+ const tasks: any = target[prop];
+ if (tasks) {
+ for (let i = 0; i < tasks.length; i++) {
+ foundTasks.push(tasks[i]);
+ }
+ }
+ }
+ }
+ return foundTasks;
+}
+
+export function patchEventPrototype(global: any, api: _ZonePrivate) {
+ const Event = global['Event'];
+ if (Event && Event.prototype) {
+ api.patchMethod(
+ Event.prototype, 'stopImmediatePropagation',
+ (delegate: Function) => function(self: any, args: any[]) {
+ self[IMMEDIATE_PROPAGATION_SYMBOL] = true;
+ // we need to call the native stopImmediatePropagation
+ // in case in some hybrid application, some part of
+ // application will be controlled by zone, some are not
+ delegate && delegate.apply(self, args);
+ });
+ }
+}
diff --git a/packages/zone.js/lib/common/fetch.ts b/packages/zone.js/lib/common/fetch.ts
new file mode 100644
index 0000000000..5b3f23058d
--- /dev/null
+++ b/packages/zone.js/lib/common/fetch.ts
@@ -0,0 +1,112 @@
+/**
+ * @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
+ */
+/**
+ * @fileoverview
+ * @suppress {missingRequire}
+ */
+
+Zone.__load_patch('fetch', (global: any, Zone: ZoneType, api: _ZonePrivate) => {
+ interface FetchTaskData extends TaskData {
+ fetchArgs?: any[];
+ }
+ let fetch = global['fetch'];
+ if (typeof fetch !== 'function') {
+ return;
+ }
+ const originalFetch = global[api.symbol('fetch')];
+ if (originalFetch) {
+ // restore unpatched fetch first
+ fetch = originalFetch;
+ }
+ const ZoneAwarePromise = global.Promise;
+ const symbolThenPatched = api.symbol('thenPatched');
+ const fetchTaskScheduling = api.symbol('fetchTaskScheduling');
+ const fetchTaskAborting = api.symbol('fetchTaskAborting');
+ const OriginalAbortController = global['AbortController'];
+ const supportAbort = typeof OriginalAbortController === 'function';
+ let abortNative: Function|null = null;
+ if (supportAbort) {
+ global['AbortController'] = function() {
+ const abortController = new OriginalAbortController();
+ const signal = abortController.signal;
+ signal.abortController = abortController;
+ return abortController;
+ };
+ abortNative = api.patchMethod(
+ OriginalAbortController.prototype, 'abort',
+ (delegate: Function) => (self: any, args: any) => {
+ if (self.task) {
+ return self.task.zone.cancelTask(self.task);
+ }
+ return delegate.apply(self, args);
+ });
+ }
+ const placeholder = function() {};
+ global['fetch'] = function() {
+ const args = Array.prototype.slice.call(arguments);
+ const options = args.length > 1 ? args[1] : null;
+ const signal = options && options.signal;
+ return new Promise((res, rej) => {
+ const task = Zone.current.scheduleMacroTask(
+ 'fetch', placeholder, { fetchArgs: args } as FetchTaskData,
+ () => {
+ let fetchPromise;
+ let zone = Zone.current;
+ try {
+ (zone as any)[fetchTaskScheduling] = true;
+ fetchPromise = fetch.apply(this, args);
+ } catch (error) {
+ rej(error);
+ return;
+ } finally {
+ (zone as any)[fetchTaskScheduling] = false;
+ }
+
+ if (!(fetchPromise instanceof ZoneAwarePromise)) {
+ let ctor = fetchPromise.constructor;
+ if (!ctor[symbolThenPatched]) {
+ api.patchThen(ctor);
+ }
+ }
+ fetchPromise.then(
+ (resource: any) => {
+ if (task.state !== 'notScheduled') {
+ task.invoke();
+ }
+ res(resource);
+ },
+ (error: any) => {
+ if (task.state !== 'notScheduled') {
+ task.invoke();
+ }
+ rej(error);
+ });
+ },
+ () => {
+ if (!supportAbort) {
+ rej('No AbortController supported, can not cancel fetch');
+ return;
+ }
+ if (signal && signal.abortController && !signal.aborted &&
+ typeof signal.abortController.abort === 'function' && abortNative) {
+ try {
+ (Zone.current as any)[fetchTaskAborting] = true;
+ abortNative.call(signal.abortController);
+ } finally {
+ (Zone.current as any)[fetchTaskAborting] = false;
+ }
+ } else {
+ rej('cancel fetch need a AbortController.signal');
+ }
+ });
+ if (signal && signal.abortController) {
+ signal.abortController.task = task;
+ }
+ });
+ };
+});
diff --git a/packages/zone.js/lib/common/promise.ts b/packages/zone.js/lib/common/promise.ts
new file mode 100644
index 0000000000..bb3f495a08
--- /dev/null
+++ b/packages/zone.js/lib/common/promise.ts
@@ -0,0 +1,481 @@
+/**
+ * @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
+ */
+Zone.__load_patch('ZoneAwarePromise', (global: any, Zone: ZoneType, api: _ZonePrivate) => {
+ const ObjectGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
+ const ObjectDefineProperty = Object.defineProperty;
+
+ function readableObjectToString(obj: any) {
+ if (obj && obj.toString === Object.prototype.toString) {
+ const className = obj.constructor && obj.constructor.name;
+ return (className ? className : '') + ': ' + JSON.stringify(obj);
+ }
+
+ return obj ? obj.toString() : Object.prototype.toString.call(obj);
+ }
+
+ const __symbol__ = api.symbol;
+ const _uncaughtPromiseErrors: UncaughtPromiseError[] = [];
+ const symbolPromise = __symbol__('Promise');
+ const symbolThen = __symbol__('then');
+ const creationTrace = '__creationTrace__';
+
+ api.onUnhandledError = (e: any) => {
+ if (api.showUncaughtError()) {
+ const rejection = e && e.rejection;
+ if (rejection) {
+ console.error(
+ 'Unhandled Promise rejection:',
+ rejection instanceof Error ? rejection.message : rejection, '; Zone:',
+ (e.zone).name, '; Task:', e.task && (e.task).source, '; Value:', rejection,
+ rejection instanceof Error ? rejection.stack : undefined);
+ } else {
+ console.error(e);
+ }
+ }
+ };
+
+ api.microtaskDrainDone = () => {
+ while (_uncaughtPromiseErrors.length) {
+ while (_uncaughtPromiseErrors.length) {
+ const uncaughtPromiseError: UncaughtPromiseError = _uncaughtPromiseErrors.shift() !;
+ try {
+ uncaughtPromiseError.zone.runGuarded(() => { throw uncaughtPromiseError; });
+ } catch (error) {
+ handleUnhandledRejection(error);
+ }
+ }
+ }
+ };
+
+ const UNHANDLED_PROMISE_REJECTION_HANDLER_SYMBOL = __symbol__('unhandledPromiseRejectionHandler');
+
+ function handleUnhandledRejection(e: any) {
+ api.onUnhandledError(e);
+ try {
+ const handler = (Zone as any)[UNHANDLED_PROMISE_REJECTION_HANDLER_SYMBOL];
+ if (handler && typeof handler === 'function') {
+ handler.call(this, e);
+ }
+ } catch (err) {
+ }
+ }
+
+ function isThenable(value: any): boolean { return value && value.then; }
+
+ function forwardResolution(value: any): any { return value; }
+
+ function forwardRejection(rejection: any): any { return ZoneAwarePromise.reject(rejection); }
+
+ const symbolState: string = __symbol__('state');
+ const symbolValue: string = __symbol__('value');
+ const symbolFinally: string = __symbol__('finally');
+ const symbolParentPromiseValue: string = __symbol__('parentPromiseValue');
+ const symbolParentPromiseState: string = __symbol__('parentPromiseState');
+ const source: string = 'Promise.then';
+ const UNRESOLVED: null = null;
+ const RESOLVED = true;
+ const REJECTED = false;
+ const REJECTED_NO_CATCH = 0;
+
+ function makeResolver(promise: ZoneAwarePromise, state: boolean): (value: any) => void {
+ return (v) => {
+ try {
+ resolvePromise(promise, state, v);
+ } catch (err) {
+ resolvePromise(promise, false, err);
+ }
+ // Do not return value or you will break the Promise spec.
+ };
+ }
+
+ const once = function() {
+ let wasCalled = false;
+
+ return function wrapper(wrappedFunction: Function) {
+ return function() {
+ if (wasCalled) {
+ return;
+ }
+ wasCalled = true;
+ wrappedFunction.apply(null, arguments);
+ };
+ };
+ };
+
+ const TYPE_ERROR = 'Promise resolved with itself';
+ const CURRENT_TASK_TRACE_SYMBOL = __symbol__('currentTaskTrace');
+
+ // Promise Resolution
+ function resolvePromise(
+ promise: ZoneAwarePromise, state: boolean, value: any): ZoneAwarePromise {
+ const onceWrapper = once();
+ if (promise === value) {
+ throw new TypeError(TYPE_ERROR);
+ }
+ if ((promise as any)[symbolState] === UNRESOLVED) {
+ // should only get value.then once based on promise spec.
+ let then: any = null;
+ try {
+ if (typeof value === 'object' || typeof value === 'function') {
+ then = value && value.then;
+ }
+ } catch (err) {
+ onceWrapper(() => { resolvePromise(promise, false, err); })();
+ return promise;
+ }
+ // if (value instanceof ZoneAwarePromise) {
+ if (state !== REJECTED && value instanceof ZoneAwarePromise &&
+ value.hasOwnProperty(symbolState) && value.hasOwnProperty(symbolValue) &&
+ (value as any)[symbolState] !== UNRESOLVED) {
+ clearRejectedNoCatch(>value as any);
+ resolvePromise(promise, (value as any)[symbolState], (value as any)[symbolValue]);
+ } else if (state !== REJECTED && typeof then === 'function') {
+ try {
+ then.call(
+ value, onceWrapper(makeResolver(promise, state)),
+ onceWrapper(makeResolver(promise, false)));
+ } catch (err) {
+ onceWrapper(() => { resolvePromise(promise, false, err); })();
+ }
+ } else {
+ (promise as any)[symbolState] = state;
+ const queue = (promise as any)[symbolValue];
+ (promise as any)[symbolValue] = value;
+
+ if ((promise as any)[symbolFinally] === symbolFinally) {
+ // the promise is generated by Promise.prototype.finally
+ if (state === RESOLVED) {
+ // the state is resolved, should ignore the value
+ // and use parent promise value
+ (promise as any)[symbolState] = (promise as any)[symbolParentPromiseState];
+ (promise as any)[symbolValue] = (promise as any)[symbolParentPromiseValue];
+ }
+ }
+
+ // record task information in value when error occurs, so we can
+ // do some additional work such as render longStackTrace
+ if (state === REJECTED && value instanceof Error) {
+ // check if longStackTraceZone is here
+ const trace = Zone.currentTask && Zone.currentTask.data &&
+ (Zone.currentTask.data as any)[creationTrace];
+ if (trace) {
+ // only keep the long stack trace into error when in longStackTraceZone
+ ObjectDefineProperty(
+ value, CURRENT_TASK_TRACE_SYMBOL,
+ {configurable: true, enumerable: false, writable: true, value: trace});
+ }
+ }
+
+ for (let i = 0; i < queue.length;) {
+ scheduleResolveOrReject(promise, queue[i++], queue[i++], queue[i++], queue[i++]);
+ }
+ if (queue.length == 0 && state == REJECTED) {
+ (promise as any)[symbolState] = REJECTED_NO_CATCH;
+ try {
+ // try to print more readable error log
+ throw new Error(
+ 'Uncaught (in promise): ' + readableObjectToString(value) +
+ (value && value.stack ? '\n' + value.stack : ''));
+ } catch (err) {
+ const error: UncaughtPromiseError = err;
+ error.rejection = value;
+ error.promise = promise;
+ error.zone = Zone.current;
+ error.task = Zone.currentTask !;
+ _uncaughtPromiseErrors.push(error);
+ api.scheduleMicroTask(); // to make sure that it is running
+ }
+ }
+ }
+ }
+ // Resolving an already resolved promise is a noop.
+ return promise;
+ }
+
+ const REJECTION_HANDLED_HANDLER = __symbol__('rejectionHandledHandler');
+ function clearRejectedNoCatch(promise: ZoneAwarePromise): void {
+ if ((promise as any)[symbolState] === REJECTED_NO_CATCH) {
+ // if the promise is rejected no catch status
+ // and queue.length > 0, means there is a error handler
+ // here to handle the rejected promise, we should trigger
+ // windows.rejectionhandled eventHandler or nodejs rejectionHandled
+ // eventHandler
+ try {
+ const handler = (Zone as any)[REJECTION_HANDLED_HANDLER];
+ if (handler && typeof handler === 'function') {
+ handler.call(this, {rejection: (promise as any)[symbolValue], promise: promise});
+ }
+ } catch (err) {
+ }
+ (promise as any)[symbolState] = REJECTED;
+ for (let i = 0; i < _uncaughtPromiseErrors.length; i++) {
+ if (promise === _uncaughtPromiseErrors[i].promise) {
+ _uncaughtPromiseErrors.splice(i, 1);
+ }
+ }
+ }
+ }
+
+ function scheduleResolveOrReject(
+ promise: ZoneAwarePromise, zone: AmbientZone, chainPromise: ZoneAwarePromise,
+ onFulfilled?: ((value: R) => U1) | null | undefined,
+ onRejected?: ((error: any) => U2) | null | undefined): void {
+ clearRejectedNoCatch(promise);
+ const promiseState = (promise as any)[symbolState];
+ const delegate = promiseState ?
+ (typeof onFulfilled === 'function') ? onFulfilled : forwardResolution :
+ (typeof onRejected === 'function') ? onRejected : forwardRejection;
+ zone.scheduleMicroTask(source, () => {
+ try {
+ const parentPromiseValue = (promise as any)[symbolValue];
+ const isFinallyPromise =
+ !!chainPromise && symbolFinally === (chainPromise as any)[symbolFinally];
+ if (isFinallyPromise) {
+ // if the promise is generated from finally call, keep parent promise's state and value
+ (chainPromise as any)[symbolParentPromiseValue] = parentPromiseValue;
+ (chainPromise as any)[symbolParentPromiseState] = promiseState;
+ }
+ // should not pass value to finally callback
+ const value = zone.run(
+ delegate, undefined,
+ isFinallyPromise && delegate !== forwardRejection && delegate !== forwardResolution ?
+ [] :
+ [parentPromiseValue]);
+ resolvePromise(chainPromise, true, value);
+ } catch (error) {
+ // if error occurs, should always return this error
+ resolvePromise(chainPromise, false, error);
+ }
+ }, chainPromise as TaskData);
+ }
+
+ const ZONE_AWARE_PROMISE_TO_STRING = 'function ZoneAwarePromise() { [native code] }';
+
+ class ZoneAwarePromise implements Promise {
+ static toString() { return ZONE_AWARE_PROMISE_TO_STRING; }
+
+ static resolve(value: R): Promise {
+ return resolvePromise(>new this(null as any), RESOLVED, value);
+ }
+
+ static reject(error: U): Promise {
+ return resolvePromise(>new this(null as any), REJECTED, error);
+ }
+
+ static race(values: PromiseLike[]): Promise {
+ let resolve: (v: any) => void;
+ let reject: (v: any) => void;
+ let promise: any = new this((res, rej) => {
+ resolve = res;
+ reject = rej;
+ });
+ function onResolve(value: any) { resolve(value); }
+ function onReject(error: any) { reject(error); }
+
+ for (let value of values) {
+ if (!isThenable(value)) {
+ value = this.resolve(value);
+ }
+ value.then(onResolve, onReject);
+ }
+ return promise;
+ }
+
+ static all(values: any): Promise {
+ let resolve: (v: any) => void;
+ let reject: (v: any) => void;
+ let promise = new this((res, rej) => {
+ resolve = res;
+ reject = rej;
+ });
+
+ // Start at 2 to prevent prematurely resolving if .then is called immediately.
+ let unresolvedCount = 2;
+ let valueIndex = 0;
+
+ const resolvedValues: any[] = [];
+ for (let value of values) {
+ if (!isThenable(value)) {
+ value = this.resolve(value);
+ }
+
+ const curValueIndex = valueIndex;
+ value.then((value: any) => {
+ resolvedValues[curValueIndex] = value;
+ unresolvedCount--;
+ if (unresolvedCount === 0) {
+ resolve !(resolvedValues);
+ }
+ }, reject !);
+
+ unresolvedCount++;
+ valueIndex++;
+ }
+
+ // Make the unresolvedCount zero-based again.
+ unresolvedCount -= 2;
+
+ if (unresolvedCount === 0) {
+ resolve !(resolvedValues);
+ }
+
+ return promise;
+ }
+
+ constructor(
+ executor:
+ (resolve: (value?: R|PromiseLike) => void, reject: (error?: any) => void) => void) {
+ const promise: ZoneAwarePromise = this;
+ if (!(promise instanceof ZoneAwarePromise)) {
+ throw new Error('Must be an instanceof Promise.');
+ }
+ (promise as any)[symbolState] = UNRESOLVED;
+ (promise as any)[symbolValue] = []; // queue;
+ try {
+ executor && executor(makeResolver(promise, RESOLVED), makeResolver(promise, REJECTED));
+ } catch (error) {
+ resolvePromise(promise, false, error);
+ }
+ }
+
+ get[Symbol.toStringTag]() { return 'Promise' as any; }
+
+ then(
+ onFulfilled?: ((value: R) => TResult1 | PromiseLike)|undefined|null,
+ onRejected?: ((reason: any) => TResult2 | PromiseLike)|undefined|
+ null): Promise {
+ const chainPromise: Promise =
+ new (this.constructor as typeof ZoneAwarePromise)(null as any);
+ const zone = Zone.current;
+ if ((this as any)[symbolState] == UNRESOLVED) {
+ ((this as any)[symbolValue]).push(zone, chainPromise, onFulfilled, onRejected);
+ } else {
+ scheduleResolveOrReject(this, zone, chainPromise as any, onFulfilled, onRejected);
+ }
+ return chainPromise;
+ }
+
+ catch(onRejected?: ((reason: any) => TResult | PromiseLike)|undefined|
+ null): Promise {
+ return this.then(null, onRejected);
+ }
+
+ finally(onFinally?: () => U | PromiseLike): Promise {
+ const chainPromise: Promise =
+ new (this.constructor as typeof ZoneAwarePromise)(null as any);
+ (chainPromise as any)[symbolFinally] = symbolFinally;
+ const zone = Zone.current;
+ if ((this as any)[symbolState] == UNRESOLVED) {
+ ((this as any)[symbolValue]).push(zone, chainPromise, onFinally, onFinally);
+ } else {
+ scheduleResolveOrReject(this, zone, chainPromise as any, onFinally, onFinally);
+ }
+ return chainPromise;
+ }
+ }
+ // Protect against aggressive optimizers dropping seemingly unused properties.
+ // E.g. Closure Compiler in advanced mode.
+ ZoneAwarePromise['resolve'] = ZoneAwarePromise.resolve;
+ ZoneAwarePromise['reject'] = ZoneAwarePromise.reject;
+ ZoneAwarePromise['race'] = ZoneAwarePromise.race;
+ ZoneAwarePromise['all'] = ZoneAwarePromise.all;
+
+ const NativePromise = global[symbolPromise] = global['Promise'];
+ const ZONE_AWARE_PROMISE = Zone.__symbol__('ZoneAwarePromise');
+
+ let desc = ObjectGetOwnPropertyDescriptor(global, 'Promise');
+ if (!desc || desc.configurable) {
+ desc && delete desc.writable;
+ desc && delete desc.value;
+ if (!desc) {
+ desc = {configurable: true, enumerable: true};
+ }
+ desc.get = function() {
+ // if we already set ZoneAwarePromise, use patched one
+ // otherwise return native one.
+ return global[ZONE_AWARE_PROMISE] ? global[ZONE_AWARE_PROMISE] : global[symbolPromise];
+ };
+ desc.set = function(NewNativePromise) {
+ if (NewNativePromise === ZoneAwarePromise) {
+ // if the NewNativePromise is ZoneAwarePromise
+ // save to global
+ global[ZONE_AWARE_PROMISE] = NewNativePromise;
+ } else {
+ // if the NewNativePromise is not ZoneAwarePromise
+ // for example: after load zone.js, some library just
+ // set es6-promise to global, if we set it to global
+ // directly, assertZonePatched will fail and angular
+ // will not loaded, so we just set the NewNativePromise
+ // to global[symbolPromise], so the result is just like
+ // we load ES6 Promise before zone.js
+ global[symbolPromise] = NewNativePromise;
+ if (!NewNativePromise.prototype[symbolThen]) {
+ patchThen(NewNativePromise);
+ }
+ api.setNativePromise(NewNativePromise);
+ }
+ };
+
+ ObjectDefineProperty(global, 'Promise', desc);
+ }
+
+ global['Promise'] = ZoneAwarePromise;
+
+ const symbolThenPatched = __symbol__('thenPatched');
+
+ function patchThen(Ctor: Function) {
+ const proto = Ctor.prototype;
+
+ const prop = ObjectGetOwnPropertyDescriptor(proto, 'then');
+ if (prop && (prop.writable === false || !prop.configurable)) {
+ // check Ctor.prototype.then propertyDescriptor is writable or not
+ // in meteor env, writable is false, we should ignore such case
+ return;
+ }
+
+ const originalThen = proto.then;
+ // Keep a reference to the original method.
+ proto[symbolThen] = originalThen;
+
+ Ctor.prototype.then = function(onResolve: any, onReject: any) {
+ const wrapped =
+ new ZoneAwarePromise((resolve, reject) => { originalThen.call(this, resolve, reject); });
+ return wrapped.then(onResolve, onReject);
+ };
+ (Ctor as any)[symbolThenPatched] = true;
+ }
+
+ api.patchThen = patchThen;
+
+ function zoneify(fn: Function) {
+ return function() {
+ let resultPromise = fn.apply(this, arguments);
+ if (resultPromise instanceof ZoneAwarePromise) {
+ return resultPromise;
+ }
+ let ctor = resultPromise.constructor;
+ if (!ctor[symbolThenPatched]) {
+ patchThen(ctor);
+ }
+ return resultPromise;
+ };
+ }
+
+ if (NativePromise) {
+ patchThen(NativePromise);
+ const fetch = global['fetch'];
+ if (typeof fetch == 'function') {
+ global[api.symbol('fetch')] = fetch;
+ global['fetch'] = zoneify(fetch);
+ }
+ }
+
+ // This is not part of public API, but it is useful for tests, so we expose it.
+ (Promise as any)[Zone.__symbol__('uncaughtPromiseErrors')] = _uncaughtPromiseErrors;
+ return ZoneAwarePromise;
+});
diff --git a/packages/zone.js/lib/common/timers.ts b/packages/zone.js/lib/common/timers.ts
new file mode 100644
index 0000000000..8b88403364
--- /dev/null
+++ b/packages/zone.js/lib/common/timers.ts
@@ -0,0 +1,133 @@
+/**
+ * @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
+ */
+/**
+ * @fileoverview
+ * @suppress {missingRequire}
+ */
+
+import {patchMethod, scheduleMacroTaskWithCurrentZone, zoneSymbol} from './utils';
+
+const taskSymbol = zoneSymbol('zoneTask');
+
+interface TimerOptions extends TaskData {
+ handleId?: number;
+ args: any[];
+}
+
+export function patchTimer(window: any, setName: string, cancelName: string, nameSuffix: string) {
+ let setNative: Function|null = null;
+ let clearNative: Function|null = null;
+ setName += nameSuffix;
+ cancelName += nameSuffix;
+
+ const tasksByHandleId: {[id: number]: Task} = {};
+
+ function scheduleTask(task: Task) {
+ const data = task.data;
+ function timer() {
+ try {
+ task.invoke.apply(this, arguments);
+ } finally {
+ // issue-934, task will be cancelled
+ // even it is a periodic task such as
+ // setInterval
+ if (!(task.data && task.data.isPeriodic)) {
+ if (typeof data.handleId === 'number') {
+ // in non-nodejs env, we remove timerId
+ // from local cache
+ delete tasksByHandleId[data.handleId];
+ } else if (data.handleId) {
+ // Node returns complex objects as handleIds
+ // we remove task reference from timer object
+ (data.handleId as any)[taskSymbol] = null;
+ }
+ }
+ }
+ }
+ data.args[0] = timer;
+ data.handleId = setNative !.apply(window, data.args);
+ return task;
+ }
+
+ function clearTask(task: Task) { return clearNative !((task.data).handleId); }
+
+ setNative =
+ patchMethod(window, setName, (delegate: Function) => function(self: any, args: any[]) {
+ if (typeof args[0] === 'function') {
+ const options: TimerOptions = {
+ isPeriodic: nameSuffix === 'Interval',
+ delay: (nameSuffix === 'Timeout' || nameSuffix === 'Interval') ? args[1] || 0 :
+ undefined,
+ args: args
+ };
+ const task =
+ scheduleMacroTaskWithCurrentZone(setName, args[0], options, scheduleTask, clearTask);
+ if (!task) {
+ return task;
+ }
+ // Node.js must additionally support the ref and unref functions.
+ const handle: any = (task.data).handleId;
+ if (typeof handle === 'number') {
+ // for non nodejs env, we save handleId: task
+ // mapping in local cache for clearTimeout
+ tasksByHandleId[handle] = task;
+ } else if (handle) {
+ // for nodejs env, we save task
+ // reference in timerId Object for clearTimeout
+ handle[taskSymbol] = task;
+ }
+
+ // check whether handle is null, because some polyfill or browser
+ // may return undefined from setTimeout/setInterval/setImmediate/requestAnimationFrame
+ if (handle && handle.ref && handle.unref && typeof handle.ref === 'function' &&
+ typeof handle.unref === 'function') {
+ (task).ref = (handle).ref.bind(handle);
+ (task).unref = (handle).unref.bind(handle);
+ }
+ if (typeof handle === 'number' || handle) {
+ return handle;
+ }
+ return task;
+ } else {
+ // cause an error by calling it directly.
+ return delegate.apply(window, args);
+ }
+ });
+
+ clearNative =
+ patchMethod(window, cancelName, (delegate: Function) => function(self: any, args: any[]) {
+ const id = args[0];
+ let task: Task;
+ if (typeof id === 'number') {
+ // non nodejs env.
+ task = tasksByHandleId[id];
+ } else {
+ // nodejs env.
+ task = id && id[taskSymbol];
+ // other environments.
+ if (!task) {
+ task = id;
+ }
+ }
+ if (task && typeof task.type === 'string') {
+ if (task.state !== 'notScheduled' &&
+ (task.cancelFn && task.data !.isPeriodic || task.runCount === 0)) {
+ if (typeof id === 'number') {
+ delete tasksByHandleId[id];
+ } else if (id) {
+ id[taskSymbol] = null;
+ }
+ // Do not cancel already canceled functions
+ task.zone.cancelTask(task);
+ }
+ } else {
+ // cause an error by calling it directly.
+ delegate.apply(window, args);
+ }
+ });
+}
diff --git a/packages/zone.js/lib/common/to-string.ts b/packages/zone.js/lib/common/to-string.ts
new file mode 100644
index 0000000000..13c95cf9db
--- /dev/null
+++ b/packages/zone.js/lib/common/to-string.ts
@@ -0,0 +1,57 @@
+/**
+ * @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
+ */
+import {zoneSymbol} from './utils';
+
+// override Function.prototype.toString to make zone.js patched function
+// look like native function
+Zone.__load_patch('toString', (global: any) => {
+ // patch Func.prototype.toString to let them look like native
+ const originalFunctionToString = Function.prototype.toString;
+
+ const ORIGINAL_DELEGATE_SYMBOL = zoneSymbol('OriginalDelegate');
+ const PROMISE_SYMBOL = zoneSymbol('Promise');
+ const ERROR_SYMBOL = zoneSymbol('Error');
+ const newFunctionToString = function toString() {
+ if (typeof this === 'function') {
+ const originalDelegate = this[ORIGINAL_DELEGATE_SYMBOL];
+ if (originalDelegate) {
+ if (typeof originalDelegate === 'function') {
+ return originalFunctionToString.call(originalDelegate);
+ } else {
+ return Object.prototype.toString.call(originalDelegate);
+ }
+ }
+ if (this === Promise) {
+ const nativePromise = global[PROMISE_SYMBOL];
+ if (nativePromise) {
+ return originalFunctionToString.call(nativePromise);
+ }
+ }
+ if (this === Error) {
+ const nativeError = global[ERROR_SYMBOL];
+ if (nativeError) {
+ return originalFunctionToString.call(nativeError);
+ }
+ }
+ }
+ return originalFunctionToString.call(this);
+ };
+ (newFunctionToString as any)[ORIGINAL_DELEGATE_SYMBOL] = originalFunctionToString;
+ Function.prototype.toString = newFunctionToString;
+
+
+ // patch Object.prototype.toString to let them look like native
+ const originalObjectToString = Object.prototype.toString;
+ const PROMISE_OBJECT_TO_STRING = '[object Promise]';
+ Object.prototype.toString = function() {
+ if (this instanceof Promise) {
+ return PROMISE_OBJECT_TO_STRING;
+ }
+ return originalObjectToString.call(this);
+ };
+});
diff --git a/packages/zone.js/lib/common/utils.ts b/packages/zone.js/lib/common/utils.ts
new file mode 100644
index 0000000000..c21dbc4e02
--- /dev/null
+++ b/packages/zone.js/lib/common/utils.ts
@@ -0,0 +1,509 @@
+/**
+ * @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
+ */
+/**
+ * Suppress closure compiler errors about unknown 'Zone' variable
+ * @fileoverview
+ * @suppress {undefinedVars,globalThis,missingRequire}
+ */
+
+///
+
+// issue #989, to reduce bundle size, use short name
+/** Object.getOwnPropertyDescriptor */
+export const ObjectGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
+/** Object.defineProperty */
+export const ObjectDefineProperty = Object.defineProperty;
+/** Object.getPrototypeOf */
+export const ObjectGetPrototypeOf = Object.getPrototypeOf;
+/** Object.create */
+export const ObjectCreate = Object.create;
+/** Array.prototype.slice */
+export const ArraySlice = Array.prototype.slice;
+/** addEventListener string const */
+export const ADD_EVENT_LISTENER_STR = 'addEventListener';
+/** removeEventListener string const */
+export const REMOVE_EVENT_LISTENER_STR = 'removeEventListener';
+/** zoneSymbol addEventListener */
+export const ZONE_SYMBOL_ADD_EVENT_LISTENER = Zone.__symbol__(ADD_EVENT_LISTENER_STR);
+/** zoneSymbol removeEventListener */
+export const ZONE_SYMBOL_REMOVE_EVENT_LISTENER = Zone.__symbol__(REMOVE_EVENT_LISTENER_STR);
+/** true string const */
+export const TRUE_STR = 'true';
+/** false string const */
+export const FALSE_STR = 'false';
+/** Zone symbol prefix string const. */
+export const ZONE_SYMBOL_PREFIX = Zone.__symbol__('');
+
+export function wrapWithCurrentZone(callback: T, source: string): T {
+ return Zone.current.wrap(callback, source);
+}
+
+export function scheduleMacroTaskWithCurrentZone(
+ source: string, callback: Function, data?: TaskData, customSchedule?: (task: Task) => void,
+ customCancel?: (task: Task) => void): MacroTask {
+ return Zone.current.scheduleMacroTask(source, callback, data, customSchedule, customCancel);
+}
+
+// Hack since TypeScript isn't compiling this for a worker.
+declare const WorkerGlobalScope: any;
+
+export const zoneSymbol = Zone.__symbol__;
+const isWindowExists = typeof window !== 'undefined';
+const internalWindow: any = isWindowExists ? window : undefined;
+const _global: any = isWindowExists && internalWindow || typeof self === 'object' && self || global;
+
+const REMOVE_ATTRIBUTE = 'removeAttribute';
+const NULL_ON_PROP_VALUE: [any] = [null];
+
+export function bindArguments(args: any[], source: string): any[] {
+ for (let i = args.length - 1; i >= 0; i--) {
+ if (typeof args[i] === 'function') {
+ args[i] = wrapWithCurrentZone(args[i], source + '_' + i);
+ }
+ }
+ return args;
+}
+
+export function patchPrototype(prototype: any, fnNames: string[]) {
+ const source = prototype.constructor['name'];
+ for (let i = 0; i < fnNames.length; i++) {
+ const name = fnNames[i];
+ const delegate = prototype[name];
+ if (delegate) {
+ const prototypeDesc = ObjectGetOwnPropertyDescriptor(prototype, name);
+ if (!isPropertyWritable(prototypeDesc)) {
+ continue;
+ }
+ prototype[name] = ((delegate: Function) => {
+ const patched: any = function() {
+ return delegate.apply(this, bindArguments(arguments, source + '.' + name));
+ };
+ attachOriginToPatched(patched, delegate);
+ return patched;
+ })(delegate);
+ }
+ }
+}
+
+export function isPropertyWritable(propertyDesc: any) {
+ if (!propertyDesc) {
+ return true;
+ }
+
+ if (propertyDesc.writable === false) {
+ return false;
+ }
+
+ return !(typeof propertyDesc.get === 'function' && typeof propertyDesc.set === 'undefined');
+}
+
+export const isWebWorker: boolean =
+ (typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope);
+
+// Make sure to access `process` through `_global` so that WebPack does not accidentally browserify
+// this code.
+export const isNode: boolean =
+ (!('nw' in _global) && typeof _global.process !== 'undefined' &&
+ {}.toString.call(_global.process) === '[object process]');
+
+export const isBrowser: boolean =
+ !isNode && !isWebWorker && !!(isWindowExists && internalWindow['HTMLElement']);
+
+// we are in electron of nw, so we are both browser and nodejs
+// Make sure to access `process` through `_global` so that WebPack does not accidentally browserify
+// this code.
+export const isMix: boolean = typeof _global.process !== 'undefined' &&
+ {}.toString.call(_global.process) === '[object process]' && !isWebWorker &&
+ !!(isWindowExists && internalWindow['HTMLElement']);
+
+const zoneSymbolEventNames: {[eventName: string]: string} = {};
+
+const wrapFn = function(event: Event) {
+ // https://github.com/angular/zone.js/issues/911, in IE, sometimes
+ // event will be undefined, so we need to use window.event
+ event = event || _global.event;
+ if (!event) {
+ return;
+ }
+ let eventNameSymbol = zoneSymbolEventNames[event.type];
+ if (!eventNameSymbol) {
+ eventNameSymbol = zoneSymbolEventNames[event.type] = zoneSymbol('ON_PROPERTY' + event.type);
+ }
+ const target = this || event.target || _global;
+ const listener = target[eventNameSymbol];
+ let result;
+ if (isBrowser && target === internalWindow && event.type === 'error') {
+ // window.onerror have different signiture
+ // https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onerror#window.onerror
+ // and onerror callback will prevent default when callback return true
+ const errorEvent: ErrorEvent = event as any;
+ result = listener &&
+ listener.call(
+ this, errorEvent.message, errorEvent.filename, errorEvent.lineno, errorEvent.colno,
+ errorEvent.error);
+ if (result === true) {
+ event.preventDefault();
+ }
+ } else {
+ result = listener && listener.apply(this, arguments);
+ if (result != undefined && !result) {
+ event.preventDefault();
+ }
+ }
+
+ return result;
+};
+
+export function patchProperty(obj: any, prop: string, prototype?: any) {
+ let desc = ObjectGetOwnPropertyDescriptor(obj, prop);
+ if (!desc && prototype) {
+ // when patch window object, use prototype to check prop exist or not
+ const prototypeDesc = ObjectGetOwnPropertyDescriptor(prototype, prop);
+ if (prototypeDesc) {
+ desc = {enumerable: true, configurable: true};
+ }
+ }
+ // if the descriptor not exists or is not configurable
+ // just return
+ if (!desc || !desc.configurable) {
+ return;
+ }
+
+ const onPropPatchedSymbol = zoneSymbol('on' + prop + 'patched');
+ if (obj.hasOwnProperty(onPropPatchedSymbol) && obj[onPropPatchedSymbol]) {
+ return;
+ }
+
+ // A property descriptor cannot have getter/setter and be writable
+ // deleting the writable and value properties avoids this error:
+ //
+ // TypeError: property descriptors must not specify a value or be writable when a
+ // getter or setter has been specified
+ delete desc.writable;
+ delete desc.value;
+ const originalDescGet = desc.get;
+ const originalDescSet = desc.set;
+
+ // substr(2) cuz 'onclick' -> 'click', etc
+ const eventName = prop.substr(2);
+
+ let eventNameSymbol = zoneSymbolEventNames[eventName];
+ if (!eventNameSymbol) {
+ eventNameSymbol = zoneSymbolEventNames[eventName] = zoneSymbol('ON_PROPERTY' + eventName);
+ }
+
+ desc.set = function(newValue) {
+ // in some of windows's onproperty callback, this is undefined
+ // so we need to check it
+ let target = this;
+ if (!target && obj === _global) {
+ target = _global;
+ }
+ if (!target) {
+ return;
+ }
+ let previousValue = target[eventNameSymbol];
+ if (previousValue) {
+ target.removeEventListener(eventName, wrapFn);
+ }
+
+ // issue #978, when onload handler was added before loading zone.js
+ // we should remove it with originalDescSet
+ if (originalDescSet) {
+ originalDescSet.apply(target, NULL_ON_PROP_VALUE);
+ }
+
+ if (typeof newValue === 'function') {
+ target[eventNameSymbol] = newValue;
+ target.addEventListener(eventName, wrapFn, false);
+ } else {
+ target[eventNameSymbol] = null;
+ }
+ };
+
+ // The getter would return undefined for unassigned properties but the default value of an
+ // unassigned property is null
+ desc.get = function() {
+ // in some of windows's onproperty callback, this is undefined
+ // so we need to check it
+ let target = this;
+ if (!target && obj === _global) {
+ target = _global;
+ }
+ if (!target) {
+ return null;
+ }
+ const listener = target[eventNameSymbol];
+ if (listener) {
+ return listener;
+ } else if (originalDescGet) {
+ // result will be null when use inline event attribute,
+ // such as OK
+ // because the onclick function is internal raw uncompiled handler
+ // the onclick will be evaluated when first time event was triggered or
+ // the property is accessed, https://github.com/angular/zone.js/issues/525
+ // so we should use original native get to retrieve the handler
+ let value = originalDescGet && originalDescGet.call(this);
+ if (value) {
+ desc !.set !.call(this, value);
+ if (typeof target[REMOVE_ATTRIBUTE] === 'function') {
+ target.removeAttribute(prop);
+ }
+ return value;
+ }
+ }
+ return null;
+ };
+
+ ObjectDefineProperty(obj, prop, desc);
+
+ obj[onPropPatchedSymbol] = true;
+}
+
+export function patchOnProperties(obj: any, properties: string[] | null, prototype?: any) {
+ if (properties) {
+ for (let i = 0; i < properties.length; i++) {
+ patchProperty(obj, 'on' + properties[i], prototype);
+ }
+ } else {
+ const onProperties = [];
+ for (const prop in obj) {
+ if (prop.substr(0, 2) == 'on') {
+ onProperties.push(prop);
+ }
+ }
+ for (let j = 0; j < onProperties.length; j++) {
+ patchProperty(obj, onProperties[j], prototype);
+ }
+ }
+}
+
+const originalInstanceKey = zoneSymbol('originalInstance');
+
+// wrap some native API on `window`
+export function patchClass(className: string) {
+ const OriginalClass = _global[className];
+ if (!OriginalClass) return;
+ // keep original class in global
+ _global[zoneSymbol(className)] = OriginalClass;
+
+ _global[className] = function() {
+ const a = bindArguments(arguments, className);
+ switch (a.length) {
+ case 0:
+ this[originalInstanceKey] = new OriginalClass();
+ break;
+ case 1:
+ this[originalInstanceKey] = new OriginalClass(a[0]);
+ break;
+ case 2:
+ this[originalInstanceKey] = new OriginalClass(a[0], a[1]);
+ break;
+ case 3:
+ this[originalInstanceKey] = new OriginalClass(a[0], a[1], a[2]);
+ break;
+ case 4:
+ this[originalInstanceKey] = new OriginalClass(a[0], a[1], a[2], a[3]);
+ break;
+ default:
+ throw new Error('Arg list too long.');
+ }
+ };
+
+ // attach original delegate to patched function
+ attachOriginToPatched(_global[className], OriginalClass);
+
+ const instance = new OriginalClass(function() {});
+
+ let prop;
+ for (prop in instance) {
+ // https://bugs.webkit.org/show_bug.cgi?id=44721
+ if (className === 'XMLHttpRequest' && prop === 'responseBlob') continue;
+ (function(prop) {
+ if (typeof instance[prop] === 'function') {
+ _global[className].prototype[prop] = function() {
+ return this[originalInstanceKey][prop].apply(this[originalInstanceKey], arguments);
+ };
+ } else {
+ ObjectDefineProperty(_global[className].prototype, prop, {
+ set: function(fn) {
+ if (typeof fn === 'function') {
+ this[originalInstanceKey][prop] = wrapWithCurrentZone(fn, className + '.' + prop);
+ // keep callback in wrapped function so we can
+ // use it in Function.prototype.toString to return
+ // the native one.
+ attachOriginToPatched(this[originalInstanceKey][prop], fn);
+ } else {
+ this[originalInstanceKey][prop] = fn;
+ }
+ },
+ get: function() { return this[originalInstanceKey][prop]; }
+ });
+ }
+ }(prop));
+ }
+
+ for (prop in OriginalClass) {
+ if (prop !== 'prototype' && OriginalClass.hasOwnProperty(prop)) {
+ _global[className][prop] = OriginalClass[prop];
+ }
+ }
+}
+
+export function copySymbolProperties(src: any, dest: any) {
+ if (typeof(Object as any).getOwnPropertySymbols !== 'function') {
+ return;
+ }
+ const symbols: any = (Object as any).getOwnPropertySymbols(src);
+ symbols.forEach((symbol: any) => {
+ const desc = Object.getOwnPropertyDescriptor(src, symbol);
+ Object.defineProperty(dest, symbol, {
+ get: function() { return src[symbol]; },
+ set: function(value: any) {
+ if (desc && (!desc.writable || typeof desc.set !== 'function')) {
+ // if src[symbol] is not writable or not have a setter, just return
+ return;
+ }
+ src[symbol] = value;
+ },
+ enumerable: desc ? desc.enumerable : true,
+ configurable: desc ? desc.configurable : true
+ });
+ });
+}
+
+let shouldCopySymbolProperties = false;
+
+export function setShouldCopySymbolProperties(flag: boolean) {
+ shouldCopySymbolProperties = flag;
+}
+
+export function patchMethod(
+ target: any, name: string, patchFn: (delegate: Function, delegateName: string, name: string) =>
+ (self: any, args: any[]) => any): Function|null {
+ let proto = target;
+ while (proto && !proto.hasOwnProperty(name)) {
+ proto = ObjectGetPrototypeOf(proto);
+ }
+ if (!proto && target[name]) {
+ // somehow we did not find it, but we can see it. This happens on IE for Window properties.
+ proto = target;
+ }
+
+ const delegateName = zoneSymbol(name);
+ let delegate: Function|null = null;
+ if (proto && !(delegate = proto[delegateName])) {
+ delegate = proto[delegateName] = proto[name];
+ // check whether proto[name] is writable
+ // some property is readonly in safari, such as HtmlCanvasElement.prototype.toBlob
+ const desc = proto && ObjectGetOwnPropertyDescriptor(proto, name);
+ if (isPropertyWritable(desc)) {
+ const patchDelegate = patchFn(delegate !, delegateName, name);
+ proto[name] = function() { return patchDelegate(this, arguments as any); };
+ attachOriginToPatched(proto[name], delegate);
+ if (shouldCopySymbolProperties) {
+ copySymbolProperties(delegate, proto[name]);
+ }
+ }
+ }
+ return delegate;
+}
+
+export interface MacroTaskMeta extends TaskData {
+ name: string;
+ target: any;
+ cbIdx: number;
+ args: any[];
+}
+
+// TODO: @JiaLiPassion, support cancel task later if necessary
+export function patchMacroTask(
+ obj: any, funcName: string, metaCreator: (self: any, args: any[]) => MacroTaskMeta) {
+ let setNative: Function|null = null;
+
+ function scheduleTask(task: Task) {
+ const data = task.data;
+ data.args[data.cbIdx] = function() { task.invoke.apply(this, arguments); };
+ setNative !.apply(data.target, data.args);
+ return task;
+ }
+
+ setNative = patchMethod(obj, funcName, (delegate: Function) => function(self: any, args: any[]) {
+ const meta = metaCreator(self, args);
+ if (meta.cbIdx >= 0 && typeof args[meta.cbIdx] === 'function') {
+ return scheduleMacroTaskWithCurrentZone(meta.name, args[meta.cbIdx], meta, scheduleTask);
+ } else {
+ // cause an error by calling it directly.
+ return delegate.apply(self, args);
+ }
+ });
+}
+
+export interface MicroTaskMeta extends TaskData {
+ name: string;
+ target: any;
+ cbIdx: number;
+ args: any[];
+}
+
+export function patchMicroTask(
+ obj: any, funcName: string, metaCreator: (self: any, args: any[]) => MicroTaskMeta) {
+ let setNative: Function|null = null;
+
+ function scheduleTask(task: Task) {
+ const data = task.data;
+ data.args[data.cbIdx] = function() { task.invoke.apply(this, arguments); };
+ setNative !.apply(data.target, data.args);
+ return task;
+ }
+
+ setNative = patchMethod(obj, funcName, (delegate: Function) => function(self: any, args: any[]) {
+ const meta = metaCreator(self, args);
+ if (meta.cbIdx >= 0 && typeof args[meta.cbIdx] === 'function') {
+ return Zone.current.scheduleMicroTask(meta.name, args[meta.cbIdx], meta, scheduleTask);
+ } else {
+ // cause an error by calling it directly.
+ return delegate.apply(self, args);
+ }
+ });
+}
+
+export function attachOriginToPatched(patched: Function, original: any) {
+ (patched as any)[zoneSymbol('OriginalDelegate')] = original;
+}
+
+let isDetectedIEOrEdge = false;
+let ieOrEdge = false;
+
+export function isIE() {
+ try {
+ const ua = internalWindow.navigator.userAgent;
+ if (ua.indexOf('MSIE ') !== -1 || ua.indexOf('Trident/') !== -1) {
+ return true;
+ }
+ } catch (error) {
+ }
+ return false;
+}
+
+export function isIEOrEdge() {
+ if (isDetectedIEOrEdge) {
+ return ieOrEdge;
+ }
+
+ isDetectedIEOrEdge = true;
+
+ try {
+ const ua = internalWindow.navigator.userAgent;
+ if (ua.indexOf('MSIE ') !== -1 || ua.indexOf('Trident/') !== -1 || ua.indexOf('Edge/') !== -1) {
+ ieOrEdge = true;
+ }
+ } catch (error) {
+ }
+ return ieOrEdge;
+}
diff --git a/packages/zone.js/lib/extra/bluebird.ts b/packages/zone.js/lib/extra/bluebird.ts
new file mode 100644
index 0000000000..92d74f920a
--- /dev/null
+++ b/packages/zone.js/lib/extra/bluebird.ts
@@ -0,0 +1,55 @@
+/**
+ * @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
+ */
+Zone.__load_patch('bluebird', (global: any, Zone: ZoneType, api: _ZonePrivate) => {
+ // TODO: @JiaLiPassion, we can automatically patch bluebird
+ // if global.Promise = Bluebird, but sometimes in nodejs,
+ // global.Promise is not Bluebird, and Bluebird is just be
+ // used by other libraries such as sequelize, so I think it is
+ // safe to just expose a method to patch Bluebird explicitly
+ const BLUEBIRD = 'bluebird';
+ (Zone as any)[Zone.__symbol__(BLUEBIRD)] = function patchBluebird(Bluebird: any) {
+ // patch method of Bluebird.prototype which not using `then` internally
+ const bluebirdApis: string[] = ['then', 'spread', 'finally'];
+ bluebirdApis.forEach(bapi => {
+ api.patchMethod(
+ Bluebird.prototype, bapi, (delegate: Function) => (self: any, args: any[]) => {
+ const zone = Zone.current;
+ for (let i = 0; i < args.length; i++) {
+ const func = args[i];
+ if (typeof func === 'function') {
+ args[i] = function() {
+ const argSelf: any = this;
+ const argArgs: any = arguments;
+ return new Bluebird((res: any, rej: any) => {
+ zone.scheduleMicroTask('Promise.then', () => {
+ try {
+ res(func.apply(argSelf, argArgs));
+ } catch (error) {
+ rej(error);
+ }
+ });
+ });
+ };
+ }
+ }
+ return delegate.apply(self, args);
+ });
+ });
+
+ Bluebird.onPossiblyUnhandledRejection(function(e: any, promise: any) {
+ try {
+ Zone.current.runGuarded(() => { throw e; });
+ } catch (err) {
+ api.onUnhandledError(err);
+ }
+ });
+
+ // override global promise
+ global[api.symbol('ZoneAwarePromise')] = Bluebird;
+ };
+});
diff --git a/packages/zone.js/lib/extra/cordova.ts b/packages/zone.js/lib/extra/cordova.ts
new file mode 100644
index 0000000000..c9bb8276d0
--- /dev/null
+++ b/packages/zone.js/lib/extra/cordova.ts
@@ -0,0 +1,39 @@
+/**
+ * @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
+ */
+Zone.__load_patch('cordova', (global: any, Zone: ZoneType, api: _ZonePrivate) => {
+ if (global.cordova) {
+ const SUCCESS_SOURCE = 'cordova.exec.success';
+ const ERROR_SOURCE = 'cordova.exec.error';
+ const FUNCTION = 'function';
+ const nativeExec: Function|null =
+ api.patchMethod(global.cordova, 'exec', () => function(self: any, args: any[]) {
+ if (args.length > 0 && typeof args[0] === FUNCTION) {
+ args[0] = Zone.current.wrap(args[0], SUCCESS_SOURCE);
+ }
+ if (args.length > 1 && typeof args[1] === FUNCTION) {
+ args[1] = Zone.current.wrap(args[1], ERROR_SOURCE);
+ }
+ return nativeExec !.apply(self, args);
+ });
+ }
+});
+
+Zone.__load_patch('cordova.FileReader', (global: any, Zone: ZoneType) => {
+ if (global.cordova && typeof global['FileReader'] !== 'undefined') {
+ document.addEventListener('deviceReady', () => {
+ const FileReader = global['FileReader'];
+ ['abort', 'error', 'load', 'loadstart', 'loadend', 'progress'].forEach(prop => {
+ const eventNameSymbol = Zone.__symbol__('ON_PROPERTY' + prop);
+ Object.defineProperty(FileReader.prototype, eventNameSymbol, {
+ configurable: true,
+ get: function() { return this._realReader && this._realReader[eventNameSymbol]; }
+ });
+ });
+ });
+ }
+});
diff --git a/packages/zone.js/lib/extra/electron.ts b/packages/zone.js/lib/extra/electron.ts
new file mode 100644
index 0000000000..73693eaab7
--- /dev/null
+++ b/packages/zone.js/lib/extra/electron.ts
@@ -0,0 +1,31 @@
+/**
+ * @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
+ */
+Zone.__load_patch('electron', (global: any, Zone: ZoneType, api: _ZonePrivate) => {
+ function patchArguments(target: any, name: string, source: string): Function|null {
+ return api.patchMethod(target, name, (delegate: Function) => (self: any, args: any[]) => {
+ return delegate && delegate.apply(self, api.bindArguments(args, source));
+ });
+ }
+ const {desktopCapturer, shell, CallbacksRegistry} = require('electron');
+ // patch api in renderer process directly
+ // desktopCapturer
+ if (desktopCapturer) {
+ patchArguments(desktopCapturer, 'getSources', 'electron.desktopCapturer.getSources');
+ }
+ // shell
+ if (shell) {
+ patchArguments(shell, 'openExternal', 'electron.shell.openExternal');
+ }
+
+ // patch api in main process through CallbackRegistry
+ if (!CallbacksRegistry) {
+ return;
+ }
+
+ patchArguments(CallbacksRegistry.prototype, 'add', 'CallbackRegistry.add');
+});
diff --git a/packages/zone.js/lib/extra/jsonp.ts b/packages/zone.js/lib/extra/jsonp.ts
new file mode 100644
index 0000000000..814e5296ae
--- /dev/null
+++ b/packages/zone.js/lib/extra/jsonp.ts
@@ -0,0 +1,77 @@
+/**
+ * @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
+ */
+Zone.__load_patch('jsonp', (global: any, Zone: ZoneType, api: _ZonePrivate) => {
+ const noop = function() {};
+ // because jsonp is not a standard api, there are a lot of
+ // implementations, so zone.js just provide a helper util to
+ // patch the jsonp send and onSuccess/onError callback
+ // the options is an object which contains
+ // - jsonp, the jsonp object which hold the send function
+ // - sendFuncName, the name of the send function
+ // - successFuncName, success func name
+ // - failedFuncName, failed func name
+ (Zone as any)[Zone.__symbol__('jsonp')] = function patchJsonp(options: any) {
+ if (!options || !options.jsonp || !options.sendFuncName) {
+ return;
+ }
+ const noop = function() {};
+
+ [options.successFuncName, options.failedFuncName].forEach(methodName => {
+ if (!methodName) {
+ return;
+ }
+
+ const oriFunc = global[methodName];
+ if (oriFunc) {
+ api.patchMethod(global, methodName, (delegate: Function) => (self: any, args: any[]) => {
+ const task = global[api.symbol('jsonTask')];
+ if (task) {
+ task.callback = delegate;
+ return task.invoke.apply(self, args);
+ } else {
+ return delegate.apply(self, args);
+ }
+ });
+ } else {
+ Object.defineProperty(global, methodName, {
+ configurable: true,
+ enumerable: true,
+ get: function() {
+ return function() {
+ const task = global[api.symbol('jsonpTask')];
+ const target = this ? this : global;
+ const delegate = global[api.symbol(`jsonp${methodName}callback`)];
+
+ if (task) {
+ if (delegate) {
+ task.callback = delegate;
+ }
+ global[api.symbol('jsonpTask')] = undefined;
+ return task.invoke.apply(this, arguments);
+ } else {
+ if (delegate) {
+ return delegate.apply(this, arguments);
+ }
+ }
+ return null;
+ };
+ },
+ set: function(callback: Function) {
+ this[api.symbol(`jsonp${methodName}callback`)] = callback;
+ }
+ });
+ }
+ });
+
+ api.patchMethod(
+ options.jsonp, options.sendFuncName, (delegate: Function) => (self: any, args: any[]) => {
+ global[api.symbol('jsonpTask')] = Zone.current.scheduleMacroTask(
+ 'jsonp', noop, {}, (task: Task) => { return delegate.apply(self, args); }, noop);
+ });
+ };
+});
diff --git a/packages/zone.js/lib/extra/socket-io.ts b/packages/zone.js/lib/extra/socket-io.ts
new file mode 100644
index 0000000000..ee7b89a86b
--- /dev/null
+++ b/packages/zone.js/lib/extra/socket-io.ts
@@ -0,0 +1,22 @@
+/**
+ * @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
+ */
+Zone.__load_patch('socketio', (global: any, Zone: ZoneType, api: _ZonePrivate) => {
+ (Zone as any)[Zone.__symbol__('socketio')] = function patchSocketIO(io: any) {
+ // patch io.Socket.prototype event listener related method
+ api.patchEventTarget(global, [io.Socket.prototype], {
+ useG: false,
+ chkDup: false,
+ rt: true,
+ diff: (task: any, delegate: any) => { return task.callback === delegate; }
+ });
+ // also patch io.Socket.prototype.on/off/removeListener/removeAllListeners
+ io.Socket.prototype.on = io.Socket.prototype.addEventListener;
+ io.Socket.prototype.off = io.Socket.prototype.removeListener =
+ io.Socket.prototype.removeAllListeners = io.Socket.prototype.removeEventListener;
+ };
+});
diff --git a/packages/zone.js/lib/jasmine/jasmine.ts b/packages/zone.js/lib/jasmine/jasmine.ts
new file mode 100644
index 0000000000..2b6ed863f0
--- /dev/null
+++ b/packages/zone.js/lib/jasmine/jasmine.ts
@@ -0,0 +1,302 @@
+/**
+ * @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
+ */
+
+///
+
+'use strict';
+((_global: any) => {
+ const __extends = function(d: any, b: any) {
+ for (const p in b)
+ if (b.hasOwnProperty(p)) d[p] = b[p];
+ function __() { this.constructor = d; }
+ d.prototype = b === null ? Object.create(b) : ((__.prototype = b.prototype), new (__ as any)());
+ };
+ // Patch jasmine's describe/it/beforeEach/afterEach functions so test code always runs
+ // in a testZone (ProxyZone). (See: angular/zone.js#91 & angular/angular#10503)
+ if (!Zone) throw new Error('Missing: zone.js');
+ if (typeof jasmine == 'undefined') throw new Error('Missing: jasmine.js');
+ if ((jasmine as any)['__zone_patch__'])
+ throw new Error(`'jasmine' has already been patched with 'Zone'.`);
+ (jasmine as any)['__zone_patch__'] = true;
+
+ const SyncTestZoneSpec: {new (name: string): ZoneSpec} = (Zone as any)['SyncTestZoneSpec'];
+ const ProxyZoneSpec: {new (): ZoneSpec} = (Zone as any)['ProxyZoneSpec'];
+ if (!SyncTestZoneSpec) throw new Error('Missing: SyncTestZoneSpec');
+ if (!ProxyZoneSpec) throw new Error('Missing: ProxyZoneSpec');
+
+ const ambientZone = Zone.current;
+ // Create a synchronous-only zone in which to run `describe` blocks in order to raise an
+ // error if any asynchronous operations are attempted inside of a `describe` but outside of
+ // a `beforeEach` or `it`.
+ const syncZone = ambientZone.fork(new SyncTestZoneSpec('jasmine.describe'));
+
+ const symbol = Zone.__symbol__;
+
+ // whether patch jasmine clock when in fakeAsync
+ const disablePatchingJasmineClock = _global[symbol('fakeAsyncDisablePatchingClock')] === true;
+ // the original variable name fakeAsyncPatchLock is not accurate, so the name will be
+ // fakeAsyncAutoFakeAsyncWhenClockPatched and if this enablePatchingJasmineClock is false, we also
+ // automatically disable the auto jump into fakeAsync feature
+ const enableAutoFakeAsyncWhenClockPatched = !disablePatchingJasmineClock &&
+ ((_global[symbol('fakeAsyncPatchLock')] === true) ||
+ (_global[symbol('fakeAsyncAutoFakeAsyncWhenClockPatched')] === true));
+
+ const ignoreUnhandledRejection = _global[symbol('ignoreUnhandledRejection')] === true;
+
+ if (!ignoreUnhandledRejection) {
+ const globalErrors = (jasmine as any).GlobalErrors;
+ if (globalErrors && !(jasmine as any)[symbol('GlobalErrors')]) {
+ (jasmine as any)[symbol('GlobalErrors')] = globalErrors;
+ (jasmine as any).GlobalErrors = function() {
+ const instance = new globalErrors();
+ const originalInstall = instance.install;
+ if (originalInstall && !instance[symbol('install')]) {
+ instance[symbol('install')] = originalInstall;
+ instance.install = function() {
+ const originalHandlers = process.listeners('unhandledRejection');
+ const r = originalInstall.apply(this, arguments);
+ process.removeAllListeners('unhandledRejection');
+ if (originalHandlers) {
+ originalHandlers.forEach(h => process.on('unhandledRejection', h));
+ }
+ return r;
+ };
+ }
+ return instance;
+ };
+ }
+ }
+
+ // Monkey patch all of the jasmine DSL so that each function runs in appropriate zone.
+ const jasmineEnv: any = jasmine.getEnv();
+ ['describe', 'xdescribe', 'fdescribe'].forEach(methodName => {
+ let originalJasmineFn: Function = jasmineEnv[methodName];
+ jasmineEnv[methodName] = function(description: string, specDefinitions: Function) {
+ return originalJasmineFn.call(this, description, wrapDescribeInZone(specDefinitions));
+ };
+ });
+ ['it', 'xit', 'fit'].forEach(methodName => {
+ let originalJasmineFn: Function = jasmineEnv[methodName];
+ jasmineEnv[symbol(methodName)] = originalJasmineFn;
+ jasmineEnv[methodName] = function(
+ description: string, specDefinitions: Function, timeout: number) {
+ arguments[1] = wrapTestInZone(specDefinitions);
+ return originalJasmineFn.apply(this, arguments);
+ };
+ });
+ ['beforeEach', 'afterEach', 'beforeAll', 'afterAll'].forEach(methodName => {
+ let originalJasmineFn: Function = jasmineEnv[methodName];
+ jasmineEnv[symbol(methodName)] = originalJasmineFn;
+ jasmineEnv[methodName] = function(specDefinitions: Function, timeout: number) {
+ arguments[0] = wrapTestInZone(specDefinitions);
+ return originalJasmineFn.apply(this, arguments);
+ };
+ });
+
+ if (!disablePatchingJasmineClock) {
+ // need to patch jasmine.clock().mockDate and jasmine.clock().tick() so
+ // they can work properly in FakeAsyncTest
+ const originalClockFn: Function = ((jasmine as any)[symbol('clock')] = jasmine['clock']);
+ (jasmine as any)['clock'] = function() {
+ const clock = originalClockFn.apply(this, arguments);
+ if (!clock[symbol('patched')]) {
+ clock[symbol('patched')] = symbol('patched');
+ const originalTick = (clock[symbol('tick')] = clock.tick);
+ clock.tick = function() {
+ const fakeAsyncZoneSpec = Zone.current.get('FakeAsyncTestZoneSpec');
+ if (fakeAsyncZoneSpec) {
+ return fakeAsyncZoneSpec.tick.apply(fakeAsyncZoneSpec, arguments);
+ }
+ return originalTick.apply(this, arguments);
+ };
+ const originalMockDate = (clock[symbol('mockDate')] = clock.mockDate);
+ clock.mockDate = function() {
+ const fakeAsyncZoneSpec = Zone.current.get('FakeAsyncTestZoneSpec');
+ if (fakeAsyncZoneSpec) {
+ const dateTime = arguments.length > 0 ? arguments[0] : new Date();
+ return fakeAsyncZoneSpec.setCurrentRealTime.apply(
+ fakeAsyncZoneSpec, dateTime && typeof dateTime.getTime === 'function' ?
+ [dateTime.getTime()] :
+ arguments);
+ }
+ return originalMockDate.apply(this, arguments);
+ };
+ // for auto go into fakeAsync feature, we need the flag to enable it
+ if (enableAutoFakeAsyncWhenClockPatched) {
+ ['install', 'uninstall'].forEach(methodName => {
+ const originalClockFn: Function = (clock[symbol(methodName)] = clock[methodName]);
+ clock[methodName] = function() {
+ const FakeAsyncTestZoneSpec = (Zone as any)['FakeAsyncTestZoneSpec'];
+ if (FakeAsyncTestZoneSpec) {
+ (jasmine as any)[symbol('clockInstalled')] = 'install' === methodName;
+ return;
+ }
+ return originalClockFn.apply(this, arguments);
+ };
+ });
+ }
+ }
+ return clock;
+ };
+ }
+ /**
+ * Gets a function wrapping the body of a Jasmine `describe` block to execute in a
+ * synchronous-only zone.
+ */
+ function wrapDescribeInZone(describeBody: Function): Function {
+ return function() { return syncZone.run(describeBody, this, (arguments as any) as any[]); };
+ }
+
+ function runInTestZone(testBody: Function, applyThis: any, queueRunner: any, done?: Function) {
+ const isClockInstalled = !!(jasmine as any)[symbol('clockInstalled')];
+ const testProxyZoneSpec = queueRunner.testProxyZoneSpec;
+ const testProxyZone = queueRunner.testProxyZone;
+ let lastDelegate;
+ if (isClockInstalled && enableAutoFakeAsyncWhenClockPatched) {
+ // auto run a fakeAsync
+ const fakeAsyncModule = (Zone as any)[Zone.__symbol__('fakeAsyncTest')];
+ if (fakeAsyncModule && typeof fakeAsyncModule.fakeAsync === 'function') {
+ testBody = fakeAsyncModule.fakeAsync(testBody);
+ }
+ }
+ if (done) {
+ return testProxyZone.run(testBody, applyThis, [done]);
+ } else {
+ return testProxyZone.run(testBody, applyThis);
+ }
+ }
+
+ /**
+ * Gets a function wrapping the body of a Jasmine `it/beforeEach/afterEach` block to
+ * execute in a ProxyZone zone.
+ * This will run in `testProxyZone`. The `testProxyZone` will be reset by the `ZoneQueueRunner`
+ */
+ function wrapTestInZone(testBody: Function): Function {
+ // The `done` callback is only passed through if the function expects at least one argument.
+ // Note we have to make a function with correct number of arguments, otherwise jasmine will
+ // think that all functions are sync or async.
+ return (testBody && (testBody.length ? function(done: Function) {
+ return runInTestZone(testBody, this, this.queueRunner, done);
+ } : function() { return runInTestZone(testBody, this, this.queueRunner); }));
+ }
+ interface QueueRunner {
+ execute(): void;
+ }
+ interface QueueRunnerAttrs {
+ queueableFns: {fn: Function}[];
+ clearStack: (fn: any) => void;
+ catchException: () => boolean;
+ fail: () => void;
+ onComplete: () => void;
+ onException: (error: any) => void;
+ userContext: any;
+ timeout: {setTimeout: Function; clearTimeout: Function};
+ }
+
+ const QueueRunner = (jasmine as any).QueueRunner as {
+ new (attrs: QueueRunnerAttrs): QueueRunner;
+ };
+ (jasmine as any).QueueRunner = (function(_super) {
+ __extends(ZoneQueueRunner, _super);
+ function ZoneQueueRunner(attrs: QueueRunnerAttrs) {
+ attrs.onComplete = (fn => () => {
+ // All functions are done, clear the test zone.
+ this.testProxyZone = null;
+ this.testProxyZoneSpec = null;
+ ambientZone.scheduleMicroTask('jasmine.onComplete', fn);
+ })(attrs.onComplete);
+
+ const nativeSetTimeout = _global[Zone.__symbol__('setTimeout')];
+ const nativeClearTimeout = _global[Zone.__symbol__('clearTimeout')];
+ if (nativeSetTimeout) {
+ // should run setTimeout inside jasmine outside of zone
+ attrs.timeout = {
+ setTimeout: nativeSetTimeout ? nativeSetTimeout : _global.setTimeout,
+ clearTimeout: nativeClearTimeout ? nativeClearTimeout : _global.clearTimeout
+ };
+ }
+
+ // create a userContext to hold the queueRunner itself
+ // so we can access the testProxy in it/xit/beforeEach ...
+ if ((jasmine as any).UserContext) {
+ if (!attrs.userContext) {
+ attrs.userContext = new (jasmine as any).UserContext();
+ }
+ attrs.userContext.queueRunner = this;
+ } else {
+ if (!attrs.userContext) {
+ attrs.userContext = {};
+ }
+ attrs.userContext.queueRunner = this;
+ }
+
+ // patch attrs.onException
+ const onException = attrs.onException;
+ attrs.onException = function(error: any) {
+ if (error &&
+ error.message ===
+ 'Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.') {
+ // jasmine timeout, we can make the error message more
+ // reasonable to tell what tasks are pending
+ const proxyZoneSpec: any = this && this.testProxyZoneSpec;
+ if (proxyZoneSpec) {
+ const pendingTasksInfo = proxyZoneSpec.getAndClearPendingTasksInfo();
+ try {
+ // try catch here in case error.message is not writable
+ error.message += pendingTasksInfo;
+ } catch (err) {
+ }
+ }
+ }
+ if (onException) {
+ onException.call(this, error);
+ }
+ };
+
+ _super.call(this, attrs);
+ }
+ ZoneQueueRunner.prototype.execute = function() {
+ let zone: Zone|null = Zone.current;
+ let isChildOfAmbientZone = false;
+ while (zone) {
+ if (zone === ambientZone) {
+ isChildOfAmbientZone = true;
+ break;
+ }
+ zone = zone.parent;
+ }
+
+ if (!isChildOfAmbientZone) throw new Error('Unexpected Zone: ' + Zone.current.name);
+
+ // This is the zone which will be used for running individual tests.
+ // It will be a proxy zone, so that the tests function can retroactively install
+ // different zones.
+ // Example:
+ // - In beforeEach() do childZone = Zone.current.fork(...);
+ // - In it() try to do fakeAsync(). The issue is that because the beforeEach forked the
+ // zone outside of fakeAsync it will be able to escape the fakeAsync rules.
+ // - Because ProxyZone is parent fo `childZone` fakeAsync can retroactively add
+ // fakeAsync behavior to the childZone.
+
+ this.testProxyZoneSpec = new ProxyZoneSpec();
+ this.testProxyZone = ambientZone.fork(this.testProxyZoneSpec);
+ if (!Zone.currentTask) {
+ // if we are not running in a task then if someone would register a
+ // element.addEventListener and then calling element.click() the
+ // addEventListener callback would think that it is the top most task and would
+ // drain the microtask queue on element.click() which would be incorrect.
+ // For this reason we always force a task when running jasmine tests.
+ Zone.current.scheduleMicroTask(
+ 'jasmine.execute().forceTask', () => QueueRunner.prototype.execute.call(this));
+ } else {
+ _super.prototype.execute.call(this);
+ }
+ };
+ return ZoneQueueRunner;
+ })(QueueRunner);
+})(global);
diff --git a/packages/zone.js/lib/mix/rollup-mix.ts b/packages/zone.js/lib/mix/rollup-mix.ts
new file mode 100644
index 0000000000..57e4877737
--- /dev/null
+++ b/packages/zone.js/lib/mix/rollup-mix.ts
@@ -0,0 +1,13 @@
+/**
+ * @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
+ */
+
+import '../zone';
+import '../common/promise';
+import '../common/to-string';
+import '../browser/browser';
+import '../node/node';
diff --git a/packages/zone.js/lib/mocha/mocha.ts b/packages/zone.js/lib/mocha/mocha.ts
new file mode 100644
index 0000000000..c3c6d13a17
--- /dev/null
+++ b/packages/zone.js/lib/mocha/mocha.ts
@@ -0,0 +1,161 @@
+/**
+ * @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
+ */
+
+'use strict';
+
+((context: any) => {
+ const Mocha = context.Mocha;
+
+ if (typeof Mocha === 'undefined') {
+ throw new Error('Missing Mocha.js');
+ }
+
+ if (typeof Zone === 'undefined') {
+ throw new Error('Missing Zone.js');
+ }
+
+ const ProxyZoneSpec = (Zone as any)['ProxyZoneSpec'];
+ const SyncTestZoneSpec = (Zone as any)['SyncTestZoneSpec'];
+
+ if (!ProxyZoneSpec) {
+ throw new Error('Missing ProxyZoneSpec');
+ }
+
+ if (Mocha['__zone_patch__']) {
+ throw new Error('"Mocha" has already been patched with "Zone".');
+ }
+
+ Mocha['__zone_patch__'] = true;
+
+ const rootZone = Zone.current;
+ const syncZone = rootZone.fork(new SyncTestZoneSpec('Mocha.describe'));
+ let testZone: Zone|null = null;
+ const suiteZone = rootZone.fork(new ProxyZoneSpec());
+
+ const mochaOriginal = {
+ after: Mocha.after,
+ afterEach: Mocha.afterEach,
+ before: Mocha.before,
+ beforeEach: Mocha.beforeEach,
+ describe: Mocha.describe,
+ it: Mocha.it
+ };
+
+ function modifyArguments(args: IArguments, syncTest: Function, asyncTest?: Function): any[] {
+ for (let i = 0; i < args.length; i++) {
+ let arg = args[i];
+ if (typeof arg === 'function') {
+ // The `done` callback is only passed through if the function expects at
+ // least one argument.
+ // Note we have to make a function with correct number of arguments,
+ // otherwise mocha will
+ // think that all functions are sync or async.
+ args[i] = (arg.length === 0) ? syncTest(arg) : asyncTest !(arg);
+ // Mocha uses toString to view the test body in the result list, make sure we return the
+ // correct function body
+ args[i].toString = function() { return arg.toString(); };
+ }
+ }
+
+ return args as any;
+ }
+
+ function wrapDescribeInZone(args: IArguments): any[] {
+ const syncTest: any = function(fn: Function) {
+ return function() { return syncZone.run(fn, this, arguments as any as any[]); };
+ };
+
+ return modifyArguments(args, syncTest);
+ }
+
+ function wrapTestInZone(args: IArguments): any[] {
+ const asyncTest = function(fn: Function) {
+ return function(done: Function) { return testZone !.run(fn, this, [done]); };
+ };
+
+ const syncTest: any = function(fn: Function) {
+ return function() { return testZone !.run(fn, this); };
+ };
+
+ return modifyArguments(args, syncTest, asyncTest);
+ }
+
+ function wrapSuiteInZone(args: IArguments): any[] {
+ const asyncTest = function(fn: Function) {
+ return function(done: Function) { return suiteZone.run(fn, this, [done]); };
+ };
+
+ const syncTest: any = function(fn: Function) {
+ return function() { return suiteZone.run(fn, this); };
+ };
+
+ return modifyArguments(args, syncTest, asyncTest);
+ }
+
+ context.describe = context.suite = Mocha.describe = function() {
+ return mochaOriginal.describe.apply(this, wrapDescribeInZone(arguments));
+ };
+
+ context.xdescribe = context.suite.skip = Mocha.describe.skip = function() {
+ return mochaOriginal.describe.skip.apply(this, wrapDescribeInZone(arguments));
+ };
+
+ context.describe.only = context.suite.only = Mocha.describe.only = function() {
+ return mochaOriginal.describe.only.apply(this, wrapDescribeInZone(arguments));
+ };
+
+ context.it = context.specify = context.test =
+ Mocha.it = function() { return mochaOriginal.it.apply(this, wrapTestInZone(arguments)); };
+
+ context.xit = context.xspecify = Mocha.it.skip = function() {
+ return mochaOriginal.it.skip.apply(this, wrapTestInZone(arguments));
+ };
+
+ context.it.only = context.test.only = Mocha.it.only = function() {
+ return mochaOriginal.it.only.apply(this, wrapTestInZone(arguments));
+ };
+
+ context.after = context.suiteTeardown = Mocha.after = function() {
+ return mochaOriginal.after.apply(this, wrapSuiteInZone(arguments));
+ };
+
+ context.afterEach = context.teardown = Mocha.afterEach = function() {
+ return mochaOriginal.afterEach.apply(this, wrapTestInZone(arguments));
+ };
+
+ context.before = context.suiteSetup = Mocha.before = function() {
+ return mochaOriginal.before.apply(this, wrapSuiteInZone(arguments));
+ };
+
+ context.beforeEach = context.setup = Mocha.beforeEach = function() {
+ return mochaOriginal.beforeEach.apply(this, wrapTestInZone(arguments));
+ };
+
+ ((originalRunTest, originalRun) => {
+ Mocha.Runner.prototype.runTest = function(fn: Function) {
+ Zone.current.scheduleMicroTask('mocha.forceTask', () => { originalRunTest.call(this, fn); });
+ };
+
+ Mocha.Runner.prototype.run = function(fn: Function) {
+ this.on('test', (e: any) => { testZone = rootZone.fork(new ProxyZoneSpec()); });
+
+ this.on('fail', (test: any, err: any) => {
+ const proxyZoneSpec = testZone && testZone.get('ProxyZoneSpec');
+ if (proxyZoneSpec && err) {
+ try {
+ // try catch here in case err.message is not writable
+ err.message += proxyZoneSpec.getAndClearPendingTasksInfo();
+ } catch (error) {
+ }
+ }
+ });
+
+ return originalRun.call(this, fn);
+ };
+ })(Mocha.Runner.prototype.runTest, Mocha.Runner.prototype.run);
+})(global);
diff --git a/packages/zone.js/lib/node/events.ts b/packages/zone.js/lib/node/events.ts
new file mode 100644
index 0000000000..09ab0c8566
--- /dev/null
+++ b/packages/zone.js/lib/node/events.ts
@@ -0,0 +1,63 @@
+/**
+ * @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
+ */
+
+import {patchEventTarget} from '../common/events';
+
+Zone.__load_patch('EventEmitter', (global: any) => {
+ // For EventEmitter
+ const EE_ADD_LISTENER = 'addListener';
+ const EE_PREPEND_LISTENER = 'prependListener';
+ const EE_REMOVE_LISTENER = 'removeListener';
+ const EE_REMOVE_ALL_LISTENER = 'removeAllListeners';
+ const EE_LISTENERS = 'listeners';
+ const EE_ON = 'on';
+
+ const compareTaskCallbackVsDelegate = function(task: any, delegate: any) {
+ // same callback, same capture, same event name, just return
+ return task.callback === delegate || task.callback.listener === delegate;
+ };
+
+ const eventNameToString = function(eventName: string|Symbol) {
+ if (typeof eventName === 'string') {
+ return eventName as string;
+ }
+ if (!eventName) {
+ return '';
+ }
+ return eventName.toString().replace('(', '_').replace(')', '_');
+ };
+
+ function patchEventEmitterMethods(obj: any) {
+ const result = patchEventTarget(global, [obj], {
+ useG: false,
+ add: EE_ADD_LISTENER,
+ rm: EE_REMOVE_LISTENER,
+ prepend: EE_PREPEND_LISTENER,
+ rmAll: EE_REMOVE_ALL_LISTENER,
+ listeners: EE_LISTENERS,
+ chkDup: false,
+ rt: true,
+ diff: compareTaskCallbackVsDelegate,
+ eventNameToString: eventNameToString
+ });
+ if (result && result[0]) {
+ obj[EE_ON] = obj[EE_ADD_LISTENER];
+ }
+ }
+
+ // EventEmitter
+ let events;
+ try {
+ events = require('events');
+ } catch (err) {
+ }
+
+ if (events && events.EventEmitter) {
+ patchEventEmitterMethods(events.EventEmitter.prototype);
+ }
+});
diff --git a/packages/zone.js/lib/node/fs.ts b/packages/zone.js/lib/node/fs.ts
new file mode 100644
index 0000000000..9af536abc6
--- /dev/null
+++ b/packages/zone.js/lib/node/fs.ts
@@ -0,0 +1,41 @@
+/**
+ * @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
+ */
+
+import {patchMacroTask} from '../common/utils';
+
+Zone.__load_patch('fs', () => {
+ let fs: any;
+ try {
+ fs = require('fs');
+ } catch (err) {
+ }
+
+ // watch, watchFile, unwatchFile has been patched
+ // because EventEmitter has been patched
+ const TO_PATCH_MACROTASK_METHODS = [
+ 'access', 'appendFile', 'chmod', 'chown', 'close', 'exists', 'fchmod',
+ 'fchown', 'fdatasync', 'fstat', 'fsync', 'ftruncate', 'futimes', 'lchmod',
+ 'lchown', 'link', 'lstat', 'mkdir', 'mkdtemp', 'open', 'read',
+ 'readdir', 'readFile', 'readlink', 'realpath', 'rename', 'rmdir', 'stat',
+ 'symlink', 'truncate', 'unlink', 'utimes', 'write', 'writeFile',
+ ];
+
+ if (fs) {
+ TO_PATCH_MACROTASK_METHODS.filter(name => !!fs[name] && typeof fs[name] === 'function')
+ .forEach(name => {
+ patchMacroTask(fs, name, (self: any, args: any[]) => {
+ return {
+ name: 'fs.' + name,
+ args: args,
+ cbIdx: args.length > 0 ? args.length - 1 : -1,
+ target: self
+ };
+ });
+ });
+ }
+});
diff --git a/packages/zone.js/lib/node/node.ts b/packages/zone.js/lib/node/node.ts
new file mode 100644
index 0000000000..94ef7fb053
--- /dev/null
+++ b/packages/zone.js/lib/node/node.ts
@@ -0,0 +1,154 @@
+/**
+ * @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
+ */
+
+import './node_util';
+import './events';
+import './fs';
+
+import {findEventTasks} from '../common/events';
+import {patchTimer} from '../common/timers';
+import {ArraySlice, isMix, patchMacroTask, patchMicroTask} from '../common/utils';
+
+const set = 'set';
+const clear = 'clear';
+
+Zone.__load_patch('node_timers', (global: any, Zone: ZoneType) => {
+ // Timers
+ let globalUseTimeoutFromTimer = false;
+ try {
+ const timers = require('timers');
+ let globalEqualTimersTimeout = global.setTimeout === timers.setTimeout;
+ if (!globalEqualTimersTimeout && !isMix) {
+ // 1. if isMix, then we are in mix environment such as Electron
+ // we should only patch timers.setTimeout because global.setTimeout
+ // have been patched
+ // 2. if global.setTimeout not equal timers.setTimeout, check
+ // whether global.setTimeout use timers.setTimeout or not
+ const originSetTimeout = timers.setTimeout;
+ timers.setTimeout = function() {
+ globalUseTimeoutFromTimer = true;
+ return originSetTimeout.apply(this, arguments);
+ };
+ const detectTimeout = global.setTimeout(() => {}, 100);
+ clearTimeout(detectTimeout);
+ timers.setTimeout = originSetTimeout;
+ }
+ patchTimer(timers, set, clear, 'Timeout');
+ patchTimer(timers, set, clear, 'Interval');
+ patchTimer(timers, set, clear, 'Immediate');
+ } catch (error) {
+ // timers module not exists, for example, when we using nativeScript
+ // timers is not available
+ }
+ if (isMix) {
+ // if we are in mix environment, such as Electron,
+ // the global.setTimeout has already been patched,
+ // so we just patch timers.setTimeout
+ return;
+ }
+ if (!globalUseTimeoutFromTimer) {
+ // 1. global setTimeout equals timers setTimeout
+ // 2. or global don't use timers setTimeout(maybe some other library patch setTimeout)
+ // 3. or load timers module error happens, we should patch global setTimeout
+ patchTimer(global, set, clear, 'Timeout');
+ patchTimer(global, set, clear, 'Interval');
+ patchTimer(global, set, clear, 'Immediate');
+ } else {
+ // global use timers setTimeout, but not equals
+ // this happens when use nodejs v0.10.x, global setTimeout will
+ // use a lazy load version of timers setTimeout
+ // we should not double patch timer's setTimeout
+ // so we only store the __symbol__ for consistency
+ global[Zone.__symbol__('setTimeout')] = global.setTimeout;
+ global[Zone.__symbol__('setInterval')] = global.setInterval;
+ global[Zone.__symbol__('setImmediate')] = global.setImmediate;
+ }
+});
+
+// patch process related methods
+Zone.__load_patch('nextTick', () => {
+ // patch nextTick as microTask
+ patchMicroTask(process, 'nextTick', (self: any, args: any[]) => {
+ return {
+ name: 'process.nextTick',
+ args: args,
+ cbIdx: (args.length > 0 && typeof args[0] === 'function') ? 0 : -1,
+ target: process
+ };
+ });
+});
+
+Zone.__load_patch(
+ 'handleUnhandledPromiseRejection', (global: any, Zone: ZoneType, api: _ZonePrivate) => {
+ (Zone as any)[api.symbol('unhandledPromiseRejectionHandler')] =
+ findProcessPromiseRejectionHandler('unhandledRejection');
+
+ (Zone as any)[api.symbol('rejectionHandledHandler')] =
+ findProcessPromiseRejectionHandler('rejectionHandled');
+
+ // handle unhandled promise rejection
+ function findProcessPromiseRejectionHandler(evtName: string) {
+ return function(e: any) {
+ const eventTasks = findEventTasks(process, evtName);
+ eventTasks.forEach(eventTask => {
+ // process has added unhandledrejection event listener
+ // trigger the event listener
+ if (evtName === 'unhandledRejection') {
+ eventTask.invoke(e.rejection, e.promise);
+ } else if (evtName === 'rejectionHandled') {
+ eventTask.invoke(e.promise);
+ }
+ });
+ };
+ }
+ });
+
+
+// Crypto
+Zone.__load_patch('crypto', () => {
+ let crypto: any;
+ try {
+ crypto = require('crypto');
+ } catch (err) {
+ }
+
+ // use the generic patchMacroTask to patch crypto
+ if (crypto) {
+ const methodNames = ['randomBytes', 'pbkdf2'];
+ methodNames.forEach(name => {
+ patchMacroTask(crypto, name, (self: any, args: any[]) => {
+ return {
+ name: 'crypto.' + name,
+ args: args,
+ cbIdx: (args.length > 0 && typeof args[args.length - 1] === 'function') ?
+ args.length - 1 :
+ -1,
+ target: crypto
+ };
+ });
+ });
+ }
+});
+
+Zone.__load_patch('console', (global: any, Zone: ZoneType) => {
+ const consoleMethods =
+ ['dir', 'log', 'info', 'error', 'warn', 'assert', 'debug', 'timeEnd', 'trace'];
+ consoleMethods.forEach((m: string) => {
+ const originalMethod = (console as any)[Zone.__symbol__(m)] = (console as any)[m];
+ if (originalMethod) {
+ (console as any)[m] = function() {
+ const args = ArraySlice.call(arguments);
+ if (Zone.current === Zone.root) {
+ return originalMethod.apply(this, args);
+ } else {
+ return Zone.root.run(originalMethod, this, args);
+ }
+ };
+ }
+ });
+});
diff --git a/packages/zone.js/lib/node/node_util.ts b/packages/zone.js/lib/node/node_util.ts
new file mode 100644
index 0000000000..68df1db812
--- /dev/null
+++ b/packages/zone.js/lib/node/node_util.ts
@@ -0,0 +1,17 @@
+/**
+ * @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
+ */
+
+import {bindArguments, patchMacroTask, patchMethod, patchOnProperties, setShouldCopySymbolProperties} from '../common/utils';
+
+Zone.__load_patch('node_util', (global: any, Zone: ZoneType, api: _ZonePrivate) => {
+ api.patchOnProperties = patchOnProperties;
+ api.patchMethod = patchMethod;
+ api.bindArguments = bindArguments;
+ api.patchMacroTask = patchMacroTask;
+ setShouldCopySymbolProperties(true);
+});
diff --git a/packages/zone.js/lib/node/rollup-main.ts b/packages/zone.js/lib/node/rollup-main.ts
new file mode 100644
index 0000000000..136714b1e7
--- /dev/null
+++ b/packages/zone.js/lib/node/rollup-main.ts
@@ -0,0 +1,12 @@
+/**
+ * @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
+ */
+
+import '../zone';
+import '../common/promise';
+import '../common/to-string';
+import './node';
\ No newline at end of file
diff --git a/packages/zone.js/lib/node/rollup-test-main.ts b/packages/zone.js/lib/node/rollup-test-main.ts
new file mode 100644
index 0000000000..91a951b244
--- /dev/null
+++ b/packages/zone.js/lib/node/rollup-test-main.ts
@@ -0,0 +1,12 @@
+/**
+ * @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
+ */
+
+import './rollup-main';
+
+// load test related files into bundle
+import '../testing/zone-testing';
diff --git a/packages/zone.js/lib/rxjs/rxjs-fake-async.ts b/packages/zone.js/lib/rxjs/rxjs-fake-async.ts
new file mode 100644
index 0000000000..a618fb000c
--- /dev/null
+++ b/packages/zone.js/lib/rxjs/rxjs-fake-async.ts
@@ -0,0 +1,21 @@
+/**
+ * @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
+ */
+
+import {Scheduler, asapScheduler, asyncScheduler} from 'rxjs';
+
+Zone.__load_patch('rxjs.Scheduler.now', (global: any, Zone: ZoneType, api: _ZonePrivate) => {
+ api.patchMethod(Scheduler, 'now', (delegate: Function) => (self: any, args: any[]) => {
+ return Date.now.call(self);
+ });
+ api.patchMethod(asyncScheduler, 'now', (delegate: Function) => (self: any, args: any[]) => {
+ return Date.now.call(self);
+ });
+ api.patchMethod(asapScheduler, 'now', (delegate: Function) => (self: any, args: any[]) => {
+ return Date.now.call(self);
+ });
+});
diff --git a/packages/zone.js/lib/rxjs/rxjs.ts b/packages/zone.js/lib/rxjs/rxjs.ts
new file mode 100644
index 0000000000..c0c1eeb01b
--- /dev/null
+++ b/packages/zone.js/lib/rxjs/rxjs.ts
@@ -0,0 +1,182 @@
+/**
+ * @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
+ */
+
+import {Observable, Subscriber, Subscription} from 'rxjs';
+
+(Zone as any).__load_patch('rxjs', (global: any, Zone: ZoneType, api: _ZonePrivate) => {
+ const symbol: (symbolString: string) => string = (Zone as any).__symbol__;
+ const nextSource = 'rxjs.Subscriber.next';
+ const errorSource = 'rxjs.Subscriber.error';
+ const completeSource = 'rxjs.Subscriber.complete';
+
+ const ObjectDefineProperties = Object.defineProperties;
+
+ const patchObservable = function() {
+ const ObservablePrototype: any = Observable.prototype;
+ const _symbolSubscribe = symbol('_subscribe');
+ const _subscribe = ObservablePrototype[_symbolSubscribe] = ObservablePrototype._subscribe;
+
+ ObjectDefineProperties(Observable.prototype, {
+ _zone: {value: null, writable: true, configurable: true},
+ _zoneSource: {value: null, writable: true, configurable: true},
+ _zoneSubscribe: {value: null, writable: true, configurable: true},
+ source: {
+ configurable: true,
+ get: function(this: Observable) { return (this as any)._zoneSource; },
+ set: function(this: Observable, source: any) {
+ (this as any)._zone = Zone.current;
+ (this as any)._zoneSource = source;
+ }
+ },
+ _subscribe: {
+ configurable: true,
+ get: function(this: Observable) {
+ if ((this as any)._zoneSubscribe) {
+ return (this as any)._zoneSubscribe;
+ } else if (this.constructor === Observable) {
+ return _subscribe;
+ }
+ const proto = Object.getPrototypeOf(this);
+ return proto && proto._subscribe;
+ },
+ set: function(this: Observable, subscribe: any) {
+ (this as any)._zone = Zone.current;
+ (this as any)._zoneSubscribe = function() {
+ if (this._zone && this._zone !== Zone.current) {
+ const tearDown = this._zone.run(subscribe, this, arguments);
+ if (tearDown && typeof tearDown === 'function') {
+ const zone = this._zone;
+ return function() {
+ if (zone !== Zone.current) {
+ return zone.run(tearDown, this, arguments);
+ }
+ return tearDown.apply(this, arguments);
+ };
+ }
+ return tearDown;
+ }
+ return subscribe.apply(this, arguments);
+ };
+ }
+ },
+ subjectFactory: {
+ get: function() { return (this as any)._zoneSubjectFactory; },
+ set: function(factory: any) {
+ const zone = this._zone;
+ this._zoneSubjectFactory = function() {
+ if (zone && zone !== Zone.current) {
+ return zone.run(factory, this, arguments);
+ }
+ return factory.apply(this, arguments);
+ };
+ }
+ }
+ });
+ };
+
+ api.patchMethod(Observable.prototype, 'lift', (delegate: any) => (self: any, args: any[]) => {
+ const observable: any = delegate.apply(self, args);
+ if (observable.operator) {
+ observable.operator._zone = Zone.current;
+ api.patchMethod(
+ observable.operator, 'call',
+ (operatorDelegate: any) => (operatorSelf: any, operatorArgs: any[]) => {
+ if (operatorSelf._zone && operatorSelf._zone !== Zone.current) {
+ return operatorSelf._zone.run(operatorDelegate, operatorSelf, operatorArgs);
+ }
+ return operatorDelegate.apply(operatorSelf, operatorArgs);
+ });
+ }
+ return observable;
+ });
+
+ const patchSubscription = function() {
+ ObjectDefineProperties(Subscription.prototype, {
+ _zone: {value: null, writable: true, configurable: true},
+ _zoneUnsubscribe: {value: null, writable: true, configurable: true},
+ _unsubscribe: {
+ get: function(this: Subscription) {
+ if ((this as any)._zoneUnsubscribe) {
+ return (this as any)._zoneUnsubscribe;
+ }
+ const proto = Object.getPrototypeOf(this);
+ return proto && proto._unsubscribe;
+ },
+ set: function(this: Subscription, unsubscribe: any) {
+ (this as any)._zone = Zone.current;
+ (this as any)._zoneUnsubscribe = function() {
+ if (this._zone && this._zone !== Zone.current) {
+ return this._zone.run(unsubscribe, this, arguments);
+ }
+ return unsubscribe.apply(this, arguments);
+ };
+ }
+ }
+ });
+ };
+
+ const patchSubscriber = function() {
+ const next = Subscriber.prototype.next;
+ const error = Subscriber.prototype.error;
+ const complete = Subscriber.prototype.complete;
+
+ Object.defineProperty(Subscriber.prototype, 'destination', {
+ configurable: true,
+ get: function(this: Subscriber) { return (this as any)._zoneDestination; },
+ set: function(this: Subscriber, destination: any) {
+ (this as any)._zone = Zone.current;
+ (this as any)._zoneDestination = destination;
+ }
+ });
+
+ // patch Subscriber.next to make sure it run
+ // into SubscriptionZone
+ Subscriber.prototype.next = function() {
+ const currentZone = Zone.current;
+ const subscriptionZone = this._zone;
+
+ // for performance concern, check Zone.current
+ // equal with this._zone(SubscriptionZone) or not
+ if (subscriptionZone && subscriptionZone !== currentZone) {
+ return subscriptionZone.run(next, this, arguments, nextSource);
+ } else {
+ return next.apply(this, arguments as any);
+ }
+ };
+
+ Subscriber.prototype.error = function() {
+ const currentZone = Zone.current;
+ const subscriptionZone = this._zone;
+
+ // for performance concern, check Zone.current
+ // equal with this._zone(SubscriptionZone) or not
+ if (subscriptionZone && subscriptionZone !== currentZone) {
+ return subscriptionZone.run(error, this, arguments, errorSource);
+ } else {
+ return error.apply(this, arguments as any);
+ }
+ };
+
+ Subscriber.prototype.complete = function() {
+ const currentZone = Zone.current;
+ const subscriptionZone = this._zone;
+
+ // for performance concern, check Zone.current
+ // equal with this._zone(SubscriptionZone) or not
+ if (subscriptionZone && subscriptionZone !== currentZone) {
+ return subscriptionZone.run(complete, this, arguments, completeSource);
+ } else {
+ return complete.call(this);
+ }
+ };
+ };
+
+ patchObservable();
+ patchSubscription();
+ patchSubscriber();
+});
diff --git a/packages/zone.js/lib/testing/async-testing.ts b/packages/zone.js/lib/testing/async-testing.ts
new file mode 100644
index 0000000000..7b81bd274c
--- /dev/null
+++ b/packages/zone.js/lib/testing/async-testing.ts
@@ -0,0 +1,99 @@
+/**
+ * @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
+ */
+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(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);
+ }
+ (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);
+ }
+});
\ No newline at end of file
diff --git a/packages/zone.js/lib/testing/fake-async.ts b/packages/zone.js/lib/testing/fake-async.ts
new file mode 100644
index 0000000000..0764dfb696
--- /dev/null
+++ b/packages/zone.js/lib/testing/fake-async.ts
@@ -0,0 +1,153 @@
+/**
+ * @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
+ */
+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
+ return function(...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();
+ }
+ };
+ }
+
+ 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): 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
+ */
+ 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};
+});
\ No newline at end of file
diff --git a/packages/zone.js/lib/testing/promise-testing.ts b/packages/zone.js/lib/testing/promise-testing.ts
new file mode 100644
index 0000000000..e37ab6e88f
--- /dev/null
+++ b/packages/zone.js/lib/testing/promise-testing.ts
@@ -0,0 +1,68 @@
+/**
+ * @license
+ * Copyright Google Inc. All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+/**
+ * Promise for async/fakeAsync zoneSpec test
+ * can support async operation which not supported by zone.js
+ * such as
+ * it ('test jsonp in AsyncZone', async() => {
+ * new Promise(res => {
+ * jsonp(url, (data) => {
+ * // success callback
+ * res(data);
+ * });
+ * }).then((jsonpResult) => {
+ * // get jsonp result.
+ *
+ * // user will expect AsyncZoneSpec wait for
+ * // then, but because jsonp is not zone aware
+ * // AsyncZone will finish before then is called.
+ * });
+ * });
+ */
+Zone.__load_patch('promisefortest', (global: any, Zone: ZoneType, api: _ZonePrivate) => {
+ const symbolState: string = api.symbol('state');
+ const UNRESOLVED: null = null;
+ const symbolParentUnresolved = api.symbol('parentUnresolved');
+
+ // patch Promise.prototype.then to keep an internal
+ // number for tracking unresolved chained promise
+ // we will decrease this number when the parent promise
+ // being resolved/rejected and chained promise was
+ // scheduled as a microTask.
+ // so we can know such kind of chained promise still
+ // not resolved in AsyncTestZone
+ (Promise as any)[api.symbol('patchPromiseForTest')] = function patchPromiseForTest() {
+ let oriThen = (Promise as any)[Zone.__symbol__('ZonePromiseThen')];
+ if (oriThen) {
+ return;
+ }
+ oriThen = (Promise as any)[Zone.__symbol__('ZonePromiseThen')] = Promise.prototype.then;
+ Promise.prototype.then = function() {
+ const chained = oriThen.apply(this, arguments);
+ if (this[symbolState] === UNRESOLVED) {
+ // parent promise is unresolved.
+ const asyncTestZoneSpec = Zone.current.get('AsyncTestZoneSpec');
+ if (asyncTestZoneSpec) {
+ asyncTestZoneSpec.unresolvedChainedPromiseCount++;
+ chained[symbolParentUnresolved] = true;
+ }
+ }
+ return chained;
+ };
+ };
+
+ (Promise as any)[api.symbol('unPatchPromiseForTest')] = function unpatchPromiseForTest() {
+ // restore origin then
+ const oriThen = (Promise as any)[Zone.__symbol__('ZonePromiseThen')];
+ if (oriThen) {
+ Promise.prototype.then = oriThen;
+ (Promise as any)[Zone.__symbol__('ZonePromiseThen')] = undefined;
+ }
+ };
+});
\ No newline at end of file
diff --git a/packages/zone.js/lib/testing/zone-testing.ts b/packages/zone.js/lib/testing/zone-testing.ts
new file mode 100644
index 0000000000..c5ebd1ad3b
--- /dev/null
+++ b/packages/zone.js/lib/testing/zone-testing.ts
@@ -0,0 +1,16 @@
+/**
+ * @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
+ */
+
+// load test related files into bundle in correct order
+import '../zone-spec/long-stack-trace';
+import '../zone-spec/proxy';
+import '../zone-spec/sync-test';
+import '../jasmine/jasmine';
+import './async-testing';
+import './fake-async';
+import './promise-testing';
\ No newline at end of file
diff --git a/packages/zone.js/lib/zone-spec/async-test.ts b/packages/zone.js/lib/zone-spec/async-test.ts
new file mode 100644
index 0000000000..071502107d
--- /dev/null
+++ b/packages/zone.js/lib/zone-spec/async-test.ts
@@ -0,0 +1,149 @@
+/**
+ * @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
+ */
+(function(_global: any) {
+ class AsyncTestZoneSpec implements ZoneSpec {
+ static symbolParentUnresolved = Zone.__symbol__('parentUnresolved');
+
+ _pendingMicroTasks: boolean = false;
+ _pendingMacroTasks: boolean = false;
+ _alreadyErrored: boolean = false;
+ _isSync: boolean = false;
+ runZone = Zone.current;
+ unresolvedChainedPromiseCount = 0;
+
+ supportWaitUnresolvedChainedPromise = false;
+
+ constructor(
+ private finishCallback: Function, private failCallback: Function, namePrefix: string) {
+ this.name = 'asyncTestZone for ' + namePrefix;
+ this.properties = {'AsyncTestZoneSpec': this};
+ this.supportWaitUnresolvedChainedPromise =
+ _global[Zone.__symbol__('supportWaitUnResolvedChainedPromise')] === true;
+ }
+
+ isUnresolvedChainedPromisePending() { return this.unresolvedChainedPromiseCount > 0; }
+
+ _finishCallbackIfDone() {
+ if (!(this._pendingMicroTasks || this._pendingMacroTasks ||
+ (this.supportWaitUnresolvedChainedPromise &&
+ this.isUnresolvedChainedPromisePending()))) {
+ // We do this because we would like to catch unhandled rejected promises.
+ this.runZone.run(() => {
+ setTimeout(() => {
+ if (!this._alreadyErrored && !(this._pendingMicroTasks || this._pendingMacroTasks)) {
+ this.finishCallback();
+ }
+ }, 0);
+ });
+ }
+ }
+
+ patchPromiseForTest() {
+ if (!this.supportWaitUnresolvedChainedPromise) {
+ return;
+ }
+ const patchPromiseForTest = (Promise as any)[Zone.__symbol__('patchPromiseForTest')];
+ if (patchPromiseForTest) {
+ patchPromiseForTest();
+ }
+ }
+
+ unPatchPromiseForTest() {
+ if (!this.supportWaitUnresolvedChainedPromise) {
+ return;
+ }
+ const unPatchPromiseForTest = (Promise as any)[Zone.__symbol__('unPatchPromiseForTest')];
+ if (unPatchPromiseForTest) {
+ unPatchPromiseForTest();
+ }
+ }
+
+ // ZoneSpec implementation below.
+
+ name: string;
+
+ properties: {[key: string]: any};
+
+ onScheduleTask(delegate: ZoneDelegate, current: Zone, target: Zone, task: Task): Task {
+ if (task.type !== 'eventTask') {
+ this._isSync = false;
+ }
+ if (task.type === 'microTask' && task.data && task.data instanceof Promise) {
+ // check whether the promise is a chained promise
+ if ((task.data as any)[AsyncTestZoneSpec.symbolParentUnresolved] === true) {
+ // chained promise is being scheduled
+ this.unresolvedChainedPromiseCount--;
+ }
+ }
+ return delegate.scheduleTask(target, task);
+ }
+
+ onInvokeTask(
+ delegate: ZoneDelegate, current: Zone, target: Zone, task: Task, applyThis: any,
+ applyArgs: any) {
+ if (task.type !== 'eventTask') {
+ this._isSync = false;
+ }
+ return delegate.invokeTask(target, task, applyThis, applyArgs);
+ }
+
+ onCancelTask(delegate: ZoneDelegate, current: Zone, target: Zone, task: Task) {
+ if (task.type !== 'eventTask') {
+ this._isSync = false;
+ }
+ return delegate.cancelTask(target, task);
+ }
+
+ // Note - we need to use onInvoke at the moment to call finish when a test is
+ // fully synchronous. TODO(juliemr): remove this when the logic for
+ // onHasTask changes and it calls whenever the task queues are dirty.
+ // updated by(JiaLiPassion), only call finish callback when no task
+ // was scheduled/invoked/canceled.
+ onInvoke(
+ parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, delegate: Function,
+ applyThis: any, applyArgs?: any[], source?: string): any {
+ let previousTaskCounts: any = null;
+ try {
+ this._isSync = true;
+ return parentZoneDelegate.invoke(targetZone, delegate, applyThis, applyArgs, source);
+ } finally {
+ const afterTaskCounts: any = (parentZoneDelegate as any)._taskCounts;
+ if (this._isSync) {
+ this._finishCallbackIfDone();
+ }
+ }
+ }
+
+ onHandleError(
+ parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
+ error: any): boolean {
+ // Let the parent try to handle the error.
+ const result = parentZoneDelegate.handleError(targetZone, error);
+ if (result) {
+ this.failCallback(error);
+ this._alreadyErrored = true;
+ }
+ return false;
+ }
+
+ 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();
+ }
+ }
+ }
+
+ // Export the class so that new instances can be created with proper
+ // constructor params.
+ (Zone as any)['AsyncTestZoneSpec'] = AsyncTestZoneSpec;
+})(global);
diff --git a/packages/zone.js/lib/zone-spec/fake-async-test.ts b/packages/zone.js/lib/zone-spec/fake-async-test.ts
new file mode 100644
index 0000000000..408dae56ab
--- /dev/null
+++ b/packages/zone.js/lib/zone-spec/fake-async-test.ts
@@ -0,0 +1,560 @@
+/**
+ * @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
+ */
+
+(function(global: any) {
+ interface ScheduledFunction {
+ endTime: number;
+ id: number;
+ func: Function;
+ args: any[];
+ delay: number;
+ isPeriodic: boolean;
+ isRequestAnimationFrame: boolean;
+ }
+
+ interface MicroTaskScheduledFunction {
+ func: Function;
+ args?: any[];
+ target: any;
+ }
+
+ interface MacroTaskOptions {
+ source: string;
+ isPeriodic?: boolean;
+ callbackArgs?: any;
+ }
+
+ const OriginalDate = global.Date;
+ class FakeDate {
+ constructor() {
+ if (arguments.length === 0) {
+ const d = new OriginalDate();
+ d.setTime(FakeDate.now());
+ return d;
+ } else {
+ const args = Array.prototype.slice.call(arguments);
+ return new OriginalDate(...args);
+ }
+ }
+
+ static now() {
+ const fakeAsyncTestZoneSpec = Zone.current.get('FakeAsyncTestZoneSpec');
+ if (fakeAsyncTestZoneSpec) {
+ return fakeAsyncTestZoneSpec.getCurrentRealTime() + fakeAsyncTestZoneSpec.getCurrentTime();
+ }
+ return OriginalDate.now.apply(this, arguments);
+ }
+ }
+
+ (FakeDate as any).UTC = OriginalDate.UTC;
+ (FakeDate as any).parse = OriginalDate.parse;
+
+ // keep a reference for zone patched timer function
+ const timers = {
+ setTimeout: global.setTimeout,
+ setInterval: global.setInterval,
+ clearTimeout: global.clearTimeout,
+ clearInterval: global.clearInterval
+ };
+
+ class Scheduler {
+ // Next scheduler id.
+ public static nextId: number = 1;
+
+ // Scheduler queue with the tuple of end time and callback function - sorted by end time.
+ private _schedulerQueue: ScheduledFunction[] = [];
+ // Current simulated time in millis.
+ private _currentTime: number = 0;
+ // Current real time in millis.
+ private _currentRealTime: number = OriginalDate.now();
+
+ constructor() {}
+
+ getCurrentTime() { return this._currentTime; }
+
+ getCurrentRealTime() { return this._currentRealTime; }
+
+ setCurrentRealTime(realTime: number) { this._currentRealTime = realTime; }
+
+ scheduleFunction(
+ cb: Function, delay: number, args: any[] = [], isPeriodic: boolean = false,
+ isRequestAnimationFrame: boolean = false, id: number = -1): number {
+ let currentId: number = id < 0 ? Scheduler.nextId++ : id;
+ let endTime = this._currentTime + delay;
+
+ // Insert so that scheduler queue remains sorted by end time.
+ let newEntry: ScheduledFunction = {
+ endTime: endTime,
+ id: currentId,
+ func: cb,
+ args: args,
+ delay: delay,
+ isPeriodic: isPeriodic,
+ isRequestAnimationFrame: isRequestAnimationFrame
+ };
+ let i = 0;
+ for (; i < this._schedulerQueue.length; i++) {
+ let currentEntry = this._schedulerQueue[i];
+ if (newEntry.endTime < currentEntry.endTime) {
+ break;
+ }
+ }
+ this._schedulerQueue.splice(i, 0, newEntry);
+ return currentId;
+ }
+
+ removeScheduledFunctionWithId(id: number): void {
+ for (let i = 0; i < this._schedulerQueue.length; i++) {
+ if (this._schedulerQueue[i].id == id) {
+ this._schedulerQueue.splice(i, 1);
+ break;
+ }
+ }
+ }
+
+ tick(millis: number = 0, doTick?: (elapsed: number) => void): void {
+ let finalTime = this._currentTime + millis;
+ let lastCurrentTime = 0;
+ if (this._schedulerQueue.length === 0 && doTick) {
+ doTick(millis);
+ return;
+ }
+ while (this._schedulerQueue.length > 0) {
+ let current = this._schedulerQueue[0];
+ if (finalTime < current.endTime) {
+ // Done processing the queue since it's sorted by endTime.
+ break;
+ } else {
+ // Time to run scheduled function. Remove it from the head of queue.
+ let current = this._schedulerQueue.shift() !;
+ lastCurrentTime = this._currentTime;
+ this._currentTime = current.endTime;
+ if (doTick) {
+ doTick(this._currentTime - lastCurrentTime);
+ }
+ let retval = current.func.apply(
+ global, current.isRequestAnimationFrame ? [this._currentTime] : current.args);
+ if (!retval) {
+ // Uncaught exception in the current scheduled function. Stop processing the queue.
+ break;
+ }
+ }
+ }
+ lastCurrentTime = this._currentTime;
+ this._currentTime = finalTime;
+ if (doTick) {
+ doTick(this._currentTime - lastCurrentTime);
+ }
+ }
+
+ flush(limit = 20, flushPeriodic = false, doTick?: (elapsed: number) => void): number {
+ if (flushPeriodic) {
+ return this.flushPeriodic(doTick);
+ } else {
+ return this.flushNonPeriodic(limit, doTick);
+ }
+ }
+
+ private flushPeriodic(doTick?: (elapsed: number) => void): number {
+ if (this._schedulerQueue.length === 0) {
+ return 0;
+ }
+ // Find the last task currently queued in the scheduler queue and tick
+ // till that time.
+ const startTime = this._currentTime;
+ const lastTask = this._schedulerQueue[this._schedulerQueue.length - 1];
+ this.tick(lastTask.endTime - startTime, doTick);
+ return this._currentTime - startTime;
+ }
+
+ private flushNonPeriodic(limit: number, doTick?: (elapsed: number) => void): number {
+ const startTime = this._currentTime;
+ let lastCurrentTime = 0;
+ let count = 0;
+ while (this._schedulerQueue.length > 0) {
+ count++;
+ if (count > limit) {
+ throw new Error(
+ 'flush failed after reaching the limit of ' + limit +
+ ' tasks. Does your code use a polling timeout?');
+ }
+
+ // flush only non-periodic timers.
+ // If the only remaining tasks are periodic(or requestAnimationFrame), finish flushing.
+ if (this._schedulerQueue.filter(task => !task.isPeriodic && !task.isRequestAnimationFrame)
+ .length === 0) {
+ break;
+ }
+
+ const current = this._schedulerQueue.shift() !;
+ lastCurrentTime = this._currentTime;
+ this._currentTime = current.endTime;
+ if (doTick) {
+ // Update any secondary schedulers like Jasmine mock Date.
+ doTick(this._currentTime - lastCurrentTime);
+ }
+ const retval = current.func.apply(global, current.args);
+ if (!retval) {
+ // Uncaught exception in the current scheduled function. Stop processing the queue.
+ break;
+ }
+ }
+ return this._currentTime - startTime;
+ }
+ }
+
+ class FakeAsyncTestZoneSpec implements ZoneSpec {
+ static assertInZone(): void {
+ if (Zone.current.get('FakeAsyncTestZoneSpec') == null) {
+ throw new Error('The code should be running in the fakeAsync zone to call this function');
+ }
+ }
+
+ private _scheduler: Scheduler = new Scheduler();
+ private _microtasks: MicroTaskScheduledFunction[] = [];
+ private _lastError: Error|null = null;
+ private _uncaughtPromiseErrors: {rejection: any}[] =
+ (Promise as any)[(Zone as any).__symbol__('uncaughtPromiseErrors')];
+
+ pendingPeriodicTimers: number[] = [];
+ pendingTimers: number[] = [];
+
+ private patchDateLocked = false;
+
+ constructor(
+ namePrefix: string, private trackPendingRequestAnimationFrame = false,
+ private macroTaskOptions?: MacroTaskOptions[]) {
+ this.name = 'fakeAsyncTestZone for ' + namePrefix;
+ // in case user can't access the construction of FakeAsyncTestSpec
+ // user can also define macroTaskOptions by define a global variable.
+ if (!this.macroTaskOptions) {
+ this.macroTaskOptions = global[Zone.__symbol__('FakeAsyncTestMacroTask')];
+ }
+ }
+
+ private _fnAndFlush(fn: Function, completers: {onSuccess?: Function, onError?: Function}):
+ Function {
+ return (...args: any[]): boolean => {
+ fn.apply(global, args);
+
+ if (this._lastError === null) { // Success
+ if (completers.onSuccess != null) {
+ completers.onSuccess.apply(global);
+ }
+ // Flush microtasks only on success.
+ this.flushMicrotasks();
+ } else { // Failure
+ if (completers.onError != null) {
+ completers.onError.apply(global);
+ }
+ }
+ // Return true if there were no errors, false otherwise.
+ return this._lastError === null;
+ };
+ }
+
+ private static _removeTimer(timers: number[], id: number): void {
+ let index = timers.indexOf(id);
+ if (index > -1) {
+ timers.splice(index, 1);
+ }
+ }
+
+ private _dequeueTimer(id: number): Function {
+ return () => { FakeAsyncTestZoneSpec._removeTimer(this.pendingTimers, id); };
+ }
+
+ private _requeuePeriodicTimer(fn: Function, interval: number, args: any[], id: number):
+ Function {
+ return () => {
+ // Requeue the timer callback if it's not been canceled.
+ if (this.pendingPeriodicTimers.indexOf(id) !== -1) {
+ this._scheduler.scheduleFunction(fn, interval, args, true, false, id);
+ }
+ };
+ }
+
+ private _dequeuePeriodicTimer(id: number): Function {
+ return () => { FakeAsyncTestZoneSpec._removeTimer(this.pendingPeriodicTimers, id); };
+ }
+
+ private _setTimeout(fn: Function, delay: number, args: any[], isTimer = true): number {
+ let removeTimerFn = this._dequeueTimer(Scheduler.nextId);
+ // Queue the callback and dequeue the timer on success and error.
+ let cb = this._fnAndFlush(fn, {onSuccess: removeTimerFn, onError: removeTimerFn});
+ let id = this._scheduler.scheduleFunction(cb, delay, args, false, !isTimer);
+ if (isTimer) {
+ this.pendingTimers.push(id);
+ }
+ return id;
+ }
+
+ private _clearTimeout(id: number): void {
+ FakeAsyncTestZoneSpec._removeTimer(this.pendingTimers, id);
+ this._scheduler.removeScheduledFunctionWithId(id);
+ }
+
+ private _setInterval(fn: Function, interval: number, args: any[]): number {
+ let id = Scheduler.nextId;
+ let completers = {onSuccess: null as any, onError: this._dequeuePeriodicTimer(id)};
+ let cb = this._fnAndFlush(fn, completers);
+
+ // Use the callback created above to requeue on success.
+ completers.onSuccess = this._requeuePeriodicTimer(cb, interval, args, id);
+
+ // Queue the callback and dequeue the periodic timer only on error.
+ this._scheduler.scheduleFunction(cb, interval, args, true);
+ this.pendingPeriodicTimers.push(id);
+ return id;
+ }
+
+ private _clearInterval(id: number): void {
+ FakeAsyncTestZoneSpec._removeTimer(this.pendingPeriodicTimers, id);
+ this._scheduler.removeScheduledFunctionWithId(id);
+ }
+
+ private _resetLastErrorAndThrow(): void {
+ let error = this._lastError || this._uncaughtPromiseErrors[0];
+ this._uncaughtPromiseErrors.length = 0;
+ this._lastError = null;
+ throw error;
+ }
+
+ getCurrentTime() { return this._scheduler.getCurrentTime(); }
+
+ getCurrentRealTime() { return this._scheduler.getCurrentRealTime(); }
+
+ setCurrentRealTime(realTime: number) { this._scheduler.setCurrentRealTime(realTime); }
+
+ static patchDate() {
+ if (!!global[Zone.__symbol__('disableDatePatching')]) {
+ // we don't want to patch global Date
+ // because in some case, global Date
+ // is already being patched, we need to provide
+ // an option to let user still use their
+ // own version of Date.
+ return;
+ }
+
+ if (global['Date'] === FakeDate) {
+ // already patched
+ return;
+ }
+ global['Date'] = FakeDate;
+ FakeDate.prototype = OriginalDate.prototype;
+
+ // try check and reset timers
+ // because jasmine.clock().install() may
+ // have replaced the global timer
+ FakeAsyncTestZoneSpec.checkTimerPatch();
+ }
+
+ static resetDate() {
+ if (global['Date'] === FakeDate) {
+ global['Date'] = OriginalDate;
+ }
+ }
+
+ static checkTimerPatch() {
+ if (global.setTimeout !== timers.setTimeout) {
+ global.setTimeout = timers.setTimeout;
+ global.clearTimeout = timers.clearTimeout;
+ }
+ if (global.setInterval !== timers.setInterval) {
+ global.setInterval = timers.setInterval;
+ global.clearInterval = timers.clearInterval;
+ }
+ }
+
+ lockDatePatch() {
+ this.patchDateLocked = true;
+ FakeAsyncTestZoneSpec.patchDate();
+ }
+ unlockDatePatch() {
+ this.patchDateLocked = false;
+ FakeAsyncTestZoneSpec.resetDate();
+ }
+
+ tick(millis: number = 0, doTick?: (elapsed: number) => void): void {
+ FakeAsyncTestZoneSpec.assertInZone();
+ this.flushMicrotasks();
+ this._scheduler.tick(millis, doTick);
+ if (this._lastError !== null) {
+ this._resetLastErrorAndThrow();
+ }
+ }
+
+ flushMicrotasks(): void {
+ FakeAsyncTestZoneSpec.assertInZone();
+ const flushErrors = () => {
+ if (this._lastError !== null || this._uncaughtPromiseErrors.length) {
+ // If there is an error stop processing the microtask queue and rethrow the error.
+ this._resetLastErrorAndThrow();
+ }
+ };
+ while (this._microtasks.length > 0) {
+ let microtask = this._microtasks.shift() !;
+ microtask.func.apply(microtask.target, microtask.args);
+ }
+ flushErrors();
+ }
+
+ flush(limit?: number, flushPeriodic?: boolean, doTick?: (elapsed: number) => void): number {
+ FakeAsyncTestZoneSpec.assertInZone();
+ this.flushMicrotasks();
+ const elapsed = this._scheduler.flush(limit, flushPeriodic, doTick);
+ if (this._lastError !== null) {
+ this._resetLastErrorAndThrow();
+ }
+ return elapsed;
+ }
+
+ // ZoneSpec implementation below.
+
+ name: string;
+
+ properties: {[key: string]: any} = {'FakeAsyncTestZoneSpec': this};
+
+ onScheduleTask(delegate: ZoneDelegate, current: Zone, target: Zone, task: Task): Task {
+ switch (task.type) {
+ case 'microTask':
+ let args = task.data && (task.data as any).args;
+ // should pass additional arguments to callback if have any
+ // currently we know process.nextTick will have such additional
+ // arguments
+ let additionalArgs: any[]|undefined;
+ if (args) {
+ let callbackIndex = (task.data as any).cbIdx;
+ if (typeof args.length === 'number' && args.length > callbackIndex + 1) {
+ additionalArgs = Array.prototype.slice.call(args, callbackIndex + 1);
+ }
+ }
+ this._microtasks.push({
+ func: task.invoke,
+ args: additionalArgs,
+ target: task.data && (task.data as any).target
+ });
+ break;
+ case 'macroTask':
+ switch (task.source) {
+ case 'setTimeout':
+ task.data !['handleId'] = this._setTimeout(
+ task.invoke, task.data !['delay'] !,
+ Array.prototype.slice.call((task.data as any)['args'], 2));
+ break;
+ case 'setImmediate':
+ task.data !['handleId'] = this._setTimeout(
+ task.invoke, 0, Array.prototype.slice.call((task.data as any)['args'], 1));
+ break;
+ case 'setInterval':
+ task.data !['handleId'] = this._setInterval(
+ task.invoke, task.data !['delay'] !,
+ Array.prototype.slice.call((task.data as any)['args'], 2));
+ break;
+ case 'XMLHttpRequest.send':
+ throw new Error(
+ 'Cannot make XHRs from within a fake async test. Request URL: ' +
+ (task.data as any)['url']);
+ case 'requestAnimationFrame':
+ case 'webkitRequestAnimationFrame':
+ case 'mozRequestAnimationFrame':
+ // Simulate a requestAnimationFrame by using a setTimeout with 16 ms.
+ // (60 frames per second)
+ task.data !['handleId'] = this._setTimeout(
+ task.invoke, 16, (task.data as any)['args'],
+ this.trackPendingRequestAnimationFrame);
+ break;
+ default:
+ // user can define which macroTask they want to support by passing
+ // macroTaskOptions
+ const macroTaskOption = this.findMacroTaskOption(task);
+ if (macroTaskOption) {
+ const args = task.data && (task.data as any)['args'];
+ const delay = args && args.length > 1 ? args[1] : 0;
+ let callbackArgs =
+ macroTaskOption.callbackArgs ? macroTaskOption.callbackArgs : args;
+ if (!!macroTaskOption.isPeriodic) {
+ // periodic macroTask, use setInterval to simulate
+ task.data !['handleId'] = this._setInterval(task.invoke, delay, callbackArgs);
+ task.data !.isPeriodic = true;
+ } else {
+ // not periodic, use setTimeout to simulate
+ task.data !['handleId'] = this._setTimeout(task.invoke, delay, callbackArgs);
+ }
+ break;
+ }
+ throw new Error('Unknown macroTask scheduled in fake async test: ' + task.source);
+ }
+ break;
+ case 'eventTask':
+ task = delegate.scheduleTask(target, task);
+ break;
+ }
+ return task;
+ }
+
+ onCancelTask(delegate: ZoneDelegate, current: Zone, target: Zone, task: Task): any {
+ switch (task.source) {
+ case 'setTimeout':
+ case 'requestAnimationFrame':
+ case 'webkitRequestAnimationFrame':
+ case 'mozRequestAnimationFrame':
+ return this._clearTimeout(task.data !['handleId']);
+ case 'setInterval':
+ return this._clearInterval(task.data !['handleId']);
+ default:
+ // user can define which macroTask they want to support by passing
+ // macroTaskOptions
+ const macroTaskOption = this.findMacroTaskOption(task);
+ if (macroTaskOption) {
+ const handleId: number = task.data !['handleId'];
+ return macroTaskOption.isPeriodic ? this._clearInterval(handleId) :
+ this._clearTimeout(handleId);
+ }
+ return delegate.cancelTask(target, task);
+ }
+ }
+
+ onInvoke(
+ delegate: ZoneDelegate, current: Zone, target: Zone, callback: Function, applyThis: any,
+ applyArgs?: any[], source?: string): any {
+ try {
+ FakeAsyncTestZoneSpec.patchDate();
+ return delegate.invoke(target, callback, applyThis, applyArgs, source);
+ } finally {
+ if (!this.patchDateLocked) {
+ FakeAsyncTestZoneSpec.resetDate();
+ }
+ }
+ }
+
+ findMacroTaskOption(task: Task) {
+ if (!this.macroTaskOptions) {
+ return null;
+ }
+ for (let i = 0; i < this.macroTaskOptions.length; i++) {
+ const macroTaskOption = this.macroTaskOptions[i];
+ if (macroTaskOption.source === task.source) {
+ return macroTaskOption;
+ }
+ }
+ return null;
+ }
+
+ onHandleError(
+ parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
+ error: any): boolean {
+ this._lastError = error;
+ return false; // Don't propagate error to parent zone.
+ }
+ }
+
+ // Export the class so that new instances can be created with proper
+ // constructor params.
+ (Zone as any)['FakeAsyncTestZoneSpec'] = FakeAsyncTestZoneSpec;
+})(global);
diff --git a/packages/zone.js/lib/zone-spec/long-stack-trace.ts b/packages/zone.js/lib/zone-spec/long-stack-trace.ts
new file mode 100644
index 0000000000..e60c704f6e
--- /dev/null
+++ b/packages/zone.js/lib/zone-spec/long-stack-trace.ts
@@ -0,0 +1,183 @@
+/**
+ * @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
+ */
+/**
+ * @fileoverview
+ * @suppress {globalThis}
+ */
+
+const NEWLINE = '\n';
+const IGNORE_FRAMES: {[k: string]: true} = {};
+const creationTrace = '__creationTrace__';
+const ERROR_TAG = 'STACKTRACE TRACKING';
+const SEP_TAG = '__SEP_TAG__';
+let sepTemplate: string = SEP_TAG + '@[native]';
+
+class LongStackTrace {
+ error: Error = getStacktrace();
+ timestamp: Date = new Date();
+}
+
+function getStacktraceWithUncaughtError(): Error {
+ return new Error(ERROR_TAG);
+}
+
+function getStacktraceWithCaughtError(): Error {
+ try {
+ throw getStacktraceWithUncaughtError();
+ } catch (err) {
+ return err;
+ }
+}
+
+// Some implementations of exception handling don't create a stack trace if the exception
+// isn't thrown, however it's faster not to actually throw the exception.
+const error = getStacktraceWithUncaughtError();
+const caughtError = getStacktraceWithCaughtError();
+const getStacktrace = error.stack ?
+ getStacktraceWithUncaughtError :
+ (caughtError.stack ? getStacktraceWithCaughtError : getStacktraceWithUncaughtError);
+
+function getFrames(error: Error): string[] {
+ return error.stack ? error.stack.split(NEWLINE) : [];
+}
+
+function addErrorStack(lines: string[], error: Error): void {
+ let trace: string[] = getFrames(error);
+ for (let i = 0; i < trace.length; i++) {
+ const frame = trace[i];
+ // Filter out the Frames which are part of stack capturing.
+ if (!IGNORE_FRAMES.hasOwnProperty(frame)) {
+ lines.push(trace[i]);
+ }
+ }
+}
+
+function renderLongStackTrace(frames: LongStackTrace[], stack?: string): string {
+ const longTrace: string[] = [stack ? stack.trim() : ''];
+
+ if (frames) {
+ let timestamp = new Date().getTime();
+ for (let i = 0; i < frames.length; i++) {
+ const traceFrames: LongStackTrace = frames[i];
+ const lastTime = traceFrames.timestamp;
+ let separator =
+ `____________________Elapsed ${timestamp - lastTime.getTime()} ms; At: ${lastTime}`;
+ separator = separator.replace(/[^\w\d]/g, '_');
+ longTrace.push(sepTemplate.replace(SEP_TAG, separator));
+ addErrorStack(longTrace, traceFrames.error);
+
+ timestamp = lastTime.getTime();
+ }
+ }
+
+ return longTrace.join(NEWLINE);
+}
+
+(Zone as any)['longStackTraceZoneSpec'] = {
+ name: 'long-stack-trace',
+ longStackTraceLimit: 10, // Max number of task to keep the stack trace for.
+ // add a getLongStackTrace method in spec to
+ // handle handled reject promise error.
+ getLongStackTrace: function(error: Error): string |
+ undefined {
+ if (!error) {
+ return undefined;
+ }
+ const trace = (error as any)[(Zone as any).__symbol__('currentTaskTrace')];
+ if (!trace) {
+ return error.stack;
+ }
+ return renderLongStackTrace(trace, error.stack);
+ },
+
+ onScheduleTask: function(
+ parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task): any {
+ if (Error.stackTraceLimit > 0) {
+ // if Error.stackTraceLimit is 0, means stack trace
+ // is disabled, so we don't need to generate long stack trace
+ // this will improve performance in some test(some test will
+ // set stackTraceLimit to 0, https://github.com/angular/zone.js/issues/698
+ const currentTask = Zone.currentTask;
+ let trace = currentTask && currentTask.data && (currentTask.data as any)[creationTrace] || [];
+ trace = [new LongStackTrace()].concat(trace);
+ if (trace.length > this.longStackTraceLimit) {
+ trace.length = this.longStackTraceLimit;
+ }
+ if (!task.data) task.data = {};
+ if (task.type === 'eventTask') {
+ // Fix issue https://github.com/angular/zone.js/issues/1195,
+ // For event task of browser, by default, all task will share a
+ // singleton instance of data object, we should create a new one here
+
+ // The cast to `any` is required to workaround a closure bug which wrongly applies
+ // URL sanitization rules to .data access.
+ (task.data as any) = {...(task.data as any)};
+ }
+ (task.data as any)[creationTrace] = trace;
+ }
+ return parentZoneDelegate.scheduleTask(targetZone, task);
+ },
+
+ onHandleError: function(
+ parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, error: any): boolean {
+ if (Error.stackTraceLimit > 0) {
+ // if Error.stackTraceLimit is 0, means stack trace
+ // is disabled, so we don't need to generate long stack trace
+ // this will improve performance in some test(some test will
+ // set stackTraceLimit to 0, https://github.com/angular/zone.js/issues/698
+ const parentTask = Zone.currentTask || error.task;
+ if (error instanceof Error && parentTask) {
+ const longStack =
+ renderLongStackTrace(parentTask.data && parentTask.data[creationTrace], error.stack);
+ try {
+ error.stack = (error as any).longStack = longStack;
+ } catch (err) {
+ }
+ }
+ }
+ return parentZoneDelegate.handleError(targetZone, error);
+ }
+};
+
+function captureStackTraces(stackTraces: string[][], count: number): void {
+ if (count > 0) {
+ stackTraces.push(getFrames((new LongStackTrace()).error));
+ captureStackTraces(stackTraces, count - 1);
+ }
+}
+
+function computeIgnoreFrames() {
+ if (Error.stackTraceLimit <= 0) {
+ return;
+ }
+ const frames: string[][] = [];
+ captureStackTraces(frames, 2);
+ const frames1 = frames[0];
+ const frames2 = frames[1];
+ for (let i = 0; i < frames1.length; i++) {
+ const frame1 = frames1[i];
+ if (frame1.indexOf(ERROR_TAG) == -1) {
+ let match = frame1.match(/^\s*at\s+/);
+ if (match) {
+ sepTemplate = match[0] + SEP_TAG + ' (http://localhost)';
+ break;
+ }
+ }
+ }
+
+ for (let i = 0; i < frames1.length; i++) {
+ const frame1 = frames1[i];
+ const frame2 = frames2[i];
+ if (frame1 === frame2) {
+ IGNORE_FRAMES[frame1] = true;
+ } else {
+ break;
+ }
+ }
+}
+computeIgnoreFrames();
diff --git a/packages/zone.js/lib/zone-spec/proxy.ts b/packages/zone.js/lib/zone-spec/proxy.ts
new file mode 100644
index 0000000000..36133c52f8
--- /dev/null
+++ b/packages/zone.js/lib/zone-spec/proxy.ts
@@ -0,0 +1,195 @@
+/**
+ * @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
+ */
+class ProxyZoneSpec implements ZoneSpec {
+ name: string = 'ProxyZone';
+
+ private _delegateSpec: ZoneSpec|null = null;
+
+ properties: {[k: string]: any} = {'ProxyZoneSpec': this};
+ propertyKeys: string[]|null = null;
+
+ lastTaskState: HasTaskState|null = null;
+ isNeedToTriggerHasTask = false;
+
+ private tasks: Task[] = [];
+
+ static get(): ProxyZoneSpec { return Zone.current.get('ProxyZoneSpec'); }
+
+ static isLoaded(): boolean { return ProxyZoneSpec.get() instanceof ProxyZoneSpec; }
+
+ static assertPresent(): ProxyZoneSpec {
+ if (!ProxyZoneSpec.isLoaded()) {
+ throw new Error(`Expected to be running in 'ProxyZone', but it was not found.`);
+ }
+ return ProxyZoneSpec.get();
+ }
+
+ constructor(private defaultSpecDelegate: ZoneSpec|null = null) {
+ this.setDelegate(defaultSpecDelegate);
+ }
+
+ setDelegate(delegateSpec: ZoneSpec|null) {
+ const isNewDelegate = this._delegateSpec !== delegateSpec;
+ this._delegateSpec = delegateSpec;
+ this.propertyKeys && this.propertyKeys.forEach((key) => delete this.properties[key]);
+ this.propertyKeys = null;
+ if (delegateSpec && delegateSpec.properties) {
+ this.propertyKeys = Object.keys(delegateSpec.properties);
+ this.propertyKeys.forEach((k) => this.properties[k] = delegateSpec.properties ![k]);
+ }
+ // if set a new delegateSpec, shoulde check whether need to
+ // trigger hasTask or not
+ if (isNewDelegate && this.lastTaskState &&
+ (this.lastTaskState.macroTask || this.lastTaskState.microTask)) {
+ this.isNeedToTriggerHasTask = true;
+ }
+ }
+
+ getDelegate() { return this._delegateSpec; }
+
+
+ resetDelegate() {
+ const delegateSpec = this.getDelegate();
+ this.setDelegate(this.defaultSpecDelegate);
+ }
+
+ tryTriggerHasTask(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone) {
+ if (this.isNeedToTriggerHasTask && this.lastTaskState) {
+ // last delegateSpec has microTask or macroTask
+ // should call onHasTask in current delegateSpec
+ this.isNeedToTriggerHasTask = false;
+ this.onHasTask(parentZoneDelegate, currentZone, targetZone, this.lastTaskState);
+ }
+ }
+
+ removeFromTasks(task: Task) {
+ if (!this.tasks) {
+ return;
+ }
+ for (let i = 0; i < this.tasks.length; i++) {
+ if (this.tasks[i] === task) {
+ this.tasks.splice(i, 1);
+ return;
+ }
+ }
+ }
+
+ getAndClearPendingTasksInfo() {
+ if (this.tasks.length === 0) {
+ return '';
+ }
+ const taskInfo = this.tasks.map((task: Task) => {
+ const dataInfo = task.data &&
+ Object.keys(task.data)
+ .map((key: string) => { return key + ':' + (task.data as any)[key]; })
+ .join(',');
+ return `type: ${task.type}, source: ${task.source}, args: {${dataInfo}}`;
+ });
+ const pendingTasksInfo = '--Pendng async tasks are: [' + taskInfo + ']';
+ // clear tasks
+ this.tasks = [];
+
+ return pendingTasksInfo;
+ }
+
+ onFork(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, zoneSpec: ZoneSpec):
+ Zone {
+ if (this._delegateSpec && this._delegateSpec.onFork) {
+ return this._delegateSpec.onFork(parentZoneDelegate, currentZone, targetZone, zoneSpec);
+ } else {
+ return parentZoneDelegate.fork(targetZone, zoneSpec);
+ }
+ }
+
+
+ onIntercept(
+ parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, delegate: Function,
+ source: string): Function {
+ if (this._delegateSpec && this._delegateSpec.onIntercept) {
+ return this._delegateSpec.onIntercept(
+ parentZoneDelegate, currentZone, targetZone, delegate, source);
+ } else {
+ return parentZoneDelegate.intercept(targetZone, delegate, source);
+ }
+ }
+
+
+ onInvoke(
+ parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, delegate: Function,
+ applyThis: any, applyArgs?: any[], source?: string): any {
+ this.tryTriggerHasTask(parentZoneDelegate, currentZone, targetZone);
+ if (this._delegateSpec && this._delegateSpec.onInvoke) {
+ return this._delegateSpec.onInvoke(
+ parentZoneDelegate, currentZone, targetZone, delegate, applyThis, applyArgs, source);
+ } else {
+ return parentZoneDelegate.invoke(targetZone, delegate, applyThis, applyArgs, source);
+ }
+ }
+
+ onHandleError(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, error: any):
+ boolean {
+ if (this._delegateSpec && this._delegateSpec.onHandleError) {
+ return this._delegateSpec.onHandleError(parentZoneDelegate, currentZone, targetZone, error);
+ } else {
+ return parentZoneDelegate.handleError(targetZone, error);
+ }
+ }
+
+ onScheduleTask(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task):
+ Task {
+ if (task.type !== 'eventTask') {
+ this.tasks.push(task);
+ }
+ if (this._delegateSpec && this._delegateSpec.onScheduleTask) {
+ return this._delegateSpec.onScheduleTask(parentZoneDelegate, currentZone, targetZone, task);
+ } else {
+ return parentZoneDelegate.scheduleTask(targetZone, task);
+ }
+ }
+
+ onInvokeTask(
+ parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task,
+ applyThis: any, applyArgs: any): any {
+ if (task.type !== 'eventTask') {
+ this.removeFromTasks(task);
+ }
+ this.tryTriggerHasTask(parentZoneDelegate, currentZone, targetZone);
+ if (this._delegateSpec && this._delegateSpec.onInvokeTask) {
+ return this._delegateSpec.onInvokeTask(
+ parentZoneDelegate, currentZone, targetZone, task, applyThis, applyArgs);
+ } else {
+ return parentZoneDelegate.invokeTask(targetZone, task, applyThis, applyArgs);
+ }
+ }
+
+ onCancelTask(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task):
+ any {
+ if (task.type !== 'eventTask') {
+ this.removeFromTasks(task);
+ }
+ this.tryTriggerHasTask(parentZoneDelegate, currentZone, targetZone);
+ if (this._delegateSpec && this._delegateSpec.onCancelTask) {
+ return this._delegateSpec.onCancelTask(parentZoneDelegate, currentZone, targetZone, task);
+ } else {
+ return parentZoneDelegate.cancelTask(targetZone, task);
+ }
+ }
+
+ onHasTask(delegate: ZoneDelegate, current: Zone, target: Zone, hasTaskState: HasTaskState): void {
+ this.lastTaskState = hasTaskState;
+ if (this._delegateSpec && this._delegateSpec.onHasTask) {
+ this._delegateSpec.onHasTask(delegate, current, target, hasTaskState);
+ } else {
+ delegate.hasTask(target, hasTaskState);
+ }
+ }
+}
+
+// Export the class so that new instances can be created with proper
+// constructor params.
+(Zone as any)['ProxyZoneSpec'] = ProxyZoneSpec;
diff --git a/packages/zone.js/lib/zone-spec/sync-test.ts b/packages/zone.js/lib/zone-spec/sync-test.ts
new file mode 100644
index 0000000000..b921dd1628
--- /dev/null
+++ b/packages/zone.js/lib/zone-spec/sync-test.ts
@@ -0,0 +1,33 @@
+/**
+ * @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
+ */
+
+class SyncTestZoneSpec implements ZoneSpec {
+ runZone = Zone.current;
+
+ constructor(namePrefix: string) { this.name = 'syncTestZone for ' + namePrefix; }
+
+ // ZoneSpec implementation below.
+
+ name: string;
+
+ onScheduleTask(delegate: ZoneDelegate, current: Zone, target: Zone, task: Task): Task {
+ switch (task.type) {
+ case 'microTask':
+ case 'macroTask':
+ throw new Error(`Cannot call ${task.source} from within a sync test.`);
+ case 'eventTask':
+ task = delegate.scheduleTask(target, task);
+ break;
+ }
+ return task;
+ }
+}
+
+// Export the class so that new instances can be created with proper
+// constructor params.
+(Zone as any)['SyncTestZoneSpec'] = SyncTestZoneSpec;
diff --git a/packages/zone.js/lib/zone-spec/task-tracking.ts b/packages/zone.js/lib/zone-spec/task-tracking.ts
new file mode 100644
index 0000000000..ce43a317ed
--- /dev/null
+++ b/packages/zone.js/lib/zone-spec/task-tracking.ts
@@ -0,0 +1,80 @@
+/**
+ * @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
+ */
+
+/**
+ * A `TaskTrackingZoneSpec` allows one to track all outstanding Tasks.
+ *
+ * This is useful in tests. For example to see which tasks are preventing a test from completing
+ * or an automated way of releasing all of the event listeners at the end of the test.
+ */
+class TaskTrackingZoneSpec implements ZoneSpec {
+ name = 'TaskTrackingZone';
+ microTasks: Task[] = [];
+ macroTasks: Task[] = [];
+ eventTasks: Task[] = [];
+ properties: {[key: string]: any} = {'TaskTrackingZone': this};
+
+ static get() { return Zone.current.get('TaskTrackingZone'); }
+
+ private getTasksFor(type: string): Task[] {
+ switch (type) {
+ case 'microTask':
+ return this.microTasks;
+ case 'macroTask':
+ return this.macroTasks;
+ case 'eventTask':
+ return this.eventTasks;
+ }
+ throw new Error('Unknown task format: ' + type);
+ }
+
+ onScheduleTask(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task):
+ Task {
+ (task as any)['creationLocation'] = new Error(`Task '${task.type}' from '${task.source}'.`);
+ const tasks = this.getTasksFor(task.type);
+ tasks.push(task);
+ return parentZoneDelegate.scheduleTask(targetZone, task);
+ }
+
+ onCancelTask(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task):
+ any {
+ const tasks = this.getTasksFor(task.type);
+ for (let i = 0; i < tasks.length; i++) {
+ if (tasks[i] == task) {
+ tasks.splice(i, 1);
+ break;
+ }
+ }
+ return parentZoneDelegate.cancelTask(targetZone, task);
+ }
+
+ onInvokeTask(
+ parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task,
+ applyThis: any, applyArgs: any): any {
+ if (task.type === 'eventTask')
+ return parentZoneDelegate.invokeTask(targetZone, task, applyThis, applyArgs);
+ const tasks = this.getTasksFor(task.type);
+ for (let i = 0; i < tasks.length; i++) {
+ if (tasks[i] == task) {
+ tasks.splice(i, 1);
+ break;
+ }
+ }
+ return parentZoneDelegate.invokeTask(targetZone, task, applyThis, applyArgs);
+ }
+
+ clearEvents() {
+ while (this.eventTasks.length) {
+ Zone.current.cancelTask(this.eventTasks[0]);
+ }
+ }
+}
+
+// Export the class so that new instances can be created with proper
+// constructor params.
+(Zone as any)['TaskTrackingZoneSpec'] = TaskTrackingZoneSpec;
diff --git a/packages/zone.js/lib/zone-spec/wtf.ts b/packages/zone.js/lib/zone-spec/wtf.ts
new file mode 100644
index 0000000000..fd46712713
--- /dev/null
+++ b/packages/zone.js/lib/zone-spec/wtf.ts
@@ -0,0 +1,161 @@
+/**
+ * @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
+ */
+/**
+ * @fileoverview
+ * @suppress {missingRequire}
+ */
+
+(function(global: any) {
+ interface Wtf {
+ trace: WtfTrace;
+ }
+ interface WtfScope {}
+ interface WtfRange {}
+ interface WtfTrace {
+ events: WtfEvents;
+ leaveScope(scope: WtfScope, returnValue?: any): void;
+ beginTimeRange(rangeType: string, action: string): WtfRange;
+ endTimeRange(range: WtfRange): void;
+ }
+ interface WtfEvents {
+ createScope(signature: string, flags?: any): WtfScopeFn;
+ createInstance(signature: string, flags?: any): WtfEventFn;
+ }
+
+ type WtfScopeFn = (...args: any[]) => WtfScope;
+ type WtfEventFn = (...args: any[]) => any;
+
+ // Detect and setup WTF.
+ let wtfTrace: WtfTrace|null = null;
+ let wtfEvents: WtfEvents|null = null;
+ const wtfEnabled: boolean = (function(): boolean {
+ const wtf: Wtf = global['wtf'];
+ if (wtf) {
+ wtfTrace = wtf.trace;
+ if (wtfTrace) {
+ wtfEvents = wtfTrace.events;
+ return true;
+ }
+ }
+ return false;
+ })();
+
+ class WtfZoneSpec implements ZoneSpec {
+ name: string = 'WTF';
+
+ static forkInstance =
+ wtfEnabled? wtfEvents !.createInstance('Zone:fork(ascii zone, ascii newZone)'): null;
+ static scheduleInstance: {[key: string]: WtfEventFn} = {};
+ static cancelInstance: {[key: string]: WtfEventFn} = {};
+ static invokeScope: {[key: string]: WtfEventFn} = {};
+ static invokeTaskScope: {[key: string]: WtfEventFn} = {};
+
+ onFork(
+ parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
+ zoneSpec: ZoneSpec): Zone {
+ const retValue = parentZoneDelegate.fork(targetZone, zoneSpec);
+ WtfZoneSpec.forkInstance !(zonePathName(targetZone), retValue.name);
+ return retValue;
+ }
+
+ onInvoke(
+ parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, delegate: Function,
+ applyThis: any, applyArgs?: any[], source?: string): any {
+ const src = source || 'unknown';
+ let scope = WtfZoneSpec.invokeScope[src];
+ if (!scope) {
+ scope = WtfZoneSpec.invokeScope[src] =
+ wtfEvents !.createScope(`Zone:invoke:${source}(ascii zone)`);
+ }
+ return wtfTrace !.leaveScope(
+ scope(zonePathName(targetZone)),
+ parentZoneDelegate.invoke(targetZone, delegate, applyThis, applyArgs, source));
+ }
+
+
+ onHandleError(
+ parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
+ error: any): boolean {
+ return parentZoneDelegate.handleError(targetZone, error);
+ }
+
+ onScheduleTask(
+ parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task): any {
+ const key = task.type + ':' + task.source;
+ let instance = WtfZoneSpec.scheduleInstance[key];
+ if (!instance) {
+ instance = WtfZoneSpec.scheduleInstance[key] =
+ wtfEvents !.createInstance(`Zone:schedule:${key}(ascii zone, any data)`);
+ }
+ const retValue = parentZoneDelegate.scheduleTask(targetZone, task);
+ instance(zonePathName(targetZone), shallowObj(task.data, 2));
+ return retValue;
+ }
+
+
+ onInvokeTask(
+ parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task,
+ applyThis?: any, applyArgs?: any[]): any {
+ const source = task.source;
+ let scope = WtfZoneSpec.invokeTaskScope[source];
+ if (!scope) {
+ scope = WtfZoneSpec.invokeTaskScope[source] =
+ wtfEvents !.createScope(`Zone:invokeTask:${source}(ascii zone)`);
+ }
+ return wtfTrace !.leaveScope(
+ scope(zonePathName(targetZone)),
+ parentZoneDelegate.invokeTask(targetZone, task, applyThis, applyArgs));
+ }
+
+ onCancelTask(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task):
+ any {
+ const key = task.source;
+ let instance = WtfZoneSpec.cancelInstance[key];
+ if (!instance) {
+ instance = WtfZoneSpec.cancelInstance[key] =
+ wtfEvents !.createInstance(`Zone:cancel:${key}(ascii zone, any options)`);
+ }
+ const retValue = parentZoneDelegate.cancelTask(targetZone, task);
+ instance(zonePathName(targetZone), shallowObj(task.data, 2));
+ return retValue;
+ }
+ }
+
+ function shallowObj(obj: {[k: string]: any} | undefined, depth: number): any {
+ if (!obj || !depth) return null;
+ const out: {[k: string]: any} = {};
+ for (const key in obj) {
+ if (obj.hasOwnProperty(key)) {
+ let value = obj[key];
+ switch (typeof value) {
+ case 'object':
+ const name = value && value.constructor && (value.constructor).name;
+ value = name == (Object).name ? shallowObj(value, depth - 1) : name;
+ break;
+ case 'function':
+ value = value.name || undefined;
+ break;
+ }
+ out[key] = value;
+ }
+ }
+ return out;
+ }
+
+ function zonePathName(zone: Zone) {
+ let name: string = zone.name;
+ let localZone = zone.parent;
+ while (localZone != null) {
+ name = localZone.name + '::' + name;
+ localZone = localZone.parent;
+ }
+ return name;
+ }
+
+ (Zone as any)['wtfZoneSpec'] = !wtfEnabled ? null : new WtfZoneSpec();
+})(global);
diff --git a/packages/zone.js/lib/zone.ts b/packages/zone.js/lib/zone.ts
new file mode 100644
index 0000000000..2ba94f3b8c
--- /dev/null
+++ b/packages/zone.js/lib/zone.ts
@@ -0,0 +1,1404 @@
+/**
+ * @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
+ */
+
+/**
+ * Suppress closure compiler errors about unknown 'global' variable
+ * @fileoverview
+ * @suppress {undefinedVars}
+ */
+
+/**
+ * Zone is a mechanism for intercepting and keeping track of asynchronous work.
+ *
+ * A Zone is a global object which is configured with rules about how to intercept and keep track
+ * of the asynchronous callbacks. Zone has these responsibilities:
+ *
+ * 1. Intercept asynchronous task scheduling
+ * 2. Wrap callbacks for error-handling and zone tracking across async operations.
+ * 3. Provide a way to attach data to zones
+ * 4. Provide a context specific last frame error handling
+ * 5. (Intercept blocking methods)
+ *
+ * A zone by itself does not do anything, instead it relies on some other code to route existing
+ * platform API through it. (The zone library ships with code which monkey patches all of the
+ * browsers's asynchronous API and redirects them through the zone for interception.)
+ *
+ * In its simplest form a zone allows one to intercept the scheduling and calling of asynchronous
+ * operations, and execute additional code before as well as after the asynchronous task. The rules
+ * of interception are configured using [ZoneConfig]. There can be many different zone instances in
+ * a system, but only one zone is active at any given time which can be retrieved using
+ * [Zone#current].
+ *
+ *
+ *
+ * ## Callback Wrapping
+ *
+ * An important aspect of the zones is that they should persist across asynchronous operations. To
+ * achieve this, when a future work is scheduled through async API, it is necessary to capture, and
+ * subsequently restore the current zone. For example if a code is running in zone `b` and it
+ * invokes `setTimeout` to scheduleTask work later, the `setTimeout` method needs to 1) capture the
+ * current zone and 2) wrap the `wrapCallback` in code which will restore the current zone `b` once
+ * the wrapCallback executes. In this way the rules which govern the current code are preserved in
+ * all future asynchronous tasks. There could be a different zone `c` which has different rules and
+ * is associated with different asynchronous tasks. As these tasks are processed, each asynchronous
+ * wrapCallback correctly restores the correct zone, as well as preserves the zone for future
+ * asynchronous callbacks.
+ *
+ * Example: Suppose a browser page consist of application code as well as third-party
+ * advertisement code. (These two code bases are independent, developed by different mutually
+ * unaware developers.) The application code may be interested in doing global error handling and
+ * so it configures the `app` zone to send all of the errors to the server for analysis, and then
+ * executes the application in the `app` zone. The advertising code is interested in the same
+ * error processing but it needs to send the errors to a different third-party. So it creates the
+ * `ads` zone with a different error handler. Now both advertising as well as application code
+ * create many asynchronous operations, but the [Zone] will ensure that all of the asynchronous
+ * operations created from the application code will execute in `app` zone with its error
+ * handler and all of the advertisement code will execute in the `ads` zone with its error handler.
+ * This will not only work for the async operations created directly, but also for all subsequent
+ * asynchronous operations.
+ *
+ * If you think of chain of asynchronous operations as a thread of execution (bit of a stretch)
+ * then [Zone#current] will act as a thread local variable.
+ *
+ *
+ *
+ * ## Asynchronous operation scheduling
+ *
+ * In addition to wrapping the callbacks to restore the zone, all operations which cause a
+ * scheduling of work for later are routed through the current zone which is allowed to intercept
+ * them by adding work before or after the wrapCallback as well as using different means of
+ * achieving the request. (Useful for unit testing, or tracking of requests). In some instances
+ * such as `setTimeout` the wrapping of the wrapCallback and scheduling is done in the same
+ * wrapCallback, but there are other examples such as `Promises` where the `then` wrapCallback is
+ * wrapped, but the execution of `then` is triggered by `Promise` scheduling `resolve` work.
+ *
+ * Fundamentally there are three kinds of tasks which can be scheduled:
+ *
+ * 1. [MicroTask] used for doing work right after the current task. This is non-cancelable which is
+ * guaranteed to run exactly once and immediately.
+ * 2. [MacroTask] used for doing work later. Such as `setTimeout`. This is typically cancelable
+ * which is guaranteed to execute at least once after some well understood delay.
+ * 3. [EventTask] used for listening on some future event. This may execute zero or more times, with
+ * an unknown delay.
+ *
+ * Each asynchronous API is modeled and routed through one of these APIs.
+ *
+ *
+ * ### [MicroTask]
+ *
+ * [MicroTask]s represent work which will be done in current VM turn as soon as possible, before VM
+ * yielding.
+ *
+ *
+ * ### [MacroTask]
+ *
+ * [MacroTask]s represent work which will be done after some delay. (Sometimes the delay is
+ * approximate such as on next available animation frame). Typically these methods include:
+ * `setTimeout`, `setImmediate`, `setInterval`, `requestAnimationFrame`, and all browser specific
+ * variants.
+ *
+ *
+ * ### [EventTask]
+ *
+ * [EventTask]s represent a request to create a listener on an event. Unlike the other task
+ * events they may never be executed, but typically execute more than once. There is no queue of
+ * events, rather their callbacks are unpredictable both in order and time.
+ *
+ *
+ * ## Global Error Handling
+ *
+ *
+ * ## Composability
+ *
+ * Zones can be composed together through [Zone.fork()]. A child zone may create its own set of
+ * rules. A child zone is expected to either:
+ *
+ * 1. Delegate the interception to a parent zone, and optionally add before and after wrapCallback
+ * hooks.
+ * 2. Process the request itself without delegation.
+ *
+ * Composability allows zones to keep their concerns clean. For example a top most zone may choose
+ * to handle error handling, while child zones may choose to do user action tracking.
+ *
+ *
+ * ## Root Zone
+ *
+ * At the start the browser will run in a special root zone, which is configured to behave exactly
+ * like the platform, making any existing code which is not zone-aware behave as expected. All
+ * zones are children of the root zone.
+ *
+ */
+interface Zone {
+ /**
+ *
+ * @returns {Zone} The parent Zone.
+ */
+ parent: Zone|null;
+ /**
+ * @returns {string} The Zone name (useful for debugging)
+ */
+ name: string;
+
+ /**
+ * Returns a value associated with the `key`.
+ *
+ * If the current zone does not have a key, the request is delegated to the parent zone. Use
+ * [ZoneSpec.properties] to configure the set of properties associated with the current zone.
+ *
+ * @param key The key to retrieve.
+ * @returns {any} The value for the key, or `undefined` if not found.
+ */
+ get(key: string): any;
+
+ /**
+ * Returns a Zone which defines a `key`.
+ *
+ * Recursively search the parent Zone until a Zone which has a property `key` is found.
+ *
+ * @param key The key to use for identification of the returned zone.
+ * @returns {Zone} The Zone which defines the `key`, `null` if not found.
+ */
+ getZoneWith(key: string): Zone|null;
+
+ /**
+ * Used to create a child zone.
+ *
+ * @param zoneSpec A set of rules which the child zone should follow.
+ * @returns {Zone} A new child zone.
+ */
+ fork(zoneSpec: ZoneSpec): Zone;
+
+ /**
+ * Wraps a callback function in a new function which will properly restore the current zone upon
+ * invocation.
+ *
+ * The wrapped function will properly forward `this` as well as `arguments` to the `callback`.
+ *
+ * Before the function is wrapped the zone can intercept the `callback` by declaring
+ * [ZoneSpec.onIntercept].
+ *
+ * @param callback the function which will be wrapped in the zone.
+ * @param source A unique debug location of the API being wrapped.
+ * @returns {function(): *} A function which will invoke the `callback` through [Zone.runGuarded].
+ */
+ wrap(callback: F, source: string): F;
+
+ /**
+ * Invokes a function in a given zone.
+ *
+ * The invocation of `callback` can be intercepted by declaring [ZoneSpec.onInvoke].
+ *
+ * @param callback The function to invoke.
+ * @param applyThis
+ * @param applyArgs
+ * @param source A unique debug location of the API being invoked.
+ * @returns {any} Value from the `callback` function.
+ */
+ run(callback: Function, applyThis?: any, applyArgs?: any[], source?: string): T;
+
+ /**
+ * Invokes a function in a given zone and catches any exceptions.
+ *
+ * Any exceptions thrown will be forwarded to [Zone.HandleError].
+ *
+ * The invocation of `callback` can be intercepted by declaring [ZoneSpec.onInvoke]. The
+ * handling of exceptions can be intercepted by declaring [ZoneSpec.handleError].
+ *
+ * @param callback The function to invoke.
+ * @param applyThis
+ * @param applyArgs
+ * @param source A unique debug location of the API being invoked.
+ * @returns {any} Value from the `callback` function.
+ */
+ runGuarded(callback: Function, applyThis?: any, applyArgs?: any[], source?: string): T;
+
+ /**
+ * Execute the Task by restoring the [Zone.currentTask] in the Task's zone.
+ *
+ * @param task to run
+ * @param applyThis
+ * @param applyArgs
+ * @returns {*}
+ */
+ runTask(task: Task, applyThis?: any, applyArgs?: any): any;
+
+ /**
+ * Schedule a MicroTask.
+ *
+ * @param source
+ * @param callback
+ * @param data
+ * @param customSchedule
+ */
+ scheduleMicroTask(
+ source: string, callback: Function, data?: TaskData,
+ customSchedule?: (task: Task) => void): MicroTask;
+
+ /**
+ * Schedule a MacroTask.
+ *
+ * @param source
+ * @param callback
+ * @param data
+ * @param customSchedule
+ * @param customCancel
+ */
+ scheduleMacroTask(
+ source: string, callback: Function, data?: TaskData, customSchedule?: (task: Task) => void,
+ customCancel?: (task: Task) => void): MacroTask;
+
+ /**
+ * Schedule an EventTask.
+ *
+ * @param source
+ * @param callback
+ * @param data
+ * @param customSchedule
+ * @param customCancel
+ */
+ scheduleEventTask(
+ source: string, callback: Function, data?: TaskData, customSchedule?: (task: Task) => void,
+ customCancel?: (task: Task) => void): EventTask;
+
+ /**
+ * Schedule an existing Task.
+ *
+ * Useful for rescheduling a task which was already canceled.
+ *
+ * @param task
+ */
+ scheduleTask(task: T): T;
+
+ /**
+ * Allows the zone to intercept canceling of scheduled Task.
+ *
+ * The interception is configured using [ZoneSpec.onCancelTask]. The default canceler invokes
+ * the [Task.cancelFn].
+ *
+ * @param task
+ * @returns {any}
+ */
+ cancelTask(task: Task): any;
+}
+
+interface ZoneType {
+ /**
+ * @returns {Zone} Returns the current [Zone]. The only way to change
+ * the current zone is by invoking a run() method, which will update the current zone for the
+ * duration of the run method callback.
+ */
+ current: Zone;
+
+ /**
+ * @returns {Task} The task associated with the current execution.
+ */
+ currentTask: Task|null;
+
+ /**
+ * Verify that Zone has been correctly patched. Specifically that Promise is zone aware.
+ */
+ assertZonePatched(): void;
+
+ /**
+ * Return the root zone.
+ */
+ root: Zone;
+
+ /** @internal */
+ __load_patch(name: string, fn: _PatchFn): void;
+
+ /** Was @ internal but this prevents compiling tests as separate unit */
+ __symbol__(name: string): string;
+}
+
+/** @internal */
+type _PatchFn = (global: Window, Zone: ZoneType, api: _ZonePrivate) => void;
+
+/** @internal */
+interface _ZonePrivate {
+ currentZoneFrame: () => _ZoneFrame;
+ symbol: (name: string) => string;
+ scheduleMicroTask: (task?: MicroTask) => void;
+ onUnhandledError: (error: Error) => void;
+ microtaskDrainDone: () => void;
+ showUncaughtError: () => boolean;
+ patchEventTarget: (global: any, apis: any[], options?: any) => boolean[];
+ patchOnProperties: (obj: any, properties: string[]|null, prototype?: any) => void;
+ patchThen: (ctro: Function) => void;
+ setNativePromise: (nativePromise: any) => void;
+ patchMethod:
+ (target: any, name: string,
+ patchFn: (delegate: Function, delegateName: string, name: string) =>
+ (self: any, args: any[]) => any) => Function | null;
+ bindArguments: (args: any[], source: string) => any[];
+ patchMacroTask:
+ (obj: any, funcName: string, metaCreator: (self: any, args: any[]) => any) => void;
+ patchEventPrototype: (_global: any, api: _ZonePrivate) => void;
+ isIEOrEdge: () => boolean;
+ ObjectDefineProperty:
+ (o: any, p: PropertyKey, attributes: PropertyDescriptor&ThisType) => any;
+ ObjectGetOwnPropertyDescriptor: (o: any, p: PropertyKey) => PropertyDescriptor | undefined;
+ ObjectCreate(o: object|null, properties?: PropertyDescriptorMap&ThisType): any;
+ ArraySlice(start?: number, end?: number): any[];
+ patchClass: (className: string) => void;
+ wrapWithCurrentZone: (callback: any, source: string) => any;
+ filterProperties: (target: any, onProperties: string[], ignoreProperties: any[]) => string[];
+ attachOriginToPatched: (target: any, origin: any) => void;
+ _redefineProperty: (target: any, callback: string, desc: any) => void;
+ patchCallbacks:
+ (api: _ZonePrivate, target: any, targetName: string, method: string,
+ callbacks: string[]) => void;
+ getGlobalObjects: () => {
+ globalSources: any, zoneSymbolEventNames: any, eventNames: string[], isBrowser: boolean,
+ isMix: boolean, isNode: boolean, TRUE_STR: string, FALSE_STR: string,
+ ZONE_SYMBOL_PREFIX: string, ADD_EVENT_LISTENER_STR: string,
+ REMOVE_EVENT_LISTENER_STR: string
+ } | undefined;
+}
+
+/** @internal */
+interface _ZoneFrame {
+ parent: _ZoneFrame|null;
+ zone: Zone;
+}
+
+interface UncaughtPromiseError extends Error {
+ zone: Zone;
+ task: Task;
+ promise: Promise;
+ rejection: any;
+}
+
+/**
+ * Provides a way to configure the interception of zone events.
+ *
+ * Only the `name` property is required (all other are optional).
+ */
+interface ZoneSpec {
+ /**
+ * The name of the zone. Useful when debugging Zones.
+ */
+ name: string;
+
+ /**
+ * A set of properties to be associated with Zone. Use [Zone.get] to retrieve them.
+ */
+ properties?: {[key: string]: any};
+
+ /**
+ * Allows the interception of zone forking.
+ *
+ * When the zone is being forked, the request is forwarded to this method for interception.
+ *
+ * @param parentZoneDelegate Delegate which performs the parent [ZoneSpec] operation.
+ * @param currentZone The current [Zone] where the current interceptor has been declared.
+ * @param targetZone The [Zone] which originally received the request.
+ * @param zoneSpec The argument passed into the `fork` method.
+ */
+ onFork?:
+ (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
+ zoneSpec: ZoneSpec) => Zone;
+
+ /**
+ * Allows interception of the wrapping of the callback.
+ *
+ * @param parentZoneDelegate Delegate which performs the parent [ZoneSpec] operation.
+ * @param currentZone The current [Zone] where the current interceptor has been declared.
+ * @param targetZone The [Zone] which originally received the request.
+ * @param delegate The argument passed into the `wrap` method.
+ * @param source The argument passed into the `wrap` method.
+ */
+ onIntercept?:
+ (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, delegate: Function,
+ source: string) => Function;
+
+ /**
+ * Allows interception of the callback invocation.
+ *
+ * @param parentZoneDelegate Delegate which performs the parent [ZoneSpec] operation.
+ * @param currentZone The current [Zone] where the current interceptor has been declared.
+ * @param targetZone The [Zone] which originally received the request.
+ * @param delegate The argument passed into the `run` method.
+ * @param applyThis The argument passed into the `run` method.
+ * @param applyArgs The argument passed into the `run` method.
+ * @param source The argument passed into the `run` method.
+ */
+ onInvoke?:
+ (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, delegate: Function,
+ applyThis: any, applyArgs?: any[], source?: string) => any;
+
+ /**
+ * Allows interception of the error handling.
+ *
+ * @param parentZoneDelegate Delegate which performs the parent [ZoneSpec] operation.
+ * @param currentZone The current [Zone] where the current interceptor has been declared.
+ * @param targetZone The [Zone] which originally received the request.
+ * @param error The argument passed into the `handleError` method.
+ */
+ onHandleError?:
+ (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
+ error: any) => boolean;
+
+ /**
+ * Allows interception of task scheduling.
+ *
+ * @param parentZoneDelegate Delegate which performs the parent [ZoneSpec] operation.
+ * @param currentZone The current [Zone] where the current interceptor has been declared.
+ * @param targetZone The [Zone] which originally received the request.
+ * @param task The argument passed into the `scheduleTask` method.
+ */
+ onScheduleTask?:
+ (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task) => Task;
+
+ onInvokeTask?:
+ (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task,
+ applyThis: any, applyArgs?: any[]) => any;
+
+ /**
+ * Allows interception of task cancellation.
+ *
+ * @param parentZoneDelegate Delegate which performs the parent [ZoneSpec] operation.
+ * @param currentZone The current [Zone] where the current interceptor has been declared.
+ * @param targetZone The [Zone] which originally received the request.
+ * @param task The argument passed into the `cancelTask` method.
+ */
+ onCancelTask?:
+ (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task) => any;
+
+ /**
+ * Notifies of changes to the task queue empty status.
+ *
+ * @param parentZoneDelegate Delegate which performs the parent [ZoneSpec] operation.
+ * @param currentZone The current [Zone] where the current interceptor has been declared.
+ * @param targetZone The [Zone] which originally received the request.
+ * @param hasTaskState
+ */
+ onHasTask?:
+ (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
+ hasTaskState: HasTaskState) => void;
+}
+
+
+/**
+ * A delegate when intercepting zone operations.
+ *
+ * A ZoneDelegate is needed because a child zone can't simply invoke a method on a parent zone. For
+ * example a child zone wrap can't just call parent zone wrap. Doing so would create a callback
+ * which is bound to the parent zone. What we are interested in is intercepting the callback before
+ * it is bound to any zone. Furthermore, we also need to pass the targetZone (zone which received
+ * the original request) to the delegate.
+ *
+ * The ZoneDelegate methods mirror those of Zone with an addition of extra targetZone argument in
+ * the method signature. (The original Zone which received the request.) Some methods are renamed
+ * to prevent confusion, because they have slightly different semantics and arguments.
+ *
+ * - `wrap` => `intercept`: The `wrap` method delegates to `intercept`. The `wrap` method returns
+ * a callback which will run in a given zone, where as intercept allows wrapping the callback
+ * so that additional code can be run before and after, but does not associate the callback
+ * with the zone.
+ * - `run` => `invoke`: The `run` method delegates to `invoke` to perform the actual execution of
+ * the callback. The `run` method switches to new zone; saves and restores the `Zone.current`;
+ * and optionally performs error handling. The invoke is not responsible for error handling,
+ * or zone management.
+ *
+ * Not every method is usually overwritten in the child zone, for this reason the ZoneDelegate
+ * stores the closest zone which overwrites this behavior along with the closest ZoneSpec.
+ *
+ * NOTE: We have tried to make this API analogous to Event bubbling with target and current
+ * properties.
+ *
+ * Note: The ZoneDelegate treats ZoneSpec as class. This allows the ZoneSpec to use its `this` to
+ * store internal state.
+ */
+interface ZoneDelegate {
+ zone: Zone;
+ fork(targetZone: Zone, zoneSpec: ZoneSpec): Zone;
+ intercept(targetZone: Zone, callback: Function, source: string): Function;
+ invoke(targetZone: Zone, callback: Function, applyThis?: any, applyArgs?: any[], source?: string):
+ any;
+ handleError(targetZone: Zone, error: any): boolean;
+ scheduleTask(targetZone: Zone, task: Task): Task;
+ invokeTask(targetZone: Zone, task: Task, applyThis?: any, applyArgs?: any[]): any;
+ cancelTask(targetZone: Zone, task: Task): any;
+ hasTask(targetZone: Zone, isEmpty: HasTaskState): void;
+}
+
+type HasTaskState = {
+ microTask: boolean; macroTask: boolean; eventTask: boolean; change: TaskType;
+};
+
+/**
+ * Task type: `microTask`, `macroTask`, `eventTask`.
+ */
+type TaskType = 'microTask' | 'macroTask' | 'eventTask';
+
+/**
+ * Task type: `notScheduled`, `scheduling`, `scheduled`, `running`, `canceling`, 'unknown'.
+ */
+type TaskState = 'notScheduled' | 'scheduling' | 'scheduled' | 'running' | 'canceling' | 'unknown';
+
+
+/**
+ */
+interface TaskData {
+ /**
+ * A periodic [MacroTask] is such which get automatically rescheduled after it is executed.
+ */
+ isPeriodic?: boolean;
+
+ /**
+ * Delay in milliseconds when the Task will run.
+ */
+ delay?: number;
+
+ /**
+ * identifier returned by the native setTimeout.
+ */
+ handleId?: number;
+}
+
+/**
+ * Represents work which is executed with a clean stack.
+ *
+ * Tasks are used in Zones to mark work which is performed on clean stack frame. There are three
+ * kinds of task. [MicroTask], [MacroTask], and [EventTask].
+ *
+ * A JS VM can be modeled as a [MicroTask] queue, [MacroTask] queue, and [EventTask] set.
+ *
+ * - [MicroTask] queue represents a set of tasks which are executing right after the current stack
+ * frame becomes clean and before a VM yield. All [MicroTask]s execute in order of insertion
+ * before VM yield and the next [MacroTask] is executed.
+ * - [MacroTask] queue represents a set of tasks which are executed one at a time after each VM
+ * yield. The queue is ordered by time, and insertions can happen in any location.
+ * - [EventTask] is a set of tasks which can at any time be inserted to the end of the [MacroTask]
+ * queue. This happens when the event fires.
+ *
+ */
+interface Task {
+ /**
+ * Task type: `microTask`, `macroTask`, `eventTask`.
+ */
+ type: TaskType;
+
+ /**
+ * Task state: `notScheduled`, `scheduling`, `scheduled`, `running`, `canceling`, `unknown`.
+ */
+ state: TaskState;
+
+ /**
+ * Debug string representing the API which requested the scheduling of the task.
+ */
+ source: string;
+
+ /**
+ * The Function to be used by the VM upon entering the [Task]. This function will delegate to
+ * [Zone.runTask] and delegate to `callback`.
+ */
+ invoke: Function;
+
+ /**
+ * Function which needs to be executed by the Task after the [Zone.currentTask] has been set to
+ * the current task.
+ */
+ callback: Function;
+
+ /**
+ * Task specific options associated with the current task. This is passed to the `scheduleFn`.
+ */
+ data?: TaskData;
+
+ /**
+ * Represents the default work which needs to be done to schedule the Task by the VM.
+ *
+ * A zone may choose to intercept this function and perform its own scheduling.
+ */
+ scheduleFn?: (task: Task) => void;
+
+ /**
+ * Represents the default work which needs to be done to un-schedule the Task from the VM. Not all
+ * Tasks are cancelable, and therefore this method is optional.
+ *
+ * A zone may chose to intercept this function and perform its own un-scheduling.
+ */
+ cancelFn?: (task: Task) => void;
+
+ /**
+ * @type {Zone} The zone which will be used to invoke the `callback`. The Zone is captured
+ * at the time of Task creation.
+ */
+ readonly zone: Zone;
+
+ /**
+ * Number of times the task has been executed, or -1 if canceled.
+ */
+ runCount: number;
+
+ /**
+ * Cancel the scheduling request. This method can be called from `ZoneSpec.onScheduleTask` to
+ * cancel the current scheduling interception. Once canceled the task can be discarded or
+ * rescheduled using `Zone.scheduleTask` on a different zone.
+ */
+ cancelScheduleRequest(): void;
+}
+
+interface MicroTask extends Task {
+ type: 'microTask';
+}
+
+interface MacroTask extends Task {
+ type: 'macroTask';
+}
+
+interface EventTask extends Task {
+ type: 'eventTask';
+}
+
+/** @internal */
+type AmbientZone = Zone;
+/** @internal */
+type AmbientZoneDelegate = ZoneDelegate;
+
+const Zone: ZoneType = (function(global: any) {
+ const performance: {mark(name: string): void; measure(name: string, label: string): void;} =
+ global['performance'];
+ function mark(name: string) { performance && performance['mark'] && performance['mark'](name); }
+ function performanceMeasure(name: string, label: string) {
+ performance && performance['measure'] && performance['measure'](name, label);
+ }
+ mark('Zone');
+
+ // Initialize before it's accessed below.
+ // __Zone_symbol_prefix global can be used to override the default zone
+ // symbol prefix with a custom one if needed.
+ const symbolPrefix = global['__Zone_symbol_prefix'] || '__zone_symbol__';
+
+ function __symbol__(name: string) { return symbolPrefix + name; }
+
+ const checkDuplicate = global[__symbol__('forceDuplicateZoneCheck')] === true;
+ if (global['Zone']) {
+ // if global['Zone'] already exists (maybe zone.js was already loaded or
+ // some other lib also registered a global object named Zone), we may need
+ // to throw an error, but sometimes user may not want this error.
+ // For example,
+ // we have two web pages, page1 includes zone.js, page2 doesn't.
+ // and the 1st time user load page1 and page2, everything work fine,
+ // but when user load page2 again, error occurs because global['Zone'] already exists.
+ // so we add a flag to let user choose whether to throw this error or not.
+ // By default, if existing Zone is from zone.js, we will not throw the error.
+ if (checkDuplicate || typeof global['Zone'].__symbol__ !== 'function') {
+ throw new Error('Zone already loaded.');
+ } else {
+ return global['Zone'];
+ }
+ }
+
+ class Zone implements AmbientZone {
+ static __symbol__: (name: string) => string = __symbol__;
+
+ static assertZonePatched() {
+ if (global['Promise'] !== patches['ZoneAwarePromise']) {
+ throw new Error(
+ 'Zone.js has detected that ZoneAwarePromise `(window|global).Promise` ' +
+ 'has been overwritten.\n' +
+ 'Most likely cause is that a Promise polyfill has been loaded ' +
+ 'after Zone.js (Polyfilling Promise api is not necessary when zone.js is loaded. ' +
+ 'If you must load one, do so before loading zone.js.)');
+ }
+ }
+
+ static get root(): AmbientZone {
+ let zone = Zone.current;
+ while (zone.parent) {
+ zone = zone.parent;
+ }
+ return zone;
+ }
+
+ static get current(): AmbientZone { return _currentZoneFrame.zone; }
+
+ static get currentTask(): Task|null { return _currentTask; }
+
+ static __load_patch(name: string, fn: _PatchFn): void {
+ if (patches.hasOwnProperty(name)) {
+ if (checkDuplicate) {
+ throw Error('Already loaded patch: ' + name);
+ }
+ } else if (!global['__Zone_disable_' + name]) {
+ const perfName = 'Zone:' + name;
+ mark(perfName);
+ patches[name] = fn(global, Zone, _api);
+ performanceMeasure(perfName, perfName);
+ }
+ }
+
+ public get parent(): AmbientZone|null { return this._parent; }
+
+ public get name(): string { return this._name; }
+
+
+ private _parent: Zone|null;
+ private _name: string;
+ private _properties: {[key: string]: any};
+ private _zoneDelegate: ZoneDelegate;
+
+ constructor(parent: Zone|null, zoneSpec: ZoneSpec|null) {
+ this._parent = parent;
+ this._name = zoneSpec ? zoneSpec.name || 'unnamed' : '';
+ this._properties = zoneSpec && zoneSpec.properties || {};
+ this._zoneDelegate =
+ new ZoneDelegate(this, this._parent && this._parent._zoneDelegate, zoneSpec);
+ }
+
+ public get(key: string): any {
+ const zone: Zone = this.getZoneWith(key) as Zone;
+ if (zone) return zone._properties[key];
+ }
+
+ public getZoneWith(key: string): AmbientZone|null {
+ let current: Zone|null = this;
+ while (current) {
+ if (current._properties.hasOwnProperty(key)) {
+ return current;
+ }
+ current = current._parent;
+ }
+ return null;
+ }
+
+ public fork(zoneSpec: ZoneSpec): AmbientZone {
+ if (!zoneSpec) throw new Error('ZoneSpec required!');
+ return this._zoneDelegate.fork(this, zoneSpec);
+ }
+
+ public wrap(callback: T, source: string): T {
+ if (typeof callback !== 'function') {
+ throw new Error('Expecting function got: ' + callback);
+ }
+ const _callback = this._zoneDelegate.intercept(this, callback, source);
+ const zone: Zone = this;
+ return function() {
+ return zone.runGuarded(_callback, (this as any), arguments, source);
+ } as any as T;
+ }
+
+ public run(callback: Function, applyThis?: any, applyArgs?: any[], source?: string): any;
+ public run(
+ callback: (...args: any[]) => T, applyThis?: any, applyArgs?: any[], source?: string): T {
+ _currentZoneFrame = {parent: _currentZoneFrame, zone: this};
+ try {
+ return this._zoneDelegate.invoke(this, callback, applyThis, applyArgs, source);
+ } finally {
+ _currentZoneFrame = _currentZoneFrame.parent !;
+ }
+ }
+
+ public runGuarded(callback: Function, applyThis?: any, applyArgs?: any[], source?: string): any;
+ public runGuarded(
+ callback: (...args: any[]) => T, applyThis: any = null, applyArgs?: any[],
+ source?: string) {
+ _currentZoneFrame = {parent: _currentZoneFrame, zone: this};
+ try {
+ try {
+ return this._zoneDelegate.invoke(this, callback, applyThis, applyArgs, source);
+ } catch (error) {
+ if (this._zoneDelegate.handleError(this, error)) {
+ throw error;
+ }
+ }
+ } finally {
+ _currentZoneFrame = _currentZoneFrame.parent !;
+ }
+ }
+
+
+ runTask(task: Task, applyThis?: any, applyArgs?: any): any {
+ if (task.zone != this) {
+ throw new Error(
+ 'A task can only be run in the zone of creation! (Creation: ' +
+ (task.zone || NO_ZONE).name + '; Execution: ' + this.name + ')');
+ }
+ // https://github.com/angular/zone.js/issues/778, sometimes eventTask
+ // will run in notScheduled(canceled) state, we should not try to
+ // run such kind of task but just return
+
+ if (task.state === notScheduled && (task.type === eventTask || task.type === macroTask)) {
+ return;
+ }
+
+ const reEntryGuard = task.state != running;
+ reEntryGuard && (task as ZoneTask)._transitionTo(running, scheduled);
+ task.runCount++;
+ const previousTask = _currentTask;
+ _currentTask = task;
+ _currentZoneFrame = {parent: _currentZoneFrame, zone: this};
+ try {
+ if (task.type == macroTask && task.data && !task.data.isPeriodic) {
+ task.cancelFn = undefined;
+ }
+ try {
+ return this._zoneDelegate.invokeTask(this, task, applyThis, applyArgs);
+ } catch (error) {
+ if (this._zoneDelegate.handleError(this, error)) {
+ throw error;
+ }
+ }
+ } finally {
+ // if the task's state is notScheduled or unknown, then it has already been cancelled
+ // we should not reset the state to scheduled
+ if (task.state !== notScheduled && task.state !== unknown) {
+ if (task.type == eventTask || (task.data && task.data.isPeriodic)) {
+ reEntryGuard && (task as ZoneTask)._transitionTo(scheduled, running);
+ } else {
+ task.runCount = 0;
+ this._updateTaskCount(task as ZoneTask, -1);
+ reEntryGuard &&
+ (task as ZoneTask)._transitionTo(notScheduled, running, notScheduled);
+ }
+ }
+ _currentZoneFrame = _currentZoneFrame.parent !;
+ _currentTask = previousTask;
+ }
+ }
+
+ scheduleTask(task: T): T {
+ if (task.zone && task.zone !== this) {
+ // check if the task was rescheduled, the newZone
+ // should not be the children of the original zone
+ let newZone: any = this;
+ while (newZone) {
+ if (newZone === task.zone) {
+ throw Error(`can not reschedule task to ${
+ this.name} which is descendants of the original zone ${task.zone.name}`);
+ }
+ newZone = newZone.parent;
+ }
+ }
+ (task as any as ZoneTask)._transitionTo(scheduling, notScheduled);
+ const zoneDelegates: ZoneDelegate[] = [];
+ (task as any as ZoneTask)._zoneDelegates = zoneDelegates;
+ (task as any as ZoneTask)._zone = this;
+ try {
+ task = this._zoneDelegate.scheduleTask(this, task) as T;
+ } catch (err) {
+ // should set task's state to unknown when scheduleTask throw error
+ // because the err may from reschedule, so the fromState maybe notScheduled
+ (task as any as ZoneTask)._transitionTo(unknown, scheduling, notScheduled);
+ // TODO: @JiaLiPassion, should we check the result from handleError?
+ this._zoneDelegate.handleError(this, err);
+ throw err;
+ }
+ if ((task as any as ZoneTask)._zoneDelegates === zoneDelegates) {
+ // we have to check because internally the delegate can reschedule the task.
+ this._updateTaskCount(task as any as ZoneTask, 1);
+ }
+ if ((task as any as ZoneTask).state == scheduling) {
+ (task as any as ZoneTask)._transitionTo(scheduled, scheduling);
+ }
+ return task;
+ }
+
+ scheduleMicroTask(
+ source: string, callback: Function, data?: TaskData,
+ customSchedule?: (task: Task) => void): MicroTask {
+ return this.scheduleTask(
+ new ZoneTask(microTask, source, callback, data, customSchedule, undefined));
+ }
+
+ scheduleMacroTask(
+ source: string, callback: Function, data?: TaskData, customSchedule?: (task: Task) => void,
+ customCancel?: (task: Task) => void): MacroTask {
+ return this.scheduleTask(
+ new ZoneTask(macroTask, source, callback, data, customSchedule, customCancel));
+ }
+
+ scheduleEventTask(
+ source: string, callback: Function, data?: TaskData, customSchedule?: (task: Task) => void,
+ customCancel?: (task: Task) => void): EventTask {
+ return this.scheduleTask(
+ new ZoneTask(eventTask, source, callback, data, customSchedule, customCancel));
+ }
+
+ cancelTask(task: Task): any {
+ if (task.zone != this)
+ throw new Error(
+ 'A task can only be cancelled in the zone of creation! (Creation: ' +
+ (task.zone || NO_ZONE).name + '; Execution: ' + this.name + ')');
+ (task as ZoneTask)._transitionTo(canceling, scheduled, running);
+ try {
+ this._zoneDelegate.cancelTask(this, task);
+ } catch (err) {
+ // if error occurs when cancelTask, transit the state to unknown
+ (task as ZoneTask)._transitionTo(unknown, canceling);
+ this._zoneDelegate.handleError(this, err);
+ throw err;
+ }
+ this._updateTaskCount(task as ZoneTask, -1);
+ (task as ZoneTask)._transitionTo(notScheduled, canceling);
+ task.runCount = 0;
+ return task;
+ }
+
+ private _updateTaskCount(task: ZoneTask, count: number) {
+ const zoneDelegates = task._zoneDelegates !;
+ if (count == -1) {
+ task._zoneDelegates = null;
+ }
+ for (let i = 0; i < zoneDelegates.length; i++) {
+ zoneDelegates[i]._updateTaskCount(task.type, count);
+ }
+ }
+ }
+
+ const DELEGATE_ZS: ZoneSpec = {
+ name: '',
+ onHasTask: (delegate: AmbientZoneDelegate, _: AmbientZone, target: AmbientZone,
+ hasTaskState: HasTaskState): void => delegate.hasTask(target, hasTaskState),
+ onScheduleTask: (delegate: AmbientZoneDelegate, _: AmbientZone, target: AmbientZone,
+ task: Task): Task => delegate.scheduleTask(target, task),
+ onInvokeTask: (delegate: AmbientZoneDelegate, _: AmbientZone, target: AmbientZone, task: Task,
+ applyThis: any, applyArgs: any): any =>
+ delegate.invokeTask(target, task, applyThis, applyArgs),
+ onCancelTask: (delegate: AmbientZoneDelegate, _: AmbientZone, target: AmbientZone, task: Task):
+ any => delegate.cancelTask(target, task)
+ };
+
+ class ZoneDelegate implements AmbientZoneDelegate {
+ public zone: Zone;
+
+ private _taskCounts: {microTask: number,
+ macroTask: number,
+ eventTask: number} = {'microTask': 0, 'macroTask': 0, 'eventTask': 0};
+
+ private _parentDelegate: ZoneDelegate|null;
+
+ private _forkDlgt: ZoneDelegate|null;
+ private _forkZS: ZoneSpec|null;
+ private _forkCurrZone: Zone|null;
+
+ private _interceptDlgt: ZoneDelegate|null;
+ private _interceptZS: ZoneSpec|null;
+ private _interceptCurrZone: Zone|null;
+
+ private _invokeDlgt: ZoneDelegate|null;
+ private _invokeZS: ZoneSpec|null;
+ private _invokeCurrZone: Zone|null;
+
+ private _handleErrorDlgt: ZoneDelegate|null;
+ private _handleErrorZS: ZoneSpec|null;
+ private _handleErrorCurrZone: Zone|null;
+
+ private _scheduleTaskDlgt: ZoneDelegate|null;
+ private _scheduleTaskZS: ZoneSpec|null;
+ private _scheduleTaskCurrZone: Zone|null;
+
+ private _invokeTaskDlgt: ZoneDelegate|null;
+ private _invokeTaskZS: ZoneSpec|null;
+ private _invokeTaskCurrZone: Zone|null;
+
+ private _cancelTaskDlgt: ZoneDelegate|null;
+ private _cancelTaskZS: ZoneSpec|null;
+ private _cancelTaskCurrZone: Zone|null;
+
+ private _hasTaskDlgt: ZoneDelegate|null;
+ private _hasTaskDlgtOwner: ZoneDelegate|null;
+ private _hasTaskZS: ZoneSpec|null;
+ private _hasTaskCurrZone: Zone|null;
+
+ constructor(zone: Zone, parentDelegate: ZoneDelegate|null, zoneSpec: ZoneSpec|null) {
+ this.zone = zone;
+ this._parentDelegate = parentDelegate;
+
+ this._forkZS =
+ zoneSpec && (zoneSpec && zoneSpec.onFork ? zoneSpec : parentDelegate !._forkZS);
+ this._forkDlgt = zoneSpec && (zoneSpec.onFork ? parentDelegate : parentDelegate !._forkDlgt);
+ this._forkCurrZone = zoneSpec && (zoneSpec.onFork ? this.zone : parentDelegate !.zone);
+
+ this._interceptZS =
+ zoneSpec && (zoneSpec.onIntercept ? zoneSpec : parentDelegate !._interceptZS);
+ this._interceptDlgt =
+ zoneSpec && (zoneSpec.onIntercept ? parentDelegate : parentDelegate !._interceptDlgt);
+ this._interceptCurrZone =
+ zoneSpec && (zoneSpec.onIntercept ? this.zone : parentDelegate !.zone);
+
+ this._invokeZS = zoneSpec && (zoneSpec.onInvoke ? zoneSpec : parentDelegate !._invokeZS);
+ this._invokeDlgt =
+ zoneSpec && (zoneSpec.onInvoke ? parentDelegate ! : parentDelegate !._invokeDlgt);
+ this._invokeCurrZone = zoneSpec && (zoneSpec.onInvoke ? this.zone : parentDelegate !.zone);
+
+ this._handleErrorZS =
+ zoneSpec && (zoneSpec.onHandleError ? zoneSpec : parentDelegate !._handleErrorZS);
+ this._handleErrorDlgt = zoneSpec &&
+ (zoneSpec.onHandleError ? parentDelegate ! : parentDelegate !._handleErrorDlgt);
+ this._handleErrorCurrZone =
+ zoneSpec && (zoneSpec.onHandleError ? this.zone : parentDelegate !.zone);
+
+ this._scheduleTaskZS =
+ zoneSpec && (zoneSpec.onScheduleTask ? zoneSpec : parentDelegate !._scheduleTaskZS);
+ this._scheduleTaskDlgt = zoneSpec &&
+ (zoneSpec.onScheduleTask ? parentDelegate ! : parentDelegate !._scheduleTaskDlgt);
+ this._scheduleTaskCurrZone =
+ zoneSpec && (zoneSpec.onScheduleTask ? this.zone : parentDelegate !.zone);
+
+ this._invokeTaskZS =
+ zoneSpec && (zoneSpec.onInvokeTask ? zoneSpec : parentDelegate !._invokeTaskZS);
+ this._invokeTaskDlgt =
+ zoneSpec && (zoneSpec.onInvokeTask ? parentDelegate ! : parentDelegate !._invokeTaskDlgt);
+ this._invokeTaskCurrZone =
+ zoneSpec && (zoneSpec.onInvokeTask ? this.zone : parentDelegate !.zone);
+
+ this._cancelTaskZS =
+ zoneSpec && (zoneSpec.onCancelTask ? zoneSpec : parentDelegate !._cancelTaskZS);
+ this._cancelTaskDlgt =
+ zoneSpec && (zoneSpec.onCancelTask ? parentDelegate ! : parentDelegate !._cancelTaskDlgt);
+ this._cancelTaskCurrZone =
+ zoneSpec && (zoneSpec.onCancelTask ? this.zone : parentDelegate !.zone);
+
+ this._hasTaskZS = null;
+ this._hasTaskDlgt = null;
+ this._hasTaskDlgtOwner = null;
+ this._hasTaskCurrZone = null;
+
+ const zoneSpecHasTask = zoneSpec && zoneSpec.onHasTask;
+ const parentHasTask = parentDelegate && parentDelegate._hasTaskZS;
+ if (zoneSpecHasTask || parentHasTask) {
+ // If we need to report hasTask, than this ZS needs to do ref counting on tasks. In such
+ // a case all task related interceptors must go through this ZD. We can't short circuit it.
+ this._hasTaskZS = zoneSpecHasTask ? zoneSpec : DELEGATE_ZS;
+ this._hasTaskDlgt = parentDelegate;
+ this._hasTaskDlgtOwner = this;
+ this._hasTaskCurrZone = zone;
+ if (!zoneSpec !.onScheduleTask) {
+ this._scheduleTaskZS = DELEGATE_ZS;
+ this._scheduleTaskDlgt = parentDelegate !;
+ this._scheduleTaskCurrZone = this.zone;
+ }
+ if (!zoneSpec !.onInvokeTask) {
+ this._invokeTaskZS = DELEGATE_ZS;
+ this._invokeTaskDlgt = parentDelegate !;
+ this._invokeTaskCurrZone = this.zone;
+ }
+ if (!zoneSpec !.onCancelTask) {
+ this._cancelTaskZS = DELEGATE_ZS;
+ this._cancelTaskDlgt = parentDelegate !;
+ this._cancelTaskCurrZone = this.zone;
+ }
+ }
+ }
+
+ fork(targetZone: Zone, zoneSpec: ZoneSpec): AmbientZone {
+ return this._forkZS ?
+ this._forkZS.onFork !(this._forkDlgt !, this.zone, targetZone, zoneSpec) :
+ new Zone(targetZone, zoneSpec);
+ }
+
+ intercept(targetZone: Zone, callback: Function, source: string): Function {
+ return this._interceptZS ?
+ this._interceptZS.onIntercept !(
+ this._interceptDlgt !, this._interceptCurrZone !, targetZone, callback, source) :
+ callback;
+ }
+
+ invoke(
+ targetZone: Zone, callback: Function, applyThis: any, applyArgs?: any[],
+ source?: string): any {
+ return this._invokeZS ?
+ this._invokeZS.onInvoke !(
+ this._invokeDlgt !, this._invokeCurrZone !, targetZone, callback, applyThis,
+ applyArgs, source) :
+ callback.apply(applyThis, applyArgs);
+ }
+
+ handleError(targetZone: Zone, error: any): boolean {
+ return this._handleErrorZS ?
+ this._handleErrorZS.onHandleError !(
+ this._handleErrorDlgt !, this._handleErrorCurrZone !, targetZone, error) :
+ true;
+ }
+
+ scheduleTask(targetZone: Zone, task: Task): Task {
+ let returnTask: ZoneTask = task as ZoneTask;
+ if (this._scheduleTaskZS) {
+ if (this._hasTaskZS) {
+ returnTask._zoneDelegates !.push(this._hasTaskDlgtOwner !);
+ }
+ // clang-format off
+ returnTask = this._scheduleTaskZS.onScheduleTask !(
+ this._scheduleTaskDlgt !, this._scheduleTaskCurrZone !, targetZone, task) as ZoneTask;
+ // clang-format on
+ if (!returnTask) returnTask = task as ZoneTask;
+ } else {
+ if (task.scheduleFn) {
+ task.scheduleFn(task);
+ } else if (task.type == microTask) {
+ scheduleMicroTask(task);
+ } else {
+ throw new Error('Task is missing scheduleFn.');
+ }
+ }
+ return returnTask;
+ }
+
+ invokeTask(targetZone: Zone, task: Task, applyThis: any, applyArgs?: any[]): any {
+ return this._invokeTaskZS ?
+ this._invokeTaskZS.onInvokeTask !(
+ this._invokeTaskDlgt !, this._invokeTaskCurrZone !, targetZone, task, applyThis,
+ applyArgs) :
+ task.callback.apply(applyThis, applyArgs);
+ }
+
+ cancelTask(targetZone: Zone, task: Task): any {
+ let value: any;
+ if (this._cancelTaskZS) {
+ value = this._cancelTaskZS.onCancelTask !(
+ this._cancelTaskDlgt !, this._cancelTaskCurrZone !, targetZone, task);
+ } else {
+ if (!task.cancelFn) {
+ throw Error('Task is not cancelable');
+ }
+ value = task.cancelFn(task);
+ }
+ return value;
+ }
+
+ hasTask(targetZone: Zone, isEmpty: HasTaskState) {
+ // hasTask should not throw error so other ZoneDelegate
+ // can still trigger hasTask callback
+ try {
+ this._hasTaskZS &&
+ this._hasTaskZS.onHasTask !(
+ this._hasTaskDlgt !, this._hasTaskCurrZone !, targetZone, isEmpty);
+ } catch (err) {
+ this.handleError(targetZone, err);
+ }
+ }
+
+ _updateTaskCount(type: TaskType, count: number) {
+ const counts = this._taskCounts;
+ const prev = counts[type];
+ const next = counts[type] = prev + count;
+ if (next < 0) {
+ throw new Error('More tasks executed then were scheduled.');
+ }
+ if (prev == 0 || next == 0) {
+ const isEmpty: HasTaskState = {
+ microTask: counts['microTask'] > 0,
+ macroTask: counts['macroTask'] > 0,
+ eventTask: counts['eventTask'] > 0,
+ change: type
+ };
+ this.hasTask(this.zone, isEmpty);
+ }
+ }
+ }
+
+ class ZoneTask implements Task {
+ public type: T;
+ public source: string;
+ public invoke: Function;
+ public callback: Function;
+ public data: TaskData|undefined;
+ public scheduleFn: ((task: Task) => void)|undefined;
+ public cancelFn: ((task: Task) => void)|undefined;
+ _zone: Zone|null = null;
+ public runCount: number = 0;
+ _zoneDelegates: ZoneDelegate[]|null = null;
+ _state: TaskState = 'notScheduled';
+
+ constructor(
+ type: T, source: string, callback: Function, options: TaskData|undefined,
+ scheduleFn: ((task: Task) => void)|undefined, cancelFn: ((task: Task) => void)|undefined) {
+ this.type = type;
+ this.source = source;
+ this.data = options;
+ this.scheduleFn = scheduleFn;
+ this.cancelFn = cancelFn;
+ this.callback = callback;
+ const self = this;
+ // TODO: @JiaLiPassion options should have interface
+ if (type === eventTask && options && (options as any).useG) {
+ this.invoke = ZoneTask.invokeTask;
+ } else {
+ this.invoke = function() {
+ return ZoneTask.invokeTask.call(global, self, this, arguments);
+ };
+ }
+ }
+
+ static invokeTask(task: any, target: any, args: any): any {
+ if (!task) {
+ task = this;
+ }
+ _numberOfNestedTaskFrames++;
+ try {
+ task.runCount++;
+ return task.zone.runTask(task, target, args);
+ } finally {
+ if (_numberOfNestedTaskFrames == 1) {
+ drainMicroTaskQueue();
+ }
+ _numberOfNestedTaskFrames--;
+ }
+ }
+
+ get zone(): Zone { return this._zone !; }
+
+ get state(): TaskState { return this._state; }
+
+ public cancelScheduleRequest() { this._transitionTo(notScheduled, scheduling); }
+
+ _transitionTo(toState: TaskState, fromState1: TaskState, fromState2?: TaskState) {
+ if (this._state === fromState1 || this._state === fromState2) {
+ this._state = toState;
+ if (toState == notScheduled) {
+ this._zoneDelegates = null;
+ }
+ } else {
+ throw new Error(`${this.type} '${this.source}': can not transition to '${
+ toState}', expecting state '${fromState1}'${
+ fromState2 ? ' or \'' + fromState2 + '\'' : ''}, was '${this._state}'.`);
+ }
+ }
+
+ public toString() {
+ if (this.data && typeof this.data.handleId !== 'undefined') {
+ return this.data.handleId.toString();
+ } else {
+ return Object.prototype.toString.call(this);
+ }
+ }
+
+ // add toJSON method to prevent cyclic error when
+ // call JSON.stringify(zoneTask)
+ public toJSON() {
+ return {
+ type: this.type,
+ state: this.state,
+ source: this.source,
+ zone: this.zone.name,
+ runCount: this.runCount
+ };
+ }
+ }
+
+
+ //////////////////////////////////////////////////////
+ //////////////////////////////////////////////////////
+ /// MICROTASK QUEUE
+ //////////////////////////////////////////////////////
+ //////////////////////////////////////////////////////
+ const symbolSetTimeout = __symbol__('setTimeout');
+ const symbolPromise = __symbol__('Promise');
+ const symbolThen = __symbol__('then');
+ let _microTaskQueue: Task[] = [];
+ let _isDrainingMicrotaskQueue: boolean = false;
+ let nativeMicroTaskQueuePromise: any;
+
+ function scheduleMicroTask(task?: MicroTask) {
+ // if we are not running in any task, and there has not been anything scheduled
+ // we must bootstrap the initial task creation by manually scheduling the drain
+ if (_numberOfNestedTaskFrames === 0 && _microTaskQueue.length === 0) {
+ // We are not running in Task, so we need to kickstart the microtask queue.
+ if (!nativeMicroTaskQueuePromise) {
+ if (global[symbolPromise]) {
+ nativeMicroTaskQueuePromise = global[symbolPromise].resolve(0);
+ }
+ }
+ if (nativeMicroTaskQueuePromise) {
+ let nativeThen = nativeMicroTaskQueuePromise[symbolThen];
+ if (!nativeThen) {
+ // native Promise is not patchable, we need to use `then` directly
+ // issue 1078
+ nativeThen = nativeMicroTaskQueuePromise['then'];
+ }
+ nativeThen.call(nativeMicroTaskQueuePromise, drainMicroTaskQueue);
+ } else {
+ global[symbolSetTimeout](drainMicroTaskQueue, 0);
+ }
+ }
+ task && _microTaskQueue.push(task);
+ }
+
+ function drainMicroTaskQueue() {
+ if (!_isDrainingMicrotaskQueue) {
+ _isDrainingMicrotaskQueue = true;
+ while (_microTaskQueue.length) {
+ const queue = _microTaskQueue;
+ _microTaskQueue = [];
+ for (let i = 0; i < queue.length; i++) {
+ const task = queue[i];
+ try {
+ task.zone.runTask(task, null, null);
+ } catch (error) {
+ _api.onUnhandledError(error);
+ }
+ }
+ }
+ _api.microtaskDrainDone();
+ _isDrainingMicrotaskQueue = false;
+ }
+ }
+
+ //////////////////////////////////////////////////////
+ //////////////////////////////////////////////////////
+ /// BOOTSTRAP
+ //////////////////////////////////////////////////////
+ //////////////////////////////////////////////////////
+
+
+ const NO_ZONE = {name: 'NO ZONE'};
+ const notScheduled: 'notScheduled' = 'notScheduled', scheduling: 'scheduling' = 'scheduling',
+ scheduled: 'scheduled' = 'scheduled', running: 'running' = 'running',
+ canceling: 'canceling' = 'canceling', unknown: 'unknown' = 'unknown';
+ const microTask: 'microTask' = 'microTask', macroTask: 'macroTask' = 'macroTask',
+ eventTask: 'eventTask' = 'eventTask';
+
+ const patches: {[key: string]: any} = {};
+ const _api: _ZonePrivate = {
+ symbol: __symbol__,
+ currentZoneFrame: () => _currentZoneFrame,
+ onUnhandledError: noop,
+ microtaskDrainDone: noop,
+ scheduleMicroTask: scheduleMicroTask,
+ showUncaughtError: () => !(Zone as any)[__symbol__('ignoreConsoleErrorUncaughtError')],
+ patchEventTarget: () => [],
+ patchOnProperties: noop,
+ patchMethod: () => noop,
+ bindArguments: () => [],
+ patchThen: () => noop,
+ patchMacroTask: () => noop,
+ setNativePromise: (NativePromise: any) => {
+ // sometimes NativePromise.resolve static function
+ // is not ready yet, (such as core-js/es6.promise)
+ // so we need to check here.
+ if (NativePromise && typeof NativePromise.resolve === 'function') {
+ nativeMicroTaskQueuePromise = NativePromise.resolve(0);
+ }
+ },
+ patchEventPrototype: () => noop,
+ isIEOrEdge: () => false,
+ getGlobalObjects: () => undefined,
+ ObjectDefineProperty: () => noop,
+ ObjectGetOwnPropertyDescriptor: () => undefined,
+ ObjectCreate: () => undefined,
+ ArraySlice: () => [],
+ patchClass: () => noop,
+ wrapWithCurrentZone: () => noop,
+ filterProperties: () => [],
+ attachOriginToPatched: () => noop,
+ _redefineProperty: () => noop,
+ patchCallbacks: () => noop
+ };
+ let _currentZoneFrame: _ZoneFrame = {parent: null, zone: new Zone(null, null)};
+ let _currentTask: Task|null = null;
+ let _numberOfNestedTaskFrames = 0;
+
+ function noop() {}
+
+ performanceMeasure('Zone', 'Zone');
+ return global['Zone'] = Zone;
+})(global);
diff --git a/packages/zone.js/package.json b/packages/zone.js/package.json
new file mode 100644
index 0000000000..139cfc4102
--- /dev/null
+++ b/packages/zone.js/package.json
@@ -0,0 +1,36 @@
+{
+ "name": "zone.js",
+ "version": "0.0.0-PLACEHOLDER",
+ "description": "Zones for JavaScript",
+ "main": "dist/zone-node.js",
+ "browser": "dist/zone.js",
+ "unpkg": "dist/zone.js",
+ "typings": "dist/zone.js.d.ts",
+ "files": [
+ "lib",
+ "dist"
+ ],
+ "directories": {
+ "lib": "lib",
+ "test": "test"
+ },
+ "devDependencies": {
+ "mocha": "^3.1.2",
+ "promises-aplus-tests": "^2.1.2",
+ "typescript": "~3.4.2"
+ },
+ "scripts": {
+ "promisetest": "tsc -p . && node ./promise-test.js",
+ "promisefinallytest": "tsc -p . && mocha promise.finally.spec.js"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git://github.com/angular/angular.git",
+ "directory": "packages/zone.js"
+ },
+ "author": "Brian Ford",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/angular/angular/issues"
+ }
+}
diff --git a/packages/zone.js/presentation.png b/packages/zone.js/presentation.png
new file mode 100644
index 0000000000..3952ce243a
Binary files /dev/null and b/packages/zone.js/presentation.png differ
diff --git a/packages/zone.js/promise-adapter.js b/packages/zone.js/promise-adapter.js
new file mode 100644
index 0000000000..be9f9ac1eb
--- /dev/null
+++ b/packages/zone.js/promise-adapter.js
@@ -0,0 +1,18 @@
+require('./build/lib/node/rollup-main');
+Zone[Zone.__symbol__('ignoreConsoleErrorUncaughtError')] = true;
+module.exports.deferred = function() {
+ const p = {};
+ p.promise = new Promise((resolve, reject) => {
+ p.resolve = resolve;
+ p.reject = reject;
+ });
+ return p;
+};
+
+module.exports.resolved = (val) => {
+ return Promise.resolve(val);
+};
+
+module.exports.rejected = (reason) => {
+ return Promise.reject(reason);
+};
diff --git a/packages/zone.js/promise-test.js b/packages/zone.js/promise-test.js
new file mode 100644
index 0000000000..c9ab27ad36
--- /dev/null
+++ b/packages/zone.js/promise-test.js
@@ -0,0 +1,10 @@
+const promisesAplusTests = require('promises-aplus-tests');
+const adapter = require('./promise-adapter');
+promisesAplusTests(adapter, {reporter: 'dot'}, function(err) {
+ if (err) {
+ console.error(err);
+ process.exit(1);
+ } else {
+ process.exit(0);
+ }
+});
diff --git a/packages/zone.js/promise.finally.spec.js b/packages/zone.js/promise.finally.spec.js
new file mode 100644
index 0000000000..6695b2981b
--- /dev/null
+++ b/packages/zone.js/promise.finally.spec.js
@@ -0,0 +1,358 @@
+'use strict';
+
+var assert = require('assert');
+var adapter = require('./promise-adapter');
+var P = global[Zone.__symbol__('Promise')];
+
+var someRejectionReason = {message: 'some rejection reason'};
+var anotherReason = {message: 'another rejection reason'};
+process.on(
+ 'unhandledRejection', function(reason, promise) { console.log('unhandledRejection', reason); });
+
+describe('mocha promise sanity check', () => {
+ it('passes with a resolved promise', () => { return P.resolve(3); });
+
+ it('passes with a rejected then resolved promise',
+ () => { return P.reject(someRejectionReason).catch(x => 'this should be resolved'); });
+
+ var ifPromiseIt = P === Promise ? it : it.skip;
+ ifPromiseIt('is the native Promise', () => { assert.equal(P, Promise); });
+});
+
+describe('onFinally', () => {
+ describe('no callback', () => {
+ specify('from resolved', (done) => {
+ adapter.resolved(3)
+ .then((x) => {
+ assert.strictEqual(x, 3);
+ return x;
+ })
+ .finally()
+ .then(
+ function onFulfilled(x) {
+ assert.strictEqual(x, 3);
+ done();
+ },
+ function onRejected() { done(new Error('should not be called')); });
+ });
+
+ specify('from rejected', (done) => {
+ adapter.rejected(someRejectionReason)
+ .catch((e) => {
+ assert.strictEqual(e, someRejectionReason);
+ throw e;
+ })
+ .finally()
+ .then(
+ function onFulfilled() { done(new Error('should not be called')); },
+ function onRejected(reason) {
+ assert.strictEqual(reason, someRejectionReason);
+ done();
+ });
+ });
+ });
+
+ describe('throws an exception', () => {
+ specify('from resolved', (done) => {
+ adapter.resolved(3)
+ .then((x) => {
+ assert.strictEqual(x, 3);
+ return x;
+ })
+ .finally(function onFinally() {
+ assert(arguments.length === 0);
+ throw someRejectionReason;
+ })
+ .then(
+ function onFulfilled() { done(new Error('should not be called')); },
+ function onRejected(reason) {
+ assert.strictEqual(reason, someRejectionReason);
+ done();
+ });
+ });
+
+ specify('from rejected', (done) => {
+ adapter.rejected(anotherReason)
+ .finally(function onFinally() {
+ assert(arguments.length === 0);
+ throw someRejectionReason;
+ })
+ .then(
+ function onFulfilled() { done(new Error('should not be called')); },
+ function onRejected(reason) {
+ assert.strictEqual(reason, someRejectionReason);
+ done();
+ });
+ });
+ });
+
+ describe('returns a non-promise', () => {
+ specify('from resolved', (done) => {
+ adapter.resolved(3)
+ .then((x) => {
+ assert.strictEqual(x, 3);
+ return x;
+ })
+ .finally(function onFinally() {
+ assert(arguments.length === 0);
+ return 4;
+ })
+ .then(
+ function onFulfilled(x) {
+ assert.strictEqual(x, 3);
+ done();
+ },
+ function onRejected() { done(new Error('should not be called')); });
+ });
+
+ specify('from rejected', (done) => {
+ adapter.rejected(anotherReason)
+ .catch((e) => {
+ assert.strictEqual(e, anotherReason);
+ throw e;
+ })
+ .finally(function onFinally() {
+ assert(arguments.length === 0);
+ throw someRejectionReason;
+ })
+ .then(
+ function onFulfilled() { done(new Error('should not be called')); },
+ function onRejected(e) {
+ assert.strictEqual(e, someRejectionReason);
+ done();
+ });
+ });
+ });
+
+ describe('returns a pending-forever promise', () => {
+ specify('from resolved', (done) => {
+ var timeout;
+ adapter.resolved(3)
+ .then((x) => {
+ assert.strictEqual(x, 3);
+ return x;
+ })
+ .finally(function onFinally() {
+ assert(arguments.length === 0);
+ timeout = setTimeout(done, 0.1e3);
+ return new P(() => {}); // forever pending
+ })
+ .then(
+ function onFulfilled(x) {
+ clearTimeout(timeout);
+ done(new Error('should not be called'));
+ },
+ function onRejected() {
+ clearTimeout(timeout);
+ done(new Error('should not be called'));
+ });
+ });
+
+ specify('from rejected', (done) => {
+ var timeout;
+ adapter.rejected(someRejectionReason)
+ .catch((e) => {
+ assert.strictEqual(e, someRejectionReason);
+ throw e;
+ })
+ .finally(function onFinally() {
+ assert(arguments.length === 0);
+ timeout = setTimeout(done, 0.1e3);
+ return new P(() => {}); // forever pending
+ })
+ .then(
+ function onFulfilled(x) {
+ clearTimeout(timeout);
+ done(new Error('should not be called'));
+ },
+ function onRejected() {
+ clearTimeout(timeout);
+ done(new Error('should not be called'));
+ });
+ });
+ });
+
+ describe('returns an immediately-fulfilled promise', () => {
+ specify('from resolved', (done) => {
+ adapter.resolved(3)
+ .then((x) => {
+ assert.strictEqual(x, 3);
+ return x;
+ })
+ .finally(function onFinally() {
+ assert(arguments.length === 0);
+ return adapter.resolved(4);
+ })
+ .then(
+ function onFulfilled(x) {
+ assert.strictEqual(x, 3);
+ done();
+ },
+ function onRejected() { done(new Error('should not be called')); });
+ });
+
+ specify('from rejected', (done) => {
+ adapter.rejected(someRejectionReason)
+ .catch((e) => {
+ assert.strictEqual(e, someRejectionReason);
+ throw e;
+ })
+ .finally(function onFinally() {
+ assert(arguments.length === 0);
+ return adapter.resolved(4);
+ })
+ .then(
+ function onFulfilled() { done(new Error('should not be called')); },
+ function onRejected(e) {
+ assert.strictEqual(e, someRejectionReason);
+ done();
+ });
+ });
+ });
+
+ describe('returns an immediately-rejected promise', () => {
+ specify('from resolved ', (done) => {
+ adapter.resolved(3)
+ .then((x) => {
+ assert.strictEqual(x, 3);
+ return x;
+ })
+ .finally(function onFinally() {
+ assert(arguments.length === 0);
+ return adapter.rejected(4);
+ })
+ .then(
+ function onFulfilled(x) { done(new Error('should not be called')); },
+ function onRejected(e) {
+ assert.strictEqual(e, 4);
+ done();
+ });
+ });
+
+ specify('from rejected', (done) => {
+ const newReason = {};
+ adapter.rejected(someRejectionReason)
+ .catch((e) => {
+ assert.strictEqual(e, someRejectionReason);
+ throw e;
+ })
+ .finally(function onFinally() {
+ assert(arguments.length === 0);
+ return adapter.rejected(newReason);
+ })
+ .then(
+ function onFulfilled(x) { done(new Error('should not be called')); },
+ function onRejected(e) {
+ assert.strictEqual(e, newReason);
+ done();
+ });
+ });
+ });
+
+ describe('returns a fulfilled-after-a-second promise', () => {
+ specify('from resolved', (done) => {
+ var timeout;
+ adapter.resolved(3)
+ .then((x) => {
+ assert.strictEqual(x, 3);
+ return x;
+ })
+ .finally(function onFinally() {
+ assert(arguments.length === 0);
+ timeout = setTimeout(done, 1.5e3);
+ return new P((resolve) => { setTimeout(() => resolve(4), 1e3); });
+ })
+ .then(
+ function onFulfilled(x) {
+ clearTimeout(timeout);
+ assert.strictEqual(x, 3);
+ done();
+ },
+ function onRejected() {
+ clearTimeout(timeout);
+ done(new Error('should not be called'));
+ });
+ });
+
+ specify('from rejected', (done) => {
+ var timeout;
+ adapter.rejected(3)
+ .catch((e) => {
+ assert.strictEqual(e, 3);
+ throw e;
+ })
+ .finally(function onFinally() {
+ assert(arguments.length === 0);
+ timeout = setTimeout(done, 1.5e3);
+ return new P((resolve) => { setTimeout(() => resolve(4), 1e3); });
+ })
+ .then(
+ function onFulfilled() {
+ clearTimeout(timeout);
+ done(new Error('should not be called'));
+ },
+ function onRejected(e) {
+ clearTimeout(timeout);
+ assert.strictEqual(e, 3);
+ done();
+ });
+ });
+ });
+
+ describe('returns a rejected-after-a-second promise', () => {
+ specify('from resolved', (done) => {
+ var timeout;
+ adapter.resolved(3)
+ .then((x) => {
+ assert.strictEqual(x, 3);
+ return x;
+ })
+ .finally(function onFinally() {
+ assert(arguments.length === 0);
+ timeout = setTimeout(done, 1.5e3);
+ return new P((resolve, reject) => { setTimeout(() => reject(4), 1e3); });
+ })
+ .then(
+ function onFulfilled() {
+ clearTimeout(timeout);
+ done(new Error('should not be called'));
+ },
+ function onRejected(e) {
+ clearTimeout(timeout);
+ assert.strictEqual(e, 4);
+ done();
+ });
+ });
+
+ specify('from rejected', (done) => {
+ var timeout;
+ adapter.rejected(someRejectionReason)
+ .catch((e) => {
+ assert.strictEqual(e, someRejectionReason);
+ throw e;
+ })
+ .finally(function onFinally() {
+ assert(arguments.length === 0);
+ timeout = setTimeout(done, 1.5e3);
+ return new P((resolve, reject) => { setTimeout(() => reject(anotherReason), 1e3); });
+ })
+ .then(
+ function onFulfilled() {
+ clearTimeout(timeout);
+ done(new Error('should not be called'));
+ },
+ function onRejected(e) {
+ clearTimeout(timeout);
+ assert.strictEqual(e, anotherReason);
+ done();
+ });
+ });
+ });
+
+ specify('has the correct property descriptor', () => {
+ var descriptor = Object.getOwnPropertyDescriptor(Promise.prototype, 'finally');
+
+ assert.strictEqual(descriptor.writable, true);
+ assert.strictEqual(descriptor.configurable, true);
+ });
+});
\ No newline at end of file
diff --git a/packages/zone.js/sauce-evergreen.conf.js b/packages/zone.js/sauce-evergreen.conf.js
new file mode 100644
index 0000000000..ce874ea63a
--- /dev/null
+++ b/packages/zone.js/sauce-evergreen.conf.js
@@ -0,0 +1,66 @@
+// Sauce configuration
+
+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: '72'},
+ 'SL_CHROME_60': {base: 'SauceLabs', browserName: 'chrome', version: '60'},
+ 'SL_ANDROID8.0': {
+ base: 'SauceLabs',
+ browserName: 'Chrome',
+ appiumVersion: '1.9.1',
+ platformName: 'Android',
+ deviceName: 'Android GoogleAPI Emulator',
+ platformVersion: '8.0'
+ }
+ };
+
+ var customLaunchers = {};
+ if (!ignoredLaunchers) {
+ customLaunchers = basicLaunchers;
+ } else {
+ Object.keys(basicLaunchers).forEach(function(key) {
+ if (ignoredLaunchers.filter(function(ignore) { return ignore === key; }).length === 0) {
+ customLaunchers[key] = basicLaunchers[key];
+ }
+ });
+ }
+
+ config.set({
+ captureTimeout: 120000,
+ browserNoActivityTimeout: 240000,
+
+ sauceLabs: {
+ testName: 'Zone.js',
+ startConnect: false,
+ recordVideo: false,
+ recordScreenshots: false,
+ options: {
+ 'selenium-version': '3.4.0',
+ 'command-timeout': 600,
+ 'idle-timeout': 600,
+ 'max-duration': 5400
+ }
+ },
+
+ customLaunchers: customLaunchers,
+
+ browsers: Object.keys(customLaunchers),
+
+ reporters: ['dots', 'saucelabs'],
+
+ singleRun: true,
+
+ plugins: ['karma-*']
+ });
+
+ if (process.env.TRAVIS) {
+ 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/packages/zone.js/sauce-selenium3.conf.js b/packages/zone.js/sauce-selenium3.conf.js
new file mode 100644
index 0000000000..0141e88089
--- /dev/null
+++ b/packages/zone.js/sauce-selenium3.conf.js
@@ -0,0 +1,49 @@
+// Sauce configuration with Welenium drivers 3+
+
+module.exports = function(config) {
+ // The WS server is not available with Sauce
+ config.files.unshift('test/saucelabs.js');
+
+ var customLaunchers = {
+ 'SL_CHROME60':
+ {base: 'SauceLabs', browserName: 'Chrome', platform: 'Windows 10', version: '60.0'},
+ 'SL_SAFARI11':
+ {base: 'SauceLabs', browserName: 'safari', platform: 'macOS 10.13', version: '11.1'},
+ };
+
+ config.set({
+ captureTimeout: 120000,
+ browserNoActivityTimeout: 240000,
+
+ sauceLabs: {
+ testName: 'Zone.js',
+ startConnect: false,
+ recordVideo: false,
+ recordScreenshots: false,
+ options: {
+ 'selenium-version': '3.5.0',
+ 'command-timeout': 600,
+ 'idle-timeout': 600,
+ 'max-duration': 5400
+ }
+ },
+
+ customLaunchers: customLaunchers,
+
+ browsers: Object.keys(customLaunchers),
+
+ reporters: ['dots', 'saucelabs'],
+
+ singleRun: true,
+
+ plugins: ['karma-*']
+ });
+
+ if (process.env.TRAVIS) {
+ 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/packages/zone.js/sauce.conf.js b/packages/zone.js/sauce.conf.js
new file mode 100644
index 0000000000..aa24eefa22
--- /dev/null
+++ b/packages/zone.js/sauce.conf.js
@@ -0,0 +1,151 @@
+// Sauce configuration
+
+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_65': {base: 'SauceLabs', browserName: 'chrome', version: '60'},
+ 'SL_FIREFOX': {base: 'SauceLabs', browserName: 'firefox', version: '52'},
+ 'SL_FIREFOX_59': {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'},
+ /*
+ no longer supported in SauceLabs
+ 'SL_IOS7': {
+ base: 'SauceLabs',
+ browserName: 'iphone',
+ platform: 'OS X 10.10',
+ version: '7.1'
+ },*/
+ /*'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.3'},
+ 'SL_IE9': {
+ base: 'SauceLabs',
+ browserName: 'internet explorer',
+ platform: 'Windows 2008',
+ version: '9'
+ },
+ 'SL_IE10': {
+ base: 'SauceLabs',
+ browserName: 'internet explorer',
+ platform: 'Windows 2012',
+ version: '10'
+ },
+ 'SL_IE11': {
+ base: 'SauceLabs',
+ browserName: 'internet explorer',
+ platform: 'Windows 10',
+ version: '11'
+ },
+ 'SL_MSEDGE': {
+ base: 'SauceLabs',
+ browserName: 'MicrosoftEdge',
+ platform: 'Windows 10',
+ version: '14.14393'
+ },
+ 'SL_MSEDGE15': {
+ base: 'SauceLabs',
+ browserName: 'MicrosoftEdge',
+ platform: 'Windows 10',
+ version: '15.15063'
+ },
+ /*
+ fix issue #584, Android 4.1~4.3 are not supported
+ 'SL_ANDROID4.1': {
+ base: 'SauceLabs',
+ browserName: 'android',
+ platform: 'Linux',
+ version: '4.1'
+ },
+ 'SL_ANDROID4.2': {
+ base: 'SauceLabs',
+ browserName: 'android',
+ platform: 'Linux',
+ version: '4.2'
+ },
+ 'SL_ANDROID4.3': {
+ base: 'SauceLabs',
+ browserName: 'android',
+ 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_ANDROID8.0': {
+ base: 'SauceLabs',
+ browserName: 'Chrome',
+ appiumVersion: '1.12.1',
+ platformName: 'Android',
+ deviceName: 'Android GoogleAPI Emulator',
+ platformVersion: '8.0'
+ }
+ };
+
+ var customLaunchers = {};
+ if (!ignoredLaunchers) {
+ customLaunchers = basicLaunchers;
+ } else {
+ Object.keys(basicLaunchers).forEach(function(key) {
+ if (ignoredLaunchers.filter(function(ignore) { return ignore === key; }).length === 0) {
+ customLaunchers[key] = basicLaunchers[key];
+ }
+ });
+ }
+
+ config.set({
+ captureTimeout: 120000,
+ browserNoActivityTimeout: 240000,
+
+ sauceLabs: {
+ testName: 'Zone.js',
+ startConnect: false,
+ recordVideo: false,
+ recordScreenshots: false,
+ options: {
+ 'selenium-version': '2.53.0',
+ 'command-timeout': 600,
+ 'idle-timeout': 600,
+ 'max-duration': 5400
+ }
+ },
+
+ customLaunchers: customLaunchers,
+
+ browsers: Object.keys(customLaunchers),
+
+ reporters: ['dots', 'saucelabs'],
+
+ singleRun: true,
+
+ plugins: ['karma-*']
+ });
+
+ if (process.env.TRAVIS) {
+ 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/packages/zone.js/sauce.es2015.conf.js b/packages/zone.js/sauce.es2015.conf.js
new file mode 100644
index 0000000000..cea30d38d1
--- /dev/null
+++ b/packages/zone.js/sauce.es2015.conf.js
@@ -0,0 +1,57 @@
+// Sauce configuration
+
+module.exports = function(config, ignoredLaunchers) {
+ // The WS server is not available with Sauce
+ config.files.unshift('test/saucelabs.js');
+
+ var basicLaunchers = {
+ 'SL_CHROME_66': {base: 'SauceLabs', browserName: 'chrome', version: '66'},
+ };
+
+ var customLaunchers = {};
+ if (!ignoredLaunchers) {
+ customLaunchers = basicLaunchers;
+ } else {
+ Object.keys(basicLaunchers).forEach(function(key) {
+ if (ignoredLaunchers.filter(function(ignore) { return ignore === key; }).length === 0) {
+ customLaunchers[key] = basicLaunchers[key];
+ }
+ });
+ }
+
+ config.set({
+ captureTimeout: 120000,
+ browserNoActivityTimeout: 240000,
+
+ sauceLabs: {
+ testName: 'Zone.js',
+ startConnect: false,
+ recordVideo: false,
+ recordScreenshots: false,
+ options: {
+ 'selenium-version': '2.53.0',
+ 'command-timeout': 600,
+ 'idle-timeout': 600,
+ 'max-duration': 5400
+ }
+ },
+
+ customLaunchers: customLaunchers,
+
+ browsers: Object.keys(customLaunchers),
+
+ reporters: ['dots', 'saucelabs'],
+
+ singleRun: true,
+
+ plugins: ['karma-*']
+ });
+
+ if (process.env.TRAVIS) {
+ 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/packages/zone.js/scripts/closure/closure_compiler.sh b/packages/zone.js/scripts/closure/closure_compiler.sh
new file mode 100755
index 0000000000..638ffcf108
--- /dev/null
+++ b/packages/zone.js/scripts/closure/closure_compiler.sh
@@ -0,0 +1,31 @@
+# compile closure test source file
+$(npm bin)/tsc -p .
+# Run the Google Closure compiler java runnable with zone externs
+java -jar node_modules/google-closure-compiler/compiler.jar --flagfile 'scripts/closure/closure_flagfile' --externs 'lib/closure/zone_externs.js'
+
+# the names of Zone exposed API should be kept correctly with zone externs, test program should exit with 0.
+node build/closure/closure-bundle.js
+
+if [ $? -eq 0 ]
+then
+ echo "Successfully pass closure compiler with zone externs"
+else
+ echo "failed to pass closure compiler with zone externs"
+ exit 1
+fi
+
+# Run the Google Closure compiler java runnable without zone externs.
+java -jar node_modules/google-closure-compiler/compiler.jar --flagfile 'scripts/closure/closure_flagfile'
+
+# the names of Zone exposed API should be renamed and fail to be executed, test program should exit with 1.
+node build/closure/closure-bundle.js
+
+if [ $? -eq 1 ]
+then
+ echo "Successfully detect closure compiler error without zone externs"
+else
+ echo "failed to detect closure compiler error without zone externs"
+ exit 1
+fi
+
+exit 0
diff --git a/packages/zone.js/scripts/closure/closure_flagfile b/packages/zone.js/scripts/closure/closure_flagfile
new file mode 100644
index 0000000000..524aa0ef66
--- /dev/null
+++ b/packages/zone.js/scripts/closure/closure_flagfile
@@ -0,0 +1,5 @@
+--compilation_level ADVANCED_OPTIMIZATIONS
+--js_output_file "build/closure/closure-bundle.js"
+--rewrite_polyfills false
+--js "build/test/closure/zone.closure.js"
+--formatting PRETTY_PRINT
\ No newline at end of file
diff --git a/packages/zone.js/scripts/grab-blink-idl.sh b/packages/zone.js/scripts/grab-blink-idl.sh
new file mode 100755
index 0000000000..4884ea73d8
--- /dev/null
+++ b/packages/zone.js/scripts/grab-blink-idl.sh
@@ -0,0 +1,36 @@
+#!/bin/bash
+
+set -e
+
+trap "echo Exit; exit;" SIGINT SIGTERM
+
+CORE_URL="https://src.chromium.org/blink/trunk/Source/core/"
+MODULE_URL="https://src.chromium.org/blink/trunk/Source/modules/"
+
+mkdir -p blink-idl/core
+mkdir -p blink-idl/modules
+
+
+echo "Fetching core idl files..."
+
+rm tmp/ -rf
+svn co $CORE_URL tmp -q
+
+for IDL in $(find tmp/ -iname '*.idl' -type f -printf '%P\n')
+do
+ echo "- $IDL"
+ mv "tmp/$IDL" blink-idl/core
+done
+
+echo "Fetching modules idl files..."
+
+rm tmp/ -rf
+svn co $MODULE_URL tmp -q
+
+for IDL in $(find tmp/ -iname '*.idl' -type f -printf '%P\n')
+do
+ echo "- $IDL"
+ mv "tmp/$IDL" blink-idl/modules
+done
+
+rm tmp/ -rf
diff --git a/packages/zone.js/scripts/sauce/sauce_connect_block.sh b/packages/zone.js/scripts/sauce/sauce_connect_block.sh
new file mode 100755
index 0000000000..ebda1fccb0
--- /dev/null
+++ b/packages/zone.js/scripts/sauce/sauce_connect_block.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+
+# Wait for Connect to be ready before exiting
+printf "Connecting to Sauce."
+while [ ! -f $BROWSER_PROVIDER_READY_FILE ]; do
+ printf "."
+ #dart2js takes longer than the travis 10 min timeout to complete
+ sleep .5
+done
+echo "Connected"
\ No newline at end of file
diff --git a/packages/zone.js/scripts/sauce/sauce_connect_setup.sh b/packages/zone.js/scripts/sauce/sauce_connect_setup.sh
new file mode 100755
index 0000000000..5a88eaa53f
--- /dev/null
+++ b/packages/zone.js/scripts/sauce/sauce_connect_setup.sh
@@ -0,0 +1,49 @@
+#!/bin/bash
+
+set -e -o pipefail
+
+# Setup and start Sauce Connect for your TravisCI build
+# This script requires your .travis.yml to include the following two private env variables:
+# SAUCE_USERNAME
+# SAUCE_ACCESS_KEY
+# Follow the steps at https://saucelabs.com/opensource/travis to set that up.
+#
+# Curl and run this script as part of your .travis.yml before_script section:
+# before_script:
+# - curl https://gist.github.com/santiycr/5139565/raw/sauce_connect_setup.sh | bash
+
+CONNECT_URL="https://saucelabs.com/downloads/sc-4.3.14-linux.tar.gz"
+CONNECT_DIR="/tmp/sauce-connect-$RANDOM"
+CONNECT_DOWNLOAD="sc-latest-linux.tar.gz"
+
+CONNECT_LOG="$LOGS_DIR/sauce-connect"
+CONNECT_STDOUT="$LOGS_DIR/sauce-connect.stdout"
+CONNECT_STDERR="$LOGS_DIR/sauce-connect.stderr"
+
+# Get Connect and start it
+mkdir -p $CONNECT_DIR
+cd $CONNECT_DIR
+curl $CONNECT_URL -o $CONNECT_DOWNLOAD 2> /dev/null 1> /dev/null
+mkdir sauce-connect
+tar --extract --file=$CONNECT_DOWNLOAD --strip-components=1 --directory=sauce-connect > /dev/null
+rm $CONNECT_DOWNLOAD
+
+SAUCE_ACCESS_KEY=`echo $SAUCE_ACCESS_KEY | rev`
+
+ARGS=""
+
+# Set tunnel-id only on Travis, to make local testing easier.
+if [ ! -z "$TRAVIS_JOB_NUMBER" ]; then
+ ARGS="$ARGS --tunnel-identifier $TRAVIS_JOB_NUMBER"
+fi
+if [ ! -z "$BROWSER_PROVIDER_READY_FILE" ]; then
+ ARGS="$ARGS --readyfile $BROWSER_PROVIDER_READY_FILE"
+fi
+
+
+echo "Starting Sauce Connect in the background, logging into:"
+echo " $CONNECT_LOG"
+echo " $CONNECT_STDOUT"
+echo " $CONNECT_STDERR"
+sauce-connect/bin/sc -u $SAUCE_USERNAME -k $SAUCE_ACCESS_KEY $ARGS \
+ --reconnect 100 --no-ssl-bump-domains all --logfile $CONNECT_LOG 2> $CONNECT_STDERR 1> $CONNECT_STDOUT &
diff --git a/packages/zone.js/simple-server.js b/packages/zone.js/simple-server.js
new file mode 100644
index 0000000000..525883b2ef
--- /dev/null
+++ b/packages/zone.js/simple-server.js
@@ -0,0 +1,34 @@
+/**
+ * @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 http = require('http');
+const path = require('path');
+const fs = require('fs');
+let server;
+
+const localFolder = __dirname;
+
+function requestHandler(req, res) {
+ if (req.url === '/close') {
+ res.end('server closing');
+ setTimeout(() => { process.exit(0); }, 1000);
+ } else {
+ const file = localFolder + req.url;
+
+ fs.readFile(file, function(err, contents) {
+ if (!err) {
+ res.end(contents);
+ } else {
+ res.writeHead(404, {'Content-Type': 'text/html'});
+ res.end('404, Not Found! ');
+ };
+ });
+ };
+};
+
+server = http.createServer(requestHandler).listen(8080);
\ No newline at end of file
diff --git a/packages/zone.js/test/BUILD.bazel b/packages/zone.js/test/BUILD.bazel
new file mode 100644
index 0000000000..f8f4b93cd5
--- /dev/null
+++ b/packages/zone.js/test/BUILD.bazel
@@ -0,0 +1,376 @@
+load("//tools:defaults.bzl", "jasmine_node_test", "ts_library")
+load("@build_bazel_rules_nodejs//:defs.bzl", "rollup_bundle")
+load("@npm_bazel_karma//:index.bzl", "karma_web_test", "karma_web_test_suite")
+load("@build_bazel_rules_nodejs//:defs.bzl", "nodejs_binary")
+
+exports_files([
+ "assets/sample.json",
+ "assets/worker.js",
+ "assets/import.html",
+])
+
+ts_library(
+ name = "common_spec_env",
+ testonly = True,
+ srcs = glob([
+ "test_fake_polyfill.ts",
+ "wtf_mock.ts",
+ "test-env-setup-jasmine.ts",
+ ]),
+ deps = [
+ "//packages/zone.js/lib",
+ ],
+)
+
+ts_library(
+ name = "common_spec_srcs",
+ testonly = True,
+ srcs = glob(
+ [
+ "common/*.ts",
+ "zone-spec/*.ts",
+ "rxjs/*.ts",
+ ],
+ exclude = [
+ "common/Error.spec.ts",
+ ],
+ ),
+ deps = [
+ ":common_spec_util",
+ "//packages/zone.js/lib",
+ "@npm//rxjs",
+ ],
+)
+
+ts_library(
+ name = "common_spec_util",
+ testonly = True,
+ srcs = glob([
+ "test-util.ts",
+ ]),
+ deps = [
+ "//packages/zone.js/lib",
+ ],
+)
+
+ts_library(
+ name = "error_spec_srcs",
+ testonly = True,
+ srcs = glob([
+ "common/Error.spec.ts",
+ ]),
+ deps = [
+ ":common_spec_util",
+ "//packages/zone.js/lib",
+ ],
+)
+
+ts_library(
+ name = "test_node_lib",
+ testonly = True,
+ srcs = glob(
+ [
+ "node/*.ts",
+ "node-env-setup.ts",
+ "node_entry_point_common.ts",
+ "node_entry_point.ts",
+ "node_entry_point_no_patch_clock.ts",
+ "test-env-setup-jasmine-no-patch-clock.ts",
+ ],
+ ),
+ deps = [
+ ":common_spec_env",
+ ":common_spec_srcs",
+ ":common_spec_util",
+ "//packages/zone.js/lib",
+ "@npm//@types/shelljs",
+ "@npm//@types/systemjs",
+ "@npm//rxjs",
+ "@npm//shelljs",
+ "@npm//systemjs",
+ ],
+)
+
+ts_library(
+ name = "bluebird_spec",
+ testonly = True,
+ srcs = glob([
+ "extra/bluebird.spec.ts",
+ "node_bluebird_entry_point.ts",
+ ]),
+ deps = [
+ ":common_spec_env",
+ "//packages/zone.js/lib",
+ "@npm//bluebird",
+ ],
+)
+
+ts_library(
+ name = "error_spec",
+ testonly = True,
+ srcs = glob([
+ "node_error_entry_point.ts",
+ "node_error_disable_policy_entry_point.ts",
+ "node_error_lazy_policy_entry_point.ts",
+ ]),
+ deps = [
+ ":common_spec_env",
+ ":common_spec_util",
+ ":error_spec_srcs",
+ "//packages/zone.js/lib",
+ ],
+)
+
+jasmine_node_test(
+ name = "test_node",
+ bootstrap = [
+ "angular/packages/zone.js/test/node_entry_point.js",
+ ],
+ deps = [
+ ":test_node_lib",
+ ],
+)
+
+jasmine_node_test(
+ name = "test_node_no_jasmine_clock",
+ bootstrap = [
+ "angular/packages/zone.js/test/node_entry_point_no_patch_clock.js",
+ ],
+ deps = [
+ ":test_node_lib",
+ ],
+)
+
+jasmine_node_test(
+ name = "test_node_bluebird",
+ bootstrap = [
+ "angular/packages/zone.js/test/node_bluebird_entry_point.js",
+ ],
+ deps = [
+ ":bluebird_spec",
+ ],
+)
+
+jasmine_node_test(
+ name = "test_node_error_disable_policy",
+ bootstrap = [
+ "angular/packages/zone.js/test/node_error_disable_policy_entry_point.js",
+ ],
+ deps = [
+ ":error_spec",
+ ],
+)
+
+jasmine_node_test(
+ name = "test_node_error_lazy_policy",
+ bootstrap = [
+ "angular/packages/zone.js/test/node_error_lazy_policy_entry_point.js",
+ ],
+ deps = [
+ ":error_spec",
+ ],
+)
+
+ts_library(
+ name = "npm_package_spec_lib",
+ testonly = True,
+ srcs = ["npm_package/npm_package.spec.ts"],
+ deps = [
+ "@npm//@types",
+ ],
+)
+
+jasmine_node_test(
+ name = "test_npm_package",
+ srcs = [":npm_package_spec_lib"],
+ data = [
+ "//packages/zone.js:npm_package",
+ "@npm//shelljs",
+ ],
+)
+
+ts_library(
+ name = "test_browser_lib",
+ testonly = True,
+ srcs = glob(
+ [
+ "browser/*.ts",
+ "extra/cordova.spec.ts",
+ "mocha-patch.spec.ts",
+ "jasmine-patch.spec.ts",
+ "common_tests.ts",
+ "browser_entry_point.ts",
+ ],
+ ),
+ deps = [
+ ":common_spec_env",
+ ":common_spec_srcs",
+ ":common_spec_util",
+ ":error_spec_srcs",
+ "//packages/zone.js/lib",
+ "@npm//@types/shelljs",
+ "@npm//@types/systemjs",
+ "@npm//rxjs",
+ "@npm//shelljs",
+ "@npm//systemjs",
+ ],
+)
+
+ts_library(
+ name = "browser_env_setup",
+ testonly = True,
+ srcs = glob([
+ "browser-env-setup.ts",
+ "browser_symbol_setup.ts",
+ ]),
+ deps = [
+ ":common_spec_env",
+ ],
+)
+
+rollup_bundle(
+ name = "browser_test_env_setup_rollup",
+ testonly = True,
+ entry_point = ":browser-env-setup.ts",
+ deps = [
+ ":browser_env_setup",
+ ],
+)
+
+filegroup(
+ name = "browser_test_env_setup_rollup.es5",
+ testonly = True,
+ srcs = [":browser_test_env_setup_rollup"],
+ output_group = "umd",
+)
+
+rollup_bundle(
+ name = "browser_test_rollup",
+ testonly = True,
+ entry_point = ":browser_entry_point.ts",
+ globals = {
+ "electron": "electron",
+ },
+ deps = [
+ ":test_browser_lib",
+ ],
+)
+
+filegroup(
+ name = "browser_test_rollup.es5",
+ testonly = True,
+ srcs = [":browser_test_rollup"],
+ output_group = "umd",
+)
+
+genrule(
+ name = "browser_test_trim_map",
+ testonly = True,
+ srcs = [
+ ":browser_test_rollup.es5",
+ ],
+ outs = [
+ "browser_test_rollup_trim_map.js",
+ ],
+ cmd = " && ".join([
+ "cp $(@D)/browser_test_rollup.umd.js $@",
+ ]),
+)
+
+genrule(
+ name = "browser_test_env_setup_trim_map",
+ testonly = True,
+ srcs = [
+ ":browser_test_env_setup_rollup.es5",
+ ],
+ outs = [
+ "browser_test_env_setup_rollup_trim_map.js",
+ ],
+ cmd = " && ".join([
+ "cp $(@D)/browser_test_env_setup_rollup.umd.js $@",
+ ]),
+)
+
+_karma_test_required_dist_files = [
+ "//packages/zone.js/dist:task-tracking-dist-dev-test",
+ "//packages/zone.js/dist:wtf-dist-dev-test",
+ "//packages/zone.js/dist:webapis-notification-dist-dev-test",
+ "//packages/zone.js/dist:webapis-media-query-dist-dev-test",
+ "//packages/zone.js/dist:zone-patch-canvas-dist-dev-test",
+ "//packages/zone.js/dist:zone-patch-fetch-dist-dev-test",
+ "//packages/zone.js/dist:zone-patch-resize-observer-dist-dev-test",
+ "//packages/zone.js/dist:zone-patch-user-media-dist-dev-test",
+ ":browser_test_trim_map",
+]
+
+karma_web_test_suite(
+ name = "karma_jasmine_test",
+ srcs = [
+ "fake_entry.js",
+ ],
+ bootstrap = [
+ ":browser_test_env_setup_trim_map",
+ "//packages/zone.js/dist:zone-testing-bundle-dist-dev-test",
+ ] + _karma_test_required_dist_files,
+ static_files = [
+ ":assets/sample.json",
+ ":assets/worker.js",
+ ":assets/import.html",
+ ],
+ tags = ["zone_karma_test"],
+ runtime_deps = [
+ "@npm//karma-browserstack-launcher",
+ ],
+)
+
+karma_web_test_suite(
+ name = "karma_jasmine_evergreen_test",
+ srcs = [
+ "fake_entry.js",
+ ],
+ bootstrap = [
+ ":browser_test_env_setup_trim_map",
+ "//packages/zone.js/dist:zone-evergreen-dist-dev-test",
+ "//packages/zone.js/dist:zone-testing-dist-dev-test",
+ ] + _karma_test_required_dist_files,
+ data = [
+ "//:browser-providers.conf.js",
+ "//tools:jasmine-seed-generator.js",
+ ],
+ static_files = [
+ ":assets/sample.json",
+ ":assets/worker.js",
+ ":assets/import.html",
+ ],
+ tags = ["zone_karma_test"],
+ runtime_deps = [
+ "@npm//karma-browserstack-launcher",
+ ],
+)
+
+karma_web_test_suite(
+ name = "karma_jasmine_test_ci",
+ srcs = [
+ "fake_entry.js",
+ ],
+ bootstrap = [
+ ":saucelabs.js",
+ ":browser_test_env_setup_trim_map",
+ "//packages/zone.js/dist:zone-testing-bundle-dist-test",
+ ] + _karma_test_required_dist_files,
+ config_file = "//:karma-js.conf.js",
+ configuration_env_vars = ["KARMA_WEB_TEST_MODE"],
+ data = [
+ "//:browser-providers.conf.js",
+ "//tools:jasmine-seed-generator.js",
+ ],
+ static_files = [
+ ":assets/sample.json",
+ ":assets/worker.js",
+ ":assets/import.html",
+ ],
+ tags = ["zone_karma_test"],
+ runtime_deps = [
+ "@npm//karma-browserstack-launcher",
+ ],
+)
diff --git a/packages/zone.js/test/assets/import.html b/packages/zone.js/test/assets/import.html
new file mode 100644
index 0000000000..28f5df8830
--- /dev/null
+++ b/packages/zone.js/test/assets/import.html
@@ -0,0 +1 @@
+hey
diff --git a/packages/zone.js/test/assets/sample.json b/packages/zone.js/test/assets/sample.json
new file mode 100644
index 0000000000..56c8e28033
--- /dev/null
+++ b/packages/zone.js/test/assets/sample.json
@@ -0,0 +1 @@
+{"hello": "world"}
diff --git a/packages/zone.js/test/assets/worker.js b/packages/zone.js/test/assets/worker.js
new file mode 100644
index 0000000000..5c7a58f5b1
--- /dev/null
+++ b/packages/zone.js/test/assets/worker.js
@@ -0,0 +1,8 @@
+/**
+ * @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
+ */
+postMessage('worker');
\ No newline at end of file
diff --git a/packages/zone.js/test/browser-env-setup.ts b/packages/zone.js/test/browser-env-setup.ts
new file mode 100644
index 0000000000..76d3e66f93
--- /dev/null
+++ b/packages/zone.js/test/browser-env-setup.ts
@@ -0,0 +1,6 @@
+///
+
+import './browser_symbol_setup';
+import './test_fake_polyfill';
+import './wtf_mock';
+import './test-env-setup-jasmine';
diff --git a/packages/zone.js/test/browser-zone-setup.ts b/packages/zone.js/test/browser-zone-setup.ts
new file mode 100644
index 0000000000..3b3f4a809e
--- /dev/null
+++ b/packages/zone.js/test/browser-zone-setup.ts
@@ -0,0 +1,29 @@
+/**
+ * @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
+ */
+if (typeof window !== 'undefined') {
+ const zoneSymbol = (window as any).Zone.__symbol__;
+ (window as any)['__Zone_enable_cross_context_check'] = true;
+ (window as any)[zoneSymbol('fakeAsyncAutoFakeAsyncWhenClockPatched')] = true;
+}
+import '../lib/common/to-string';
+import '../lib/browser/api-util';
+import '../lib/browser/browser-legacy';
+import '../lib/browser/browser';
+import '../lib/browser/canvas';
+import '../lib/common/fetch';
+import '../lib/browser/webapis-user-media';
+import '../lib/browser/webapis-media-query';
+import '../lib/testing/zone-testing';
+import '../lib/zone-spec/task-tracking';
+import '../lib/zone-spec/wtf';
+import '../lib/extra/cordova';
+import '../lib/testing/promise-testing';
+import '../lib/testing/async-testing';
+import '../lib/testing/fake-async';
+import '../lib/browser/webapis-resize-observer';
+import '../lib/rxjs/rxjs-fake-async';
diff --git a/packages/zone.js/test/browser/FileReader.spec.ts b/packages/zone.js/test/browser/FileReader.spec.ts
new file mode 100644
index 0000000000..b84cd8dcdf
--- /dev/null
+++ b/packages/zone.js/test/browser/FileReader.spec.ts
@@ -0,0 +1,106 @@
+/**
+ * @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
+ */
+
+import {ifEnvSupports} from '../test-util';
+declare const global: any;
+
+describe('FileReader', ifEnvSupports('FileReader', function() {
+ let fileReader: FileReader;
+ let blob: Blob;
+ const data = 'Hello, World!';
+ const testZone = Zone.current.fork({name: 'TestZone'});
+
+ // Android 4.3's native browser doesn't implement add/RemoveEventListener for FileReader
+ function supportsEventTargetFns() {
+ return FileReader.prototype.addEventListener &&
+ FileReader.prototype.removeEventListener;
+ }
+ (supportsEventTargetFns).message =
+ 'FileReader#addEventListener and FileReader#removeEventListener';
+
+ beforeEach(function() {
+ fileReader = new FileReader();
+
+ try {
+ blob = new Blob([data]);
+ } catch (e) {
+ // For hosts that don't support the Blob ctor (e.g. Android 4.3's native browser)
+ const blobBuilder = new global['WebKitBlobBuilder']();
+ blobBuilder.append(data);
+
+ blob = blobBuilder.getBlob();
+ }
+ });
+
+ describe('EventTarget methods', ifEnvSupports(supportsEventTargetFns, function() {
+ it('should bind addEventListener listeners', function(done) {
+ testZone.run(function() {
+ fileReader.addEventListener('load', function() {
+ expect(Zone.current).toBe(testZone);
+ expect(fileReader.result).toEqual(data);
+ done();
+ });
+ });
+
+ fileReader.readAsText(blob);
+ });
+
+ it('should remove listeners via removeEventListener', function(done) {
+ const listenerSpy = jasmine.createSpy('listener');
+
+ testZone.run(function() {
+ fileReader.addEventListener('loadstart', listenerSpy);
+ fileReader.addEventListener('loadend', function() {
+ expect(listenerSpy).not.toHaveBeenCalled();
+ done();
+ });
+ });
+
+ fileReader.removeEventListener('loadstart', listenerSpy);
+ fileReader.readAsText(blob);
+ });
+ }));
+
+ it('should bind onEventType listeners', function(done) {
+ let listenersCalled = 0;
+
+ testZone.run(function() {
+ fileReader.onloadstart = function() {
+ listenersCalled++;
+ expect(Zone.current).toBe(testZone);
+ };
+
+ fileReader.onload = function() {
+ listenersCalled++;
+ expect(Zone.current).toBe(testZone);
+ };
+
+ fileReader.onloadend = function() {
+ listenersCalled++;
+
+ expect(Zone.current).toBe(testZone);
+ expect(fileReader.result).toEqual(data);
+ expect(listenersCalled).toBe(3);
+ done();
+ };
+ });
+
+ fileReader.readAsText(blob);
+ });
+
+ it('should have correct readyState', function(done) {
+ fileReader.onloadend = function() {
+ expect(fileReader.readyState).toBe((FileReader).DONE);
+ done();
+ };
+
+ expect(fileReader.readyState).toBe((FileReader).EMPTY);
+
+ fileReader.readAsText(blob);
+ });
+ }));
\ No newline at end of file
diff --git a/packages/zone.js/test/browser/HTMLImports.spec.ts b/packages/zone.js/test/browser/HTMLImports.spec.ts
new file mode 100644
index 0000000000..ef271ebc5d
--- /dev/null
+++ b/packages/zone.js/test/browser/HTMLImports.spec.ts
@@ -0,0 +1,79 @@
+/**
+ * @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
+ */
+
+import {ifEnvSupports} from '../test-util';
+
+function supportsImports() {
+ return 'import' in document.createElement('link');
+}
+(supportsImports).message = 'HTML Imports';
+
+describe('HTML Imports', ifEnvSupports(supportsImports, function() {
+ const testZone = Zone.current.fork({name: 'test'});
+
+ it('should work with addEventListener', function(done) {
+ let link: HTMLLinkElement;
+
+ testZone.run(function() {
+ link = document.createElement('link');
+ link.rel = 'import';
+ link.href = 'someUrl';
+ link.addEventListener('error', function() {
+ expect(Zone.current).toBe(testZone);
+ document.head.removeChild(link);
+ done();
+ });
+ });
+
+ document.head.appendChild(link !);
+ });
+
+ function supportsOnEvents() {
+ const link = document.createElement('link');
+ const linkPropDesc = Object.getOwnPropertyDescriptor(link, 'onerror');
+ return !(linkPropDesc && linkPropDesc.value === null);
+ }
+ (supportsOnEvents).message = 'Supports HTMLLinkElement#onxxx patching';
+
+
+ ifEnvSupports(supportsOnEvents, function() {
+ it('should work with onerror', function(done) {
+ let link: HTMLLinkElement;
+
+ testZone.run(function() {
+ link = document.createElement('link');
+ link.rel = 'import';
+ link.href = 'anotherUrl';
+ link.onerror = function() {
+ expect(Zone.current).toBe(testZone);
+ document.head.removeChild(link);
+ done();
+ };
+ });
+
+ document.head.appendChild(link !);
+ });
+
+ it('should work with onload', function(done) {
+ let link: HTMLLinkElement;
+
+ testZone.run(function() {
+ link = document.createElement('link');
+ link.rel = 'import';
+ link.href = '/base/angular/packages/zone.js/test/assets/import.html';
+ link.onload = function() {
+ expect(Zone.current).toBe(testZone);
+ document.head.removeChild(link);
+ done();
+ };
+ });
+
+ document.head.appendChild(link !);
+ });
+ });
+ }));
diff --git a/packages/zone.js/test/browser/MediaQuery.spec.ts b/packages/zone.js/test/browser/MediaQuery.spec.ts
new file mode 100644
index 0000000000..fcb6dfb39a
--- /dev/null
+++ b/packages/zone.js/test/browser/MediaQuery.spec.ts
@@ -0,0 +1,26 @@
+/**
+ * @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
+ */
+
+import {zoneSymbol} from '../../lib/common/utils';
+import {ifEnvSupports} from '../test-util';
+declare const global: any;
+
+function supportMediaQuery() {
+ const _global =
+ typeof window === 'object' && window || typeof self === 'object' && self || global;
+ return _global['MediaQueryList'] && _global['matchMedia'];
+}
+
+describe('test mediaQuery patch', ifEnvSupports(supportMediaQuery, () => {
+ it('test whether addListener is patched', () => {
+ const mqList = window.matchMedia('min-width:500px');
+ if (mqList && mqList['addListener']) {
+ expect((mqList as any)[zoneSymbol('addListener')]).toBeTruthy();
+ }
+ });
+ }));
diff --git a/packages/zone.js/test/browser/MutationObserver.spec.ts b/packages/zone.js/test/browser/MutationObserver.spec.ts
new file mode 100644
index 0000000000..2bbbb4abb0
--- /dev/null
+++ b/packages/zone.js/test/browser/MutationObserver.spec.ts
@@ -0,0 +1,67 @@
+/**
+ * @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
+ */
+
+import {ifEnvSupports} from '../test-util';
+declare const global: any;
+
+
+describe('MutationObserver', ifEnvSupports('MutationObserver', function() {
+ let elt: HTMLDivElement;
+ const testZone = Zone.current.fork({name: 'test'});
+
+ beforeEach(function() { elt = document.createElement('div'); });
+
+ it('should run observers within the zone', function(done) {
+ let ob;
+
+ testZone.run(function() {
+ ob = new MutationObserver(function() {
+ expect(Zone.current).toBe(testZone);
+ done();
+ });
+
+ ob.observe(elt, {childList: true});
+ });
+
+ elt.innerHTML = 'hey
';
+ });
+
+ it('should only dequeue upon disconnect if something is observed', function() {
+ let ob: MutationObserver;
+ let flag = false;
+ const elt = document.createElement('div');
+ const childZone =
+ Zone.current.fork({name: 'test', onInvokeTask: function() { flag = true; }});
+
+ childZone.run(function() { ob = new MutationObserver(function() {}); });
+
+ ob !.disconnect();
+ expect(flag).toBe(false);
+ });
+ }));
+
+describe('WebKitMutationObserver', ifEnvSupports('WebKitMutationObserver', function() {
+ const testZone = Zone.current.fork({name: 'test'});
+
+ it('should run observers within the zone', function(done) {
+ let elt: HTMLDivElement;
+
+ testZone.run(function() {
+ elt = document.createElement('div');
+
+ const ob = new global['WebKitMutationObserver'](function() {
+ expect(Zone.current).toBe(testZone);
+ done();
+ });
+
+ ob.observe(elt, {childList: true});
+ });
+
+ elt !.innerHTML = 'hey
';
+ });
+ }));
diff --git a/packages/zone.js/test/browser/Notification.spec.ts b/packages/zone.js/test/browser/Notification.spec.ts
new file mode 100644
index 0000000000..8e02739913
--- /dev/null
+++ b/packages/zone.js/test/browser/Notification.spec.ts
@@ -0,0 +1,26 @@
+/**
+ * @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
+ */
+
+import {zoneSymbol} from '../../lib/common/utils';
+import {ifEnvSupports} from '../test-util';
+declare const window: any;
+
+function notificationSupport() {
+ const desc = window['Notification'] &&
+ Object.getOwnPropertyDescriptor(window['Notification'].prototype, 'onerror');
+ return window['Notification'] && window['Notification'].prototype && desc && desc.configurable;
+}
+
+(notificationSupport).message = 'Notification Support';
+
+describe('Notification API', ifEnvSupports(notificationSupport, function() {
+ it('Notification API should be patched by Zone', () => {
+ const Notification = window['Notification'];
+ expect(Notification.prototype[zoneSymbol('addEventListener')]).toBeTruthy();
+ });
+ }));
diff --git a/packages/zone.js/test/browser/WebSocket.spec.ts b/packages/zone.js/test/browser/WebSocket.spec.ts
new file mode 100644
index 0000000000..77b2308abe
--- /dev/null
+++ b/packages/zone.js/test/browser/WebSocket.spec.ts
@@ -0,0 +1,138 @@
+/**
+ * @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
+ */
+
+import {ifEnvSupports} from '../test-util';
+declare const window: any;
+
+const TIMEOUT = 5000;
+
+if (!window['saucelabs']) {
+ // sauceLabs does not support WebSockets; skip these tests
+
+ xdescribe('WebSocket', ifEnvSupports('WebSocket', function() {
+ let socket: WebSocket;
+ const TEST_SERVER_URL = 'ws://localhost:8001';
+ const testZone = Zone.current.fork({name: 'test'});
+
+
+ beforeEach(function(done) {
+ socket = new WebSocket(TEST_SERVER_URL);
+ socket.addEventListener('open', function() { done(); });
+ socket.addEventListener('error', function() {
+ fail(
+ 'Can\'t establish socket to ' + TEST_SERVER_URL +
+ '! do you have test/ws-server.js running?');
+ done();
+ });
+ }, TIMEOUT);
+
+ afterEach(function(done) {
+ socket.addEventListener('close', function() { done(); });
+ socket.close();
+ }, TIMEOUT);
+
+ xit('should be patched in a Web Worker', done => {
+ const worker = new Worker('/base/test/ws-webworker-context.js');
+ worker.onmessage = (e: MessageEvent) => {
+ if (e.data !== 'pass' && e.data !== 'fail') {
+ fail(`web worker ${e.data}`);
+ return;
+ }
+ expect(e.data).toBe('pass');
+ done();
+ };
+ }, 10000);
+
+ it('should work with addEventListener', function(done) {
+ testZone.run(function() {
+ socket.addEventListener('message', function(event) {
+ expect(Zone.current).toBe(testZone);
+ expect(event['data']).toBe('hi');
+ done();
+ });
+ });
+ socket.send('hi');
+ }, TIMEOUT);
+
+
+ it('should respect removeEventListener', function(done) {
+ let log = '';
+
+ function logOnMessage() {
+ log += 'a';
+
+ expect(log).toEqual('a');
+
+ socket.removeEventListener('message', logOnMessage);
+ socket.send('hi');
+
+ setTimeout(function() {
+ expect(log).toEqual('a');
+ done();
+ }, 10);
+ }
+
+ socket.addEventListener('message', logOnMessage);
+ socket.send('hi');
+ }, TIMEOUT);
+
+
+ it('should work with onmessage', function(done) {
+ testZone.run(function() {
+ socket.onmessage = function(contents) {
+ expect(Zone.current).toBe(testZone);
+ expect(contents.data).toBe('hi');
+ done();
+ };
+ });
+ socket.send('hi');
+ }, TIMEOUT);
+
+
+ it('should only allow one onmessage handler', function(done) {
+ let log = '';
+
+ socket.onmessage = function() {
+ log += 'a';
+ expect(log).toEqual('b');
+ done();
+ };
+
+ socket.onmessage = function() {
+ log += 'b';
+ expect(log).toEqual('b');
+ done();
+ };
+
+ socket.send('hi');
+ }, TIMEOUT);
+
+
+ it('should handler removing onmessage', function(done) {
+ let log = '';
+
+ socket.onmessage = function() { log += 'a'; };
+
+ socket.onmessage = null as any;
+
+ socket.send('hi');
+
+ setTimeout(function() {
+ expect(log).toEqual('');
+ done();
+ }, 100);
+ }, TIMEOUT);
+
+ it('should have constants', function() {
+ expect(Object.keys(WebSocket)).toContain('CONNECTING');
+ expect(Object.keys(WebSocket)).toContain('OPEN');
+ expect(Object.keys(WebSocket)).toContain('CLOSING');
+ expect(Object.keys(WebSocket)).toContain('CLOSED');
+ });
+ }));
+}
diff --git a/packages/zone.js/test/browser/Worker.spec.ts b/packages/zone.js/test/browser/Worker.spec.ts
new file mode 100644
index 0000000000..27681ac8e0
--- /dev/null
+++ b/packages/zone.js/test/browser/Worker.spec.ts
@@ -0,0 +1,39 @@
+/**
+ * @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
+ */
+
+import {zoneSymbol} from '../../lib/common/utils';
+import {asyncTest, ifEnvSupports} from '../test-util';
+
+function workerSupport() {
+ const Worker = (window as any)['Worker'];
+ if (!Worker) {
+ return false;
+ }
+ const desc = Object.getOwnPropertyDescriptor(Worker.prototype, 'onmessage');
+ if (!desc || !desc.configurable) {
+ return false;
+ }
+ return true;
+}
+
+(workerSupport as any).message = 'Worker Support';
+
+xdescribe('Worker API', ifEnvSupports(workerSupport, function() {
+ it('Worker API should be patched by Zone', asyncTest((done: Function) => {
+ const zone: Zone = Zone.current.fork({name: 'worker'});
+ zone.run(() => {
+ const worker =
+ new Worker('/base/angular/packages/zone.js/test/assets/worker.js');
+ worker.onmessage = function(evt: MessageEvent) {
+ expect(evt.data).toEqual('worker');
+ expect(Zone.current.name).toEqual('worker');
+ done();
+ };
+ });
+ }, Zone.root));
+ }));
diff --git a/packages/zone.js/test/browser/XMLHttpRequest.spec.ts b/packages/zone.js/test/browser/XMLHttpRequest.spec.ts
new file mode 100644
index 0000000000..d70f90462f
--- /dev/null
+++ b/packages/zone.js/test/browser/XMLHttpRequest.spec.ts
@@ -0,0 +1,381 @@
+/**
+ * @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
+ */
+
+import {ifEnvSupports, ifEnvSupportsWithDone, supportPatchXHROnProperty, zoneSymbol} from '../test-util';
+declare const global: any;
+const wtfMock = global.wtfMock;
+
+describe('XMLHttpRequest', function() {
+ let testZone: Zone;
+
+ beforeEach(() => { testZone = Zone.current.fork({name: 'test'}); });
+
+ it('should intercept XHRs and treat them as MacroTasks', function(done) {
+ let req: XMLHttpRequest;
+ let onStable: any;
+ const testZoneWithWtf = Zone.current.fork((Zone as any)['wtfZoneSpec']).fork({
+ name: 'TestZone',
+ onHasTask: (delegate: ZoneDelegate, curr: Zone, target: Zone, hasTask: HasTaskState) => {
+ if (!hasTask.macroTask) {
+ onStable && onStable();
+ }
+ }
+ });
+
+ testZoneWithWtf.run(() => {
+ req = new XMLHttpRequest();
+ const logs: string[] = [];
+ req.onload = () => { logs.push('onload'); };
+ onStable = function() {
+ expect(wtfMock.log[wtfMock.log.length - 2])
+ .toEqual('> Zone:invokeTask:XMLHttpRequest.send("::ProxyZone::WTF::TestZone")');
+ expect(wtfMock.log[wtfMock.log.length - 1])
+ .toEqual('< Zone:invokeTask:XMLHttpRequest.send');
+ if (supportPatchXHROnProperty()) {
+ expect(wtfMock.log[wtfMock.log.length - 3])
+ .toMatch(/\< Zone\:invokeTask.*addEventListener\:load/);
+ expect(wtfMock.log[wtfMock.log.length - 4])
+ .toMatch(/\> Zone\:invokeTask.*addEventListener\:load/);
+ }
+ // if browser can patch onload
+ if ((req as any)[zoneSymbol('loadfalse')]) {
+ expect(logs).toEqual(['onload']);
+ }
+ done();
+ };
+
+ req.open('get', '/', true);
+ req.send();
+ const lastScheduled = wtfMock.log[wtfMock.log.length - 1];
+ expect(lastScheduled).toMatch('# Zone:schedule:macroTask:XMLHttpRequest.send');
+ }, null, undefined, 'unit-test');
+ });
+
+ it('should not trigger Zone callback of internal onreadystatechange', function(done) {
+ const scheduleSpy = jasmine.createSpy('schedule');
+ const xhrZone = Zone.current.fork({
+ name: 'xhr',
+ onScheduleTask: (delegate: ZoneDelegate, currentZone: Zone, targetZone, task: Task) => {
+ if (task.type === 'eventTask') {
+ scheduleSpy(task.source);
+ }
+ return delegate.scheduleTask(targetZone, task);
+ }
+ });
+
+ xhrZone.run(() => {
+ const req = new XMLHttpRequest();
+ req.onload = function() {
+ expect(Zone.current.name).toEqual('xhr');
+ if (supportPatchXHROnProperty()) {
+ expect(scheduleSpy).toHaveBeenCalled();
+ }
+ done();
+ };
+ req.open('get', '/', true);
+ req.send();
+ });
+ });
+
+ it('should work with onreadystatechange', function(done) {
+ let req: XMLHttpRequest;
+
+ testZone.run(function() {
+ req = new XMLHttpRequest();
+ req.onreadystatechange = function() {
+ // Make sure that the wrapCallback will only be called once
+ req.onreadystatechange = null as any;
+ expect(Zone.current).toBe(testZone);
+ done();
+ };
+ req.open('get', '/', true);
+ });
+
+ req !.send();
+ });
+
+ it('should return null when access ontimeout first time without error', function() {
+ let req: XMLHttpRequest = new XMLHttpRequest();
+ expect(req.ontimeout).toBe(null);
+ });
+
+ const supportsOnProgress = function() { return 'onprogress' in (new XMLHttpRequest()); };
+
+ (supportsOnProgress).message = 'XMLHttpRequest.onprogress';
+
+ describe('onprogress', ifEnvSupports(supportsOnProgress, function() {
+ it('should work with onprogress', function(done) {
+ let req: XMLHttpRequest;
+ testZone.run(function() {
+ req = new XMLHttpRequest();
+ req.onprogress = function() {
+ // Make sure that the wrapCallback will only be called once
+ req.onprogress = null as any;
+ expect(Zone.current).toBe(testZone);
+ done();
+ };
+ req.open('get', '/', true);
+ });
+
+ req !.send();
+ });
+
+ it('should allow canceling of an XMLHttpRequest', function(done) {
+ const spy = jasmine.createSpy('spy');
+ let req: XMLHttpRequest;
+ let pending = false;
+
+ const trackingTestZone = Zone.current.fork({
+ name: 'tracking test zone',
+ onHasTask: (delegate: ZoneDelegate, current: Zone, target: Zone,
+ hasTaskState: HasTaskState) => {
+ if (hasTaskState.change == 'macroTask') {
+ pending = hasTaskState.macroTask;
+ }
+ delegate.hasTask(target, hasTaskState);
+ }
+ });
+
+ trackingTestZone.run(function() {
+ req = new XMLHttpRequest();
+ req.onreadystatechange = function() {
+ if (req.readyState === XMLHttpRequest.DONE) {
+ if (req.status !== 0) {
+ spy();
+ }
+ }
+ };
+ req.open('get', '/', true);
+
+ req.send();
+ req.abort();
+ });
+
+ setTimeout(function() {
+ expect(spy).not.toHaveBeenCalled();
+ expect(pending).toEqual(false);
+ done();
+ }, 0);
+ });
+
+ it('should allow aborting an XMLHttpRequest after its completed', function(done) {
+ let req: XMLHttpRequest;
+
+ testZone.run(function() {
+ req = new XMLHttpRequest();
+ req.onreadystatechange = function() {
+ if (req.readyState === XMLHttpRequest.DONE) {
+ if (req.status !== 0) {
+ setTimeout(function() {
+ req.abort();
+ done();
+ }, 0);
+ }
+ }
+ };
+ req.open('get', '/', true);
+
+ req.send();
+ });
+ });
+ }));
+
+ it('should preserve other setters', function() {
+ const req = new XMLHttpRequest();
+ req.open('get', '/', true);
+ req.send();
+ try {
+ req.responseType = 'document';
+ expect(req.responseType).toBe('document');
+ } catch (e) {
+ // Android browser: using this setter throws, this should be preserved
+ expect(e.message).toBe('INVALID_STATE_ERR: DOM Exception 11');
+ }
+ });
+
+ it('should work with synchronous XMLHttpRequest', function() {
+ const log: HasTaskState[] = [];
+ Zone.current
+ .fork({
+ name: 'sync-xhr-test',
+ onHasTask: function(
+ delegate: ZoneDelegate, current: Zone, target: Zone, hasTaskState: HasTaskState) {
+ log.push(hasTaskState);
+ delegate.hasTask(target, hasTaskState);
+ }
+ })
+ .run(() => {
+ const req = new XMLHttpRequest();
+ req.open('get', '/', false);
+ req.send();
+ });
+ expect(log).toEqual([]);
+ });
+
+ it('should preserve static constants', function() {
+ expect(XMLHttpRequest.UNSENT).toEqual(0);
+ expect(XMLHttpRequest.OPENED).toEqual(1);
+ expect(XMLHttpRequest.HEADERS_RECEIVED).toEqual(2);
+ expect(XMLHttpRequest.LOADING).toEqual(3);
+ expect(XMLHttpRequest.DONE).toEqual(4);
+ });
+
+ it('should work properly when send request multiple times on single xmlRequest instance',
+ function(done) {
+ testZone.run(function() {
+ const req = new XMLHttpRequest();
+ req.open('get', '/', true);
+ req.send();
+ req.onload = function() {
+ req.onload = null as any;
+ req.open('get', '/', true);
+ req.onload = function() { done(); };
+ expect(() => { req.send(); }).not.toThrow();
+ };
+ });
+ });
+
+ it('should keep taskcount correctly when abort was called multiple times before request is done',
+ function(done) {
+ testZone.run(function() {
+ const req = new XMLHttpRequest();
+ req.open('get', '/', true);
+ req.send();
+ req.addEventListener('readystatechange', function(ev) {
+ if (req.readyState >= 2) {
+ expect(() => { req.abort(); }).not.toThrow();
+ done();
+ }
+ });
+ });
+ });
+
+ it('should trigger readystatechange if xhr request trigger cors error', (done) => {
+ const req = new XMLHttpRequest();
+ let err: any = null;
+ try {
+ req.open('get', 'file:///test', true);
+ } catch (err) {
+ // in IE, open will throw Access is denied error
+ done();
+ return;
+ }
+ req.addEventListener('readystatechange', function(ev) {
+ if (req.readyState === 4) {
+ const xhrScheduled = (req as any)[zoneSymbol('xhrScheduled')];
+ const task = (req as any)[zoneSymbol('xhrTask')];
+ if (xhrScheduled === false) {
+ expect(task.state).toEqual('scheduling');
+ setTimeout(() => {
+ if (err) {
+ expect(task.state).toEqual('unknown');
+ } else {
+ expect(task.state).toEqual('notScheduled');
+ }
+ done();
+ });
+ } else {
+ expect(task.state).toEqual('scheduled');
+ done();
+ }
+ }
+ });
+ try {
+ req.send();
+ } catch (error) {
+ err = error;
+ }
+ });
+
+ it('should invoke task if xhr request trigger cors error', (done) => {
+ const logs: string[] = [];
+ const zone = Zone.current.fork({
+ name: 'xhr',
+ onHasTask: (delegate: ZoneDelegate, curr: Zone, target: Zone, hasTask: HasTaskState) => {
+ logs.push(JSON.stringify(hasTask));
+ }
+ });
+ const req = new XMLHttpRequest();
+ try {
+ req.open('get', 'file:///test', true);
+ } catch (err) {
+ // in IE, open will throw Access is denied error
+ done();
+ return;
+ }
+ zone.run(() => {
+ let isError = false;
+ let timerId = null;
+ try {
+ timerId = (window as any)[zoneSymbol('setTimeout')](() => {
+ expect(logs).toEqual([
+ `{"microTask":false,"macroTask":true,"eventTask":false,"change":"macroTask"}`,
+ `{"microTask":false,"macroTask":false,"eventTask":false,"change":"macroTask"}`
+ ]);
+ done();
+ }, 500);
+ req.send();
+ } catch (error) {
+ isError = true;
+ (window as any)[zoneSymbol('clearTimeout')](timerId);
+ done();
+ }
+ });
+ });
+
+ it('should not throw error when get XMLHttpRequest.prototype.onreadystatechange the first time',
+ function() {
+ const func = function() {
+ testZone.run(function() {
+ const req = new XMLHttpRequest();
+ req.onreadystatechange;
+ });
+ };
+ expect(func).not.toThrow();
+ });
+
+ it('should be in the zone when use XMLHttpRequest.addEventListener', function(done) {
+ testZone.run(function() {
+ // sometimes this case will cause timeout
+ // so we set it longer
+ const interval = (jasmine).DEFAULT_TIMEOUT_INTERVAL;
+ (jasmine).DEFAULT_TIMEOUT_INTERVAL = 5000;
+ const req = new XMLHttpRequest();
+ req.open('get', '/', true);
+ req.addEventListener('readystatechange', function() {
+ if (req.readyState === 4) {
+ // expect(Zone.current.name).toEqual('test');
+ (jasmine).DEFAULT_TIMEOUT_INTERVAL = interval;
+ done();
+ }
+ });
+ req.send();
+ });
+ });
+
+ it('should return origin listener when call xhr.onreadystatechange',
+ ifEnvSupportsWithDone(supportPatchXHROnProperty, function(done: Function) {
+ testZone.run(function() {
+ // sometimes this case will cause timeout
+ // so we set it longer
+ const req = new XMLHttpRequest();
+ req.open('get', '/', true);
+ const interval = (jasmine).DEFAULT_TIMEOUT_INTERVAL;
+ (jasmine).DEFAULT_TIMEOUT_INTERVAL = 5000;
+ const listener = req.onreadystatechange = function() {
+ if (req.readyState === 4) {
+ (jasmine).DEFAULT_TIMEOUT_INTERVAL = interval;
+ done();
+ }
+ };
+ expect(req.onreadystatechange).toBe(listener);
+ req.onreadystatechange = function() { return listener.call(this); };
+ req.send();
+ });
+ }));
+});
diff --git a/packages/zone.js/test/browser/browser.spec.ts b/packages/zone.js/test/browser/browser.spec.ts
new file mode 100644
index 0000000000..bc41ea7810
--- /dev/null
+++ b/packages/zone.js/test/browser/browser.spec.ts
@@ -0,0 +1,2363 @@
+/**
+ * @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
+ */
+
+import {patchFilteredProperties} from '../../lib/browser/property-descriptor';
+import {patchEventTarget} from '../../lib/common/events';
+import {isIEOrEdge, zoneSymbol} from '../../lib/common/utils';
+import {getEdgeVersion, getIEVersion, ifEnvSupports, ifEnvSupportsWithDone, isEdge} from '../test-util';
+
+import Spy = jasmine.Spy;
+declare const global: any;
+
+const noop = function() {};
+
+function windowPrototype() {
+ return !!(global['Window'] && global['Window'].prototype);
+}
+
+function promiseUnhandleRejectionSupport() {
+ return !!global['PromiseRejectionEvent'];
+}
+
+function canPatchOnProperty(obj: any, prop: string) {
+ const func = function() {
+ if (!obj) {
+ return false;
+ }
+ const desc = Object.getOwnPropertyDescriptor(obj, prop);
+ if (!desc || !desc.configurable) {
+ return false;
+ }
+ return true;
+ };
+
+ (func as any).message = 'patchOnProperties';
+ return func;
+}
+
+let supportsPassive = false;
+try {
+ const opts = Object.defineProperty({}, 'passive', {get: function() { supportsPassive = true; }});
+ window.addEventListener('test', opts as any, opts);
+ window.removeEventListener('test', opts as any, opts);
+} catch (e) {
+}
+
+function supportEventListenerOptions() {
+ return supportsPassive;
+}
+
+(supportEventListenerOptions as any).message = 'supportsEventListenerOptions';
+
+function supportCanvasTest() {
+ const HTMLCanvasElement = (window as any)['HTMLCanvasElement'];
+ const supportCanvas = typeof HTMLCanvasElement !== 'undefined' && HTMLCanvasElement.prototype &&
+ HTMLCanvasElement.prototype.toBlob;
+ const FileReader = (window as any)['FileReader'];
+ const supportFileReader = typeof FileReader !== 'undefined';
+ return supportCanvas && supportFileReader;
+}
+
+(supportCanvasTest as any).message = 'supportCanvasTest';
+
+function ieOrEdge() {
+ return isIEOrEdge();
+}
+
+(ieOrEdge as any).message = 'IE/Edge Test';
+
+class TestEventListener {
+ logs: any[] = [];
+ addEventListener(eventName: string, listener: any, options: any) { this.logs.push(options); }
+ removeEventListener(eventName: string, listener: any, options: any) {}
+}
+
+describe('Zone', function() {
+ const rootZone = Zone.current;
+ (Zone as any)[zoneSymbol('ignoreConsoleErrorUncaughtError')] = true;
+
+ describe('hooks', function() {
+ it('should allow you to override alert/prompt/confirm', function() {
+ const alertSpy = jasmine.createSpy('alert');
+ const promptSpy = jasmine.createSpy('prompt');
+ const confirmSpy = jasmine.createSpy('confirm');
+ const spies: {[k: string]:
+ Function} = {'alert': alertSpy, 'prompt': promptSpy, 'confirm': confirmSpy};
+ const myZone = Zone.current.fork({
+ name: 'spy',
+ onInvoke: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
+ callback: Function, applyThis?: any, applyArgs?: any[],
+ source?: string): any => {
+ if (source) {
+ spies[source].apply(null, applyArgs);
+ } else {
+ return parentZoneDelegate.invoke(targetZone, callback, applyThis, applyArgs, source);
+ }
+ }
+ });
+
+ myZone.run(function() {
+ alert('alertMsg');
+ prompt('promptMsg', 'default');
+ confirm('confirmMsg');
+ });
+
+ expect(alertSpy).toHaveBeenCalledWith('alertMsg');
+ expect(promptSpy).toHaveBeenCalledWith('promptMsg', 'default');
+ expect(confirmSpy).toHaveBeenCalledWith('confirmMsg');
+ });
+
+ describe(
+ 'DOM onProperty hooks',
+ ifEnvSupports(canPatchOnProperty(HTMLElement.prototype, 'onclick'), function() {
+ let mouseEvent = document.createEvent('Event');
+ let hookSpy: Spy, eventListenerSpy: Spy;
+ const zone = rootZone.fork({
+ name: 'spy',
+ onScheduleTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
+ task: Task): any => {
+ hookSpy();
+ return parentZoneDelegate.scheduleTask(targetZone, task);
+ }
+ });
+
+ beforeEach(function() {
+ mouseEvent.initEvent('mousedown', true, true);
+ hookSpy = jasmine.createSpy('hook');
+ eventListenerSpy = jasmine.createSpy('eventListener');
+ });
+
+ function checkIsOnPropertiesPatched(target: any, ignoredProperties?: string[]) {
+ for (let prop in target) {
+ if (ignoredProperties &&
+ ignoredProperties.filter(ignoreProp => ignoreProp === prop).length > 0) {
+ continue;
+ }
+ if (prop.substr(0, 2) === 'on' && prop.length > 2) {
+ target[prop] = noop;
+ if (!target[Zone.__symbol__('ON_PROPERTY' + prop.substr(2))]) {
+ console.log('onProp is null:', prop);
+ } else {
+ target[prop] = null;
+ expect(!target[Zone.__symbol__('ON_PROPERTY' + prop.substr(2))]).toBeTruthy();
+ }
+ }
+ }
+ }
+
+ it('should patch all possbile on properties on element', function() {
+ const htmlElementTagNames: string[] = [
+ 'a', 'area', 'audio', 'base', 'basefont', 'blockquote', 'br',
+ 'button', 'canvas', 'caption', 'col', 'colgroup', 'data', 'datalist',
+ 'del', 'dir', 'div', 'dl', 'embed', 'fieldset', 'font',
+ 'form', 'frame', 'frameset', 'h1', 'h2', 'h3', 'h4',
+ 'h5', 'h6', 'head', 'hr', 'html', 'iframe', 'img',
+ 'input', 'ins', 'isindex', 'label', 'legend', 'li', 'link',
+ 'listing', 'map', 'marquee', 'menu', 'meta', 'meter', 'nextid',
+ 'ol', 'optgroup', 'option', 'output', 'p', 'param', 'picture',
+ 'pre', 'progress', 'q', 'script', 'select', 'source', 'span',
+ 'style', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot',
+ 'th', 'thead', 'time', 'title', 'tr', 'track', 'ul',
+ 'video'
+ ];
+ htmlElementTagNames.forEach(tagName => {
+ checkIsOnPropertiesPatched(document.createElement(tagName), ['onorientationchange']);
+ });
+ });
+
+ it('should patch all possbile on properties on body',
+ function() { checkIsOnPropertiesPatched(document.body, ['onorientationchange']); });
+
+ it('should patch all possbile on properties on Document',
+ function() { checkIsOnPropertiesPatched(document, ['onorientationchange']); });
+
+ it('should patch all possbile on properties on Window', function() {
+ checkIsOnPropertiesPatched(window, [
+ 'onvrdisplayactivate', 'onvrdisplayblur', 'onvrdisplayconnect',
+ 'onvrdisplaydeactivate', 'onvrdisplaydisconnect', 'onvrdisplayfocus',
+ 'onvrdisplaypointerrestricted', 'onvrdisplaypointerunrestricted',
+ 'onorientationchange', 'onerror'
+ ]);
+ });
+
+ it('should patch all possbile on properties on xhr',
+ function() { checkIsOnPropertiesPatched(new XMLHttpRequest()); });
+
+ it('should not patch ignored on properties', function() {
+ const TestTarget: any = (window as any)['TestTarget'];
+ patchFilteredProperties(
+ TestTarget.prototype, ['prop1', 'prop2'], global['__Zone_ignore_on_properties']);
+ const testTarget = new TestTarget();
+ Zone.current.fork({name: 'test'}).run(() => {
+ testTarget.onprop1 = function() {
+ // onprop1 should not be patched
+ expect(Zone.current.name).toEqual('test1');
+ };
+ testTarget.onprop2 = function() {
+ // onprop2 should be patched
+ expect(Zone.current.name).toEqual('test');
+ };
+ });
+
+ Zone.current.fork({name: 'test1'}).run(() => {
+ testTarget.dispatchEvent('prop1');
+ testTarget.dispatchEvent('prop2');
+ });
+ });
+
+ it('should not patch ignored eventListener', function() {
+ let scrollEvent = document.createEvent('Event');
+ scrollEvent.initEvent('scroll', true, true);
+
+ const zone = Zone.current.fork({name: 'run'});
+
+ Zone.current.fork({name: 'scroll'}).run(() => {
+ document.addEventListener(
+ 'scroll', () => { expect(Zone.current.name).toEqual(zone.name); });
+ });
+
+ zone.run(() => { document.dispatchEvent(scrollEvent); });
+ });
+
+ it('should be able to clear on handler added before load zone.js', function() {
+ const TestTarget: any = (window as any)['TestTarget'];
+ patchFilteredProperties(
+ TestTarget.prototype, ['prop3'], global['__Zone_ignore_on_properties']);
+ const testTarget = new TestTarget();
+ Zone.current.fork({name: 'test'}).run(() => {
+ expect(testTarget.onprop3).toBeTruthy();
+ const newProp3Handler = function() {};
+ testTarget.onprop3 = newProp3Handler;
+ expect(testTarget.onprop3).toBe(newProp3Handler);
+ testTarget.onprop3 = null;
+ expect(!testTarget.onprop3).toBeTruthy();
+ testTarget.onprop3 = function() {
+ // onprop1 should not be patched
+ expect(Zone.current.name).toEqual('test');
+ };
+ });
+
+ Zone.current.fork({name: 'test1'}).run(() => { testTarget.dispatchEvent('prop3'); });
+ });
+
+ it('window onclick should be in zone',
+ ifEnvSupports(canPatchOnProperty(window, 'onmousedown'), function() {
+ zone.run(function() { window.onmousedown = eventListenerSpy; });
+
+ window.dispatchEvent(mouseEvent);
+
+ expect(hookSpy).toHaveBeenCalled();
+ expect(eventListenerSpy).toHaveBeenCalled();
+ window.removeEventListener('mousedown', eventListenerSpy);
+ }));
+
+ it('window onresize should be patched',
+ ifEnvSupports(canPatchOnProperty(window, 'onmousedown'), function() {
+ window.onresize = eventListenerSpy;
+ const innerResizeProp: any = (window as any)[zoneSymbol('ON_PROPERTYresize')];
+ expect(innerResizeProp).toBeTruthy();
+ innerResizeProp();
+ expect(eventListenerSpy).toHaveBeenCalled();
+ window.removeEventListener('resize', eventListenerSpy);
+ }));
+
+ it('document onclick should be in zone',
+ ifEnvSupports(canPatchOnProperty(Document.prototype, 'onmousedown'), function() {
+ zone.run(function() { document.onmousedown = eventListenerSpy; });
+
+ document.dispatchEvent(mouseEvent);
+
+ expect(hookSpy).toHaveBeenCalled();
+ expect(eventListenerSpy).toHaveBeenCalled();
+ document.removeEventListener('mousedown', eventListenerSpy);
+ }));
+
+ // TODO: JiaLiPassion, need to find out why the test bundle is not `use strict`.
+ xit('event handler with null context should use event.target',
+ ifEnvSupports(canPatchOnProperty(Document.prototype, 'onmousedown'), function() {
+ const ieVer = getIEVersion();
+ if (ieVer && ieVer === 9) {
+ // in ie9, this is window object even we call func.apply(undefined)
+ return;
+ }
+ const logs: string[] = [];
+ const EventTarget = (window as any)['EventTarget'];
+ let oriAddEventListener = EventTarget && EventTarget.prototype ?
+ (EventTarget.prototype as any)[zoneSymbol('addEventListener')] :
+ (HTMLSpanElement.prototype as any)[zoneSymbol('addEventListener')];
+
+ if (!oriAddEventListener) {
+ // no patched addEventListener found
+ return;
+ }
+ let handler1: Function;
+ let handler2: Function;
+
+ const listener = function() { logs.push('listener1'); };
+
+ const listener1 = function() { logs.push('listener2'); };
+
+ HTMLSpanElement.prototype.addEventListener = function(
+ eventName: string, callback: any) {
+ if (eventName === 'click') {
+ handler1 = callback;
+ } else if (eventName === 'mousedown') {
+ handler2 = callback;
+ }
+ return oriAddEventListener.apply(this, arguments);
+ };
+
+ (HTMLSpanElement.prototype as any)[zoneSymbol('addEventListener')] = null;
+
+ patchEventTarget(window, [HTMLSpanElement.prototype]);
+
+ const span = document.createElement('span');
+ document.body.appendChild(span);
+
+ zone.run(function() {
+ span.addEventListener('click', listener);
+ span.onmousedown = listener1;
+ });
+
+ expect(handler1 !).toBe(handler2 !);
+
+ handler1 !.apply(null, [{type: 'click', target: span}]);
+
+ handler2 !.apply(null, [{type: 'mousedown', target: span}]);
+
+ expect(hookSpy).toHaveBeenCalled();
+ expect(logs).toEqual(['listener1', 'listener2']);
+ document.body.removeChild(span);
+ if (EventTarget) {
+ (EventTarget.prototype as any)[zoneSymbol('addEventListener')] =
+ oriAddEventListener;
+ } else {
+ (HTMLSpanElement.prototype as any)[zoneSymbol('addEventListener')] =
+ oriAddEventListener;
+ }
+ }));
+
+ it('SVGElement onclick should be in zone',
+ ifEnvSupports(
+ canPatchOnProperty(SVGElement && SVGElement.prototype, 'onmousedown'), function() {
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+ document.body.appendChild(svg);
+ zone.run(function() { svg.onmousedown = eventListenerSpy; });
+
+ svg.dispatchEvent(mouseEvent);
+
+ expect(hookSpy).toHaveBeenCalled();
+ expect(eventListenerSpy).toHaveBeenCalled();
+ svg.removeEventListener('mouse', eventListenerSpy);
+ document.body.removeChild(svg);
+ }));
+
+ it('get window onerror should not throw error',
+ ifEnvSupports(canPatchOnProperty(window, 'onerror'), function() {
+ const testFn = function() {
+ let onerror = window.onerror;
+ window.onerror = function() {};
+ onerror = window.onerror;
+ };
+ expect(testFn).not.toThrow();
+ }));
+
+ it('window.onerror callback signiture should be (message, source, lineno, colno, error)',
+ ifEnvSupportsWithDone(canPatchOnProperty(window, 'onerror'), function(done: DoneFn) {
+ let testError = new Error('testError');
+ window.onerror = function(
+ message: any, source?: string, lineno?: number, colno?: number, error?: any) {
+ expect(message).toContain('testError');
+ if (getEdgeVersion() !== 14) {
+ // Edge 14, error will be undefined.
+ expect(error).toBe(testError);
+ }
+ (window as any).onerror = null;
+ setTimeout(done);
+ return true;
+ };
+ setTimeout(() => { throw testError; }, 100);
+ }));
+ }));
+
+ describe('eventListener hooks', function() {
+ let button: HTMLButtonElement;
+ let clickEvent: Event;
+
+ beforeEach(function() {
+ button = document.createElement('button');
+ clickEvent = document.createEvent('Event');
+ clickEvent.initEvent('click', true, true);
+ document.body.appendChild(button);
+ });
+
+ afterEach(function() { document.body.removeChild(button); });
+
+ it('should support addEventListener', function() {
+ const hookSpy = jasmine.createSpy('hook');
+ const eventListenerSpy = jasmine.createSpy('eventListener');
+ const zone = rootZone.fork({
+ name: 'spy',
+ onScheduleTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
+ task: Task): any => {
+ hookSpy();
+ return parentZoneDelegate.scheduleTask(targetZone, task);
+ }
+ });
+
+ zone.run(function() { button.addEventListener('click', eventListenerSpy); });
+
+ button.dispatchEvent(clickEvent);
+
+ expect(hookSpy).toHaveBeenCalled();
+ expect(eventListenerSpy).toHaveBeenCalled();
+ });
+
+ it('should be able to access addEventListener information in onScheduleTask', function() {
+ const hookSpy = jasmine.createSpy('hook');
+ const eventListenerSpy = jasmine.createSpy('eventListener');
+ let scheduleButton;
+ let scheduleEventName: string|undefined;
+ let scheduleCapture: boolean|undefined;
+ let scheduleTask;
+ const zone = rootZone.fork({
+ name: 'spy',
+ onScheduleTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
+ task: Task): any => {
+ hookSpy();
+ scheduleButton = (task.data as any).taskData.target;
+ scheduleEventName = (task.data as any).taskData.eventName;
+ scheduleCapture = (task.data as any).taskData.capture;
+ scheduleTask = task;
+ return parentZoneDelegate.scheduleTask(targetZone, task);
+ }
+ });
+
+ zone.run(function() { button.addEventListener('click', eventListenerSpy); });
+
+ button.dispatchEvent(clickEvent);
+
+ expect(hookSpy).toHaveBeenCalled();
+ expect(eventListenerSpy).toHaveBeenCalled();
+ expect(scheduleButton).toBe(button as any);
+ expect(scheduleEventName).toBe('click');
+ expect(scheduleCapture).toBe(false);
+ expect(scheduleTask && (scheduleTask as any).data.taskData).toBe(null as any);
+ });
+
+ it('should support addEventListener on window', ifEnvSupports(windowPrototype, function() {
+ const hookSpy = jasmine.createSpy('hook');
+ const eventListenerSpy = jasmine.createSpy('eventListener');
+ const zone = rootZone.fork({
+ name: 'spy',
+ onScheduleTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
+ task: Task): any => {
+ hookSpy();
+ return parentZoneDelegate.scheduleTask(targetZone, task);
+ }
+ });
+
+ zone.run(function() { window.addEventListener('click', eventListenerSpy); });
+
+ window.dispatchEvent(clickEvent);
+
+ expect(hookSpy).toHaveBeenCalled();
+ expect(eventListenerSpy).toHaveBeenCalled();
+ }));
+
+ it('should support removeEventListener', function() {
+ const hookSpy = jasmine.createSpy('hook');
+ const eventListenerSpy = jasmine.createSpy('eventListener');
+ const zone = rootZone.fork({
+ name: 'spy',
+ onCancelTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
+ task: Task): any => {
+ hookSpy();
+ return parentZoneDelegate.cancelTask(targetZone, task);
+ }
+ });
+
+ zone.run(function() {
+ button.addEventListener('click', eventListenerSpy);
+ button.removeEventListener('click', eventListenerSpy);
+ });
+
+ button.dispatchEvent(clickEvent);
+
+ expect(hookSpy).toHaveBeenCalled();
+ expect(eventListenerSpy).not.toHaveBeenCalled();
+ });
+
+ describe(
+ 'should support addEventListener/removeEventListener with AddEventListenerOptions with capture setting',
+ ifEnvSupports(supportEventListenerOptions, function() {
+ let hookSpy: Spy;
+ let cancelSpy: Spy;
+ let logs: string[];
+ const zone = rootZone.fork({
+ name: 'spy',
+ onScheduleTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone,
+ targetZone: Zone, task: Task): any => {
+ hookSpy();
+ return parentZoneDelegate.scheduleTask(targetZone, task);
+ },
+ onCancelTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
+ task: Task): any => {
+ cancelSpy();
+ return parentZoneDelegate.cancelTask(targetZone, task);
+ }
+ });
+
+ const docListener = () => { logs.push('document'); };
+ const btnListener = () => { logs.push('button'); };
+
+ beforeEach(() => {
+ logs = [];
+ hookSpy = jasmine.createSpy('hook');
+ cancelSpy = jasmine.createSpy('cancel');
+ });
+
+ it('should handle child event when addEventListener with capture true', () => {
+ // test capture true
+ zone.run(function() {
+ (document as any).addEventListener('click', docListener, {capture: true});
+ button.addEventListener('click', btnListener);
+ });
+
+ button.dispatchEvent(clickEvent);
+ expect(hookSpy).toHaveBeenCalled();
+
+ expect(logs).toEqual(['document', 'button']);
+ logs = [];
+
+ (document as any).removeEventListener('click', docListener, {capture: true});
+ button.removeEventListener('click', btnListener);
+ expect(cancelSpy).toHaveBeenCalled();
+
+ button.dispatchEvent(clickEvent);
+ expect(logs).toEqual([]);
+ });
+
+ it('should handle child event when addEventListener with capture true', () => {
+ // test capture false
+ zone.run(function() {
+ (document as any).addEventListener('click', docListener, {capture: false});
+ button.addEventListener('click', btnListener);
+ });
+
+ button.dispatchEvent(clickEvent);
+ expect(hookSpy).toHaveBeenCalled();
+ expect(logs).toEqual(['button', 'document']);
+ logs = [];
+
+ (document as any).removeEventListener('click', docListener, {capture: false});
+ button.removeEventListener('click', btnListener);
+ expect(cancelSpy).toHaveBeenCalled();
+
+ button.dispatchEvent(clickEvent);
+ expect(logs).toEqual([]);
+ });
+ }));
+
+ describe(
+ 'should ignore duplicate event handler',
+ ifEnvSupports(supportEventListenerOptions, function() {
+ let hookSpy: Spy;
+ let cancelSpy: Spy;
+ let logs: string[];
+ const zone = rootZone.fork({
+ name: 'spy',
+ onScheduleTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone,
+ targetZone: Zone, task: Task): any => {
+ hookSpy();
+ return parentZoneDelegate.scheduleTask(targetZone, task);
+ },
+ onCancelTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
+ task: Task): any => {
+ cancelSpy();
+ return parentZoneDelegate.cancelTask(targetZone, task);
+ }
+ });
+
+ const docListener = () => { logs.push('document options'); };
+
+ beforeEach(() => {
+ logs = [];
+ hookSpy = jasmine.createSpy('hook');
+ cancelSpy = jasmine.createSpy('cancel');
+ });
+
+ const testDuplicate = function(args1?: any, args2?: any) {
+ zone.run(function() {
+ if (args1) {
+ (document as any).addEventListener('click', docListener, args1);
+ } else {
+ (document as any).addEventListener('click', docListener);
+ }
+ if (args2) {
+ (document as any).addEventListener('click', docListener, args2);
+ } else {
+ (document as any).addEventListener('click', docListener);
+ }
+ });
+
+ button.dispatchEvent(clickEvent);
+ expect(hookSpy).toHaveBeenCalled();
+ expect(logs).toEqual(['document options']);
+ logs = [];
+
+ (document as any).removeEventListener('click', docListener, args1);
+ expect(cancelSpy).toHaveBeenCalled();
+ button.dispatchEvent(clickEvent);
+ expect(logs).toEqual([]);
+ };
+
+ it('should ignore duplicate handler', () => {
+ let captureFalse = [
+ undefined, false, {capture: false}, {capture: false, passive: false},
+ {passive: false}, {}
+ ];
+ let captureTrue = [true, {capture: true}, {capture: true, passive: false}];
+ for (let i = 0; i < captureFalse.length; i++) {
+ for (let j = 0; j < captureFalse.length; j++) {
+ testDuplicate(captureFalse[i], captureFalse[j]);
+ }
+ }
+ for (let i = 0; i < captureTrue.length; i++) {
+ for (let j = 0; j < captureTrue.length; j++) {
+ testDuplicate(captureTrue[i], captureTrue[j]);
+ }
+ }
+ });
+ }));
+
+ describe(
+ 'should support mix useCapture with AddEventListenerOptions capture',
+ ifEnvSupports(supportEventListenerOptions, function() {
+ let hookSpy: Spy;
+ let cancelSpy: Spy;
+ let logs: string[];
+ const zone = rootZone.fork({
+ name: 'spy',
+ onScheduleTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone,
+ targetZone: Zone, task: Task): any => {
+ hookSpy();
+ return parentZoneDelegate.scheduleTask(targetZone, task);
+ },
+ onCancelTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
+ task: Task): any => {
+ cancelSpy();
+ return parentZoneDelegate.cancelTask(targetZone, task);
+ }
+ });
+
+ const docListener = () => { logs.push('document options'); };
+ const docListener1 = () => { logs.push('document useCapture'); };
+ const btnListener = () => { logs.push('button'); };
+
+ beforeEach(() => {
+ logs = [];
+ hookSpy = jasmine.createSpy('hook');
+ cancelSpy = jasmine.createSpy('cancel');
+ });
+
+ const testAddRemove = function(args1?: any, args2?: any) {
+ zone.run(function() {
+ if (args1) {
+ (document as any).addEventListener('click', docListener, args1);
+ } else {
+ (document as any).addEventListener('click', docListener);
+ }
+ if (args2) {
+ (document as any).removeEventListener('click', docListener, args2);
+ } else {
+ (document as any).removeEventListener('click', docListener);
+ }
+ });
+
+ button.dispatchEvent(clickEvent);
+ expect(cancelSpy).toHaveBeenCalled();
+ expect(logs).toEqual([]);
+ };
+
+ it('should be able to add/remove same handler with mix options and capture',
+ function() {
+ let captureFalse = [
+ undefined, false, {capture: false}, {capture: false, passive: false},
+ {passive: false}, {}
+ ];
+ let captureTrue = [true, {capture: true}, {capture: true, passive: false}];
+ for (let i = 0; i < captureFalse.length; i++) {
+ for (let j = 0; j < captureFalse.length; j++) {
+ testAddRemove(captureFalse[i], captureFalse[j]);
+ }
+ }
+ for (let i = 0; i < captureTrue.length; i++) {
+ for (let j = 0; j < captureTrue.length; j++) {
+ testAddRemove(captureTrue[i], captureTrue[j]);
+ }
+ }
+ });
+
+ const testDifferent = function(args1?: any, args2?: any) {
+ zone.run(function() {
+ if (args1) {
+ (document as any).addEventListener('click', docListener, args1);
+ } else {
+ (document as any).addEventListener('click', docListener);
+ }
+ if (args2) {
+ (document as any).addEventListener('click', docListener1, args2);
+ } else {
+ (document as any).addEventListener('click', docListener1);
+ }
+ });
+
+ button.dispatchEvent(clickEvent);
+ expect(hookSpy).toHaveBeenCalled();
+ expect(logs.sort()).toEqual(['document options', 'document useCapture']);
+ logs = [];
+
+ if (args1) {
+ (document as any).removeEventListener('click', docListener, args1);
+ } else {
+ (document as any).removeEventListener('click', docListener);
+ }
+
+ button.dispatchEvent(clickEvent);
+ expect(logs).toEqual(['document useCapture']);
+ logs = [];
+
+ if (args2) {
+ (document as any).removeEventListener('click', docListener1, args2);
+ } else {
+ (document as any).removeEventListener('click', docListener1);
+ }
+
+ button.dispatchEvent(clickEvent);
+ expect(logs).toEqual([]);
+ };
+
+ it('should be able to add different handlers for same event', function() {
+ let captureFalse = [
+ undefined, false, {capture: false}, {capture: false, passive: false},
+ {passive: false}, {}
+ ];
+ let captureTrue = [true, {capture: true}, {capture: true, passive: false}];
+ for (let i = 0; i < captureFalse.length; i++) {
+ for (let j = 0; j < captureTrue.length; j++) {
+ testDifferent(captureFalse[i], captureTrue[j]);
+ }
+ }
+ for (let i = 0; i < captureTrue.length; i++) {
+ for (let j = 0; j < captureFalse.length; j++) {
+ testDifferent(captureTrue[i], captureFalse[j]);
+ }
+ }
+ });
+
+ it('should handle options.capture true with capture true correctly', function() {
+ zone.run(function() {
+ (document as any).addEventListener('click', docListener, {capture: true});
+ document.addEventListener('click', docListener1, true);
+ button.addEventListener('click', btnListener);
+ });
+
+ button.dispatchEvent(clickEvent);
+ expect(hookSpy).toHaveBeenCalled();
+ expect(logs).toEqual(['document options', 'document useCapture', 'button']);
+ logs = [];
+
+ (document as any).removeEventListener('click', docListener, {capture: true});
+ button.dispatchEvent(clickEvent);
+ expect(logs).toEqual(['document useCapture', 'button']);
+ logs = [];
+
+ document.removeEventListener('click', docListener1, true);
+ button.dispatchEvent(clickEvent);
+ expect(logs).toEqual(['button']);
+ logs = [];
+
+ button.removeEventListener('click', btnListener);
+ expect(cancelSpy).toHaveBeenCalled();
+
+ button.dispatchEvent(clickEvent);
+ expect(logs).toEqual([]);
+ });
+ }));
+
+ it('should support addEventListener with AddEventListenerOptions once setting',
+ ifEnvSupports(supportEventListenerOptions, function() {
+ let hookSpy = jasmine.createSpy('hook');
+ let logs: string[] = [];
+ const zone = rootZone.fork({
+ name: 'spy',
+ onScheduleTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
+ task: Task): any => {
+ hookSpy();
+ return parentZoneDelegate.scheduleTask(targetZone, task);
+ }
+ });
+
+ zone.run(function() {
+ (button as any).addEventListener('click', function() {
+ logs.push('click');
+ }, {once: true});
+ });
+
+ button.dispatchEvent(clickEvent);
+
+ expect(hookSpy).toHaveBeenCalled();
+ expect(logs.length).toBe(1);
+ expect(logs).toEqual(['click']);
+ logs = [];
+
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(0);
+ }));
+
+ it('should support addEventListener with AddEventListenerOptions once setting and capture',
+ ifEnvSupports(supportEventListenerOptions, function() {
+ let hookSpy = jasmine.createSpy('hook');
+ let logs: string[] = [];
+ const zone = rootZone.fork({
+ name: 'spy',
+ onScheduleTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
+ task: Task): any => {
+ hookSpy();
+ return parentZoneDelegate.scheduleTask(targetZone, task);
+ }
+ });
+
+ zone.run(function() {
+ (button as any).addEventListener('click', function() {
+ logs.push('click');
+ }, {once: true, capture: true});
+ });
+
+ button.dispatchEvent(clickEvent);
+
+ expect(hookSpy).toHaveBeenCalled();
+ expect(logs.length).toBe(1);
+ expect(logs).toEqual(['click']);
+ logs = [];
+
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(0);
+ }));
+
+
+ it('should support add multipe listeners with AddEventListenerOptions once setting and same capture after normal listener',
+ ifEnvSupports(supportEventListenerOptions, function() {
+ let logs: string[] = [];
+
+ button.addEventListener('click', function() { logs.push('click'); }, true);
+ (button as any).addEventListener('click', function() {
+ logs.push('once click');
+ }, {once: true, capture: true});
+
+ button.dispatchEvent(clickEvent);
+
+ expect(logs.length).toBe(2);
+ expect(logs).toEqual(['click', 'once click']);
+ logs = [];
+
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(1);
+ expect(logs).toEqual(['click']);
+ }));
+
+ it('should support add multipe listeners with AddEventListenerOptions once setting and mixed capture after normal listener',
+ ifEnvSupports(supportEventListenerOptions, function() {
+ let logs: string[] = [];
+
+ button.addEventListener('click', function() { logs.push('click'); });
+ (button as any).addEventListener('click', function() {
+ logs.push('once click');
+ }, {once: true, capture: true});
+
+ button.dispatchEvent(clickEvent);
+
+ expect(logs.length).toBe(2);
+ expect(logs).toEqual(['click', 'once click']);
+ logs = [];
+
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(1);
+ expect(logs).toEqual(['click']);
+ }));
+
+ it('should support add multipe listeners with AddEventListenerOptions once setting before normal listener',
+ ifEnvSupports(supportEventListenerOptions, function() {
+ let logs: string[] = [];
+
+ (button as any).addEventListener('click', function() {
+ logs.push('once click');
+ }, {once: true});
+
+ button.addEventListener('click', function() { logs.push('click'); });
+
+ button.dispatchEvent(clickEvent);
+
+ expect(logs.length).toBe(2);
+ expect(logs).toEqual(['once click', 'click']);
+ logs = [];
+
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(1);
+ expect(logs).toEqual(['click']);
+ }));
+
+ it('should support add multipe listeners with AddEventListenerOptions once setting with same capture before normal listener',
+ ifEnvSupports(supportEventListenerOptions, function() {
+ let logs: string[] = [];
+
+ (button as any).addEventListener('click', function() {
+ logs.push('once click');
+ }, {once: true, capture: true});
+
+ button.addEventListener('click', function() { logs.push('click'); }, true);
+
+ button.dispatchEvent(clickEvent);
+
+ expect(logs.length).toBe(2);
+ expect(logs).toEqual(['once click', 'click']);
+ logs = [];
+
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(1);
+ expect(logs).toEqual(['click']);
+ }));
+
+ it('should support add multipe listeners with AddEventListenerOptions once setting with mixed capture before normal listener',
+ ifEnvSupports(supportEventListenerOptions, function() {
+ let logs: string[] = [];
+
+ (button as any).addEventListener('click', function() {
+ logs.push('once click');
+ }, {once: true, capture: true});
+
+ button.addEventListener('click', function() { logs.push('click'); });
+
+ button.dispatchEvent(clickEvent);
+
+ expect(logs.length).toBe(2);
+ expect(logs).toEqual(['once click', 'click']);
+ logs = [];
+
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(1);
+ expect(logs).toEqual(['click']);
+ }));
+
+ it('should change options to boolean if not support passive', () => {
+ patchEventTarget(window, [TestEventListener.prototype]);
+ const testEventListener = new TestEventListener();
+
+ const listener = function() {};
+ testEventListener.addEventListener('test', listener, {passive: true});
+ testEventListener.addEventListener('test1', listener, {once: true});
+ testEventListener.addEventListener('test2', listener, {capture: true});
+ testEventListener.addEventListener('test3', listener, {passive: false});
+ testEventListener.addEventListener('test4', listener, {once: false});
+ testEventListener.addEventListener('test5', listener, {capture: false});
+ if (!supportsPassive) {
+ expect(testEventListener.logs).toEqual([false, false, true, false, false, false]);
+ } else {
+ expect(testEventListener.logs).toEqual([
+ {passive: true}, {once: true}, {capture: true}, {passive: false}, {once: false},
+ {capture: false}
+ ]);
+ }
+ });
+
+ it('should change options to boolean if not support passive on HTMLElement', () => {
+ const logs: string[] = [];
+ const listener = (e: Event) => { logs.push('clicked'); };
+
+ (button as any).addEventListener('click', listener, {once: true});
+ button.dispatchEvent(clickEvent);
+ expect(logs).toEqual(['clicked']);
+ button.dispatchEvent(clickEvent);
+ if (supportsPassive) {
+ expect(logs).toEqual(['clicked']);
+ } else {
+ expect(logs).toEqual(['clicked', 'clicked']);
+ }
+
+ button.removeEventListener('click', listener);
+ });
+
+ it('should support addEventListener with AddEventListenerOptions passive setting',
+ ifEnvSupports(supportEventListenerOptions, function() {
+ const hookSpy = jasmine.createSpy('hook');
+ const logs: string[] = [];
+ const zone = rootZone.fork({
+ name: 'spy',
+ onScheduleTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
+ task: Task): any => {
+ hookSpy();
+ return parentZoneDelegate.scheduleTask(targetZone, task);
+ }
+ });
+
+ const listener = (e: Event) => {
+ logs.push(e.defaultPrevented.toString());
+ e.preventDefault();
+ logs.push(e.defaultPrevented.toString());
+ };
+
+ zone.run(function() {
+ (button as any).addEventListener('click', listener, {passive: true});
+ });
+
+ button.dispatchEvent(clickEvent);
+
+ expect(hookSpy).toHaveBeenCalled();
+ expect(logs).toEqual(['false', 'false']);
+
+ button.removeEventListener('click', listener);
+ }));
+
+ it('should support Event.stopImmediatePropagation',
+ ifEnvSupports(supportEventListenerOptions, function() {
+ const hookSpy = jasmine.createSpy('hook');
+ const logs: any[] = [];
+ const zone = rootZone.fork({
+ name: 'spy',
+ onScheduleTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
+ task: Task): any => {
+ hookSpy();
+ return parentZoneDelegate.scheduleTask(targetZone, task);
+ }
+ });
+
+ const listener1 = (e: Event) => {
+ logs.push('listener1');
+ e.stopImmediatePropagation();
+ };
+
+ const listener2 = (e: Event) => { logs.push('listener2'); };
+
+ zone.run(function() {
+ (button as any).addEventListener('click', listener1);
+ (button as any).addEventListener('click', listener2);
+ });
+
+ button.dispatchEvent(clickEvent);
+
+ expect(hookSpy).toHaveBeenCalled();
+ expect(logs).toEqual(['listener1']);
+
+ button.removeEventListener('click', listener1);
+ button.removeEventListener('click', listener2);
+ }));
+
+ it('should support remove event listener by call zone.cancelTask directly', function() {
+ let logs: string[] = [];
+ let eventTask: Task;
+ const zone = rootZone.fork({
+ name: 'spy',
+ onScheduleTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
+ task: Task): any => {
+ eventTask = task;
+ return parentZoneDelegate.scheduleTask(targetZone, task);
+ }
+ });
+
+ zone.run(() => { button.addEventListener('click', function() { logs.push('click'); }); });
+ let listeners = (button as any).eventListeners('click');
+ expect(listeners.length).toBe(1);
+ eventTask !.zone.cancelTask(eventTask !);
+
+ listeners = (button as any).eventListeners('click');
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(0);
+ expect(listeners.length).toBe(0);
+ });
+
+ it('should support remove event listener by call zone.cancelTask directly with capture=true',
+ function() {
+ let logs: string[] = [];
+ let eventTask: Task;
+ const zone = rootZone.fork({
+ name: 'spy',
+ onScheduleTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
+ task: Task): any => {
+ eventTask = task;
+ return parentZoneDelegate.scheduleTask(targetZone, task);
+ }
+ });
+
+ zone.run(() => {
+ button.addEventListener('click', function() { logs.push('click'); }, true);
+ });
+ let listeners = (button as any).eventListeners('click');
+ expect(listeners.length).toBe(1);
+ eventTask !.zone.cancelTask(eventTask !);
+
+ listeners = (button as any).eventListeners('click');
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(0);
+ expect(listeners.length).toBe(0);
+ });
+
+ it('should support remove event listeners by call zone.cancelTask directly with multiple listeners',
+ function() {
+ let logs: string[] = [];
+ let eventTask: Task;
+ const zone = rootZone.fork({
+ name: 'spy',
+ onScheduleTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
+ task: Task): any => {
+ eventTask = task;
+ return parentZoneDelegate.scheduleTask(targetZone, task);
+ }
+ });
+
+ zone.run(
+ () => { button.addEventListener('click', function() { logs.push('click1'); }); });
+ button.addEventListener('click', function() { logs.push('click2'); });
+ let listeners = (button as any).eventListeners('click');
+ expect(listeners.length).toBe(2);
+
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(2);
+ expect(logs).toEqual(['click1', 'click2']);
+ eventTask !.zone.cancelTask(eventTask !);
+ logs = [];
+
+ listeners = (button as any).eventListeners('click');
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(1);
+ expect(listeners.length).toBe(1);
+ expect(logs).toEqual(['click2']);
+ });
+
+ it('should support remove event listeners by call zone.cancelTask directly with multiple listeners with same capture=true',
+ function() {
+ let logs: string[] = [];
+ let eventTask: Task;
+ const zone = rootZone.fork({
+ name: 'spy',
+ onScheduleTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
+ task: Task): any => {
+ eventTask = task;
+ return parentZoneDelegate.scheduleTask(targetZone, task);
+ }
+ });
+
+ zone.run(() => {
+ button.addEventListener('click', function() { logs.push('click1'); }, true);
+ });
+ button.addEventListener('click', function() { logs.push('click2'); }, true);
+ let listeners = (button as any).eventListeners('click');
+ expect(listeners.length).toBe(2);
+
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(2);
+ expect(logs).toEqual(['click1', 'click2']);
+ eventTask !.zone.cancelTask(eventTask !);
+ logs = [];
+
+ listeners = (button as any).eventListeners('click');
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(1);
+ expect(listeners.length).toBe(1);
+ expect(logs).toEqual(['click2']);
+ });
+
+ it('should support remove event listeners by call zone.cancelTask directly with multiple listeners with mixed capture',
+ function() {
+ let logs: string[] = [];
+ let eventTask: Task;
+ const zone = rootZone.fork({
+ name: 'spy',
+ onScheduleTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
+ task: Task): any => {
+ eventTask = task;
+ return parentZoneDelegate.scheduleTask(targetZone, task);
+ }
+ });
+
+ zone.run(() => {
+ button.addEventListener('click', function() { logs.push('click1'); }, true);
+ });
+ button.addEventListener('click', function() { logs.push('click2'); });
+ let listeners = (button as any).eventListeners('click');
+ expect(listeners.length).toBe(2);
+
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(2);
+ expect(logs).toEqual(['click1', 'click2']);
+ eventTask !.zone.cancelTask(eventTask !);
+ logs = [];
+
+ listeners = (button as any).eventListeners('click');
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(1);
+ expect(listeners.length).toBe(1);
+ expect(logs).toEqual(['click2']);
+ });
+
+ it('should support reschedule eventTask',
+ ifEnvSupports(supportEventListenerOptions, function() {
+ let hookSpy1 = jasmine.createSpy('spy1');
+ let hookSpy2 = jasmine.createSpy('spy2');
+ let hookSpy3 = jasmine.createSpy('spy3');
+ let logs: string[] = [];
+ const isBlacklistedEvent = function(source: string) {
+ return source.lastIndexOf('click') !== -1;
+ };
+ const zone1 = Zone.current.fork({
+ name: 'zone1',
+ onScheduleTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
+ task: Task): any => {
+ if ((task.type === 'eventTask' || task.type === 'macroTask') &&
+ isBlacklistedEvent(task.source)) {
+ task.cancelScheduleRequest();
+
+ return zone2.scheduleTask(task);
+ } else {
+ return parentZoneDelegate.scheduleTask(targetZone, task);
+ }
+ },
+ onInvokeTask(
+ parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task,
+ applyThis: any, applyArgs: any) {
+ hookSpy1();
+ return parentZoneDelegate.invokeTask(targetZone, task, applyThis, applyArgs);
+ }
+ });
+ const zone2 = Zone.current.fork({
+ name: 'zone2',
+ onScheduleTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
+ task: Task): any => {
+ hookSpy2();
+ return parentZoneDelegate.scheduleTask(targetZone, task);
+ },
+ onInvokeTask(
+ parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task,
+ applyThis: any, applyArgs: any) {
+ hookSpy3();
+ return parentZoneDelegate.invokeTask(targetZone, task, applyThis, applyArgs);
+ }
+ });
+
+ const listener = function() { logs.push(Zone.current.name); };
+ zone1.run(() => {
+ button.addEventListener('click', listener);
+ button.addEventListener('mouseover', listener);
+ });
+
+ const clickEvent = document.createEvent('Event');
+ clickEvent.initEvent('click', true, true);
+ const mouseEvent = document.createEvent('Event');
+ mouseEvent.initEvent('mouseover', true, true);
+
+ button.dispatchEvent(clickEvent);
+ button.removeEventListener('click', listener);
+
+ expect(logs).toEqual(['zone2']);
+ expect(hookSpy1).not.toHaveBeenCalled();
+ expect(hookSpy2).toHaveBeenCalled();
+ expect(hookSpy3).toHaveBeenCalled();
+ logs = [];
+ hookSpy2 = jasmine.createSpy('hookSpy2');
+ hookSpy3 = jasmine.createSpy('hookSpy3');
+
+ button.dispatchEvent(mouseEvent);
+ button.removeEventListener('mouseover', listener);
+ expect(logs).toEqual(['zone1']);
+ expect(hookSpy1).toHaveBeenCalled();
+ expect(hookSpy2).not.toHaveBeenCalled();
+ expect(hookSpy3).not.toHaveBeenCalled();
+ }));
+
+ it('should support inline event handler attributes', function() {
+ const hookSpy = jasmine.createSpy('hook');
+ const zone = rootZone.fork({
+ name: 'spy',
+ onScheduleTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
+ task: Task): any => {
+ hookSpy();
+ return parentZoneDelegate.scheduleTask(targetZone, task);
+ }
+ });
+
+ zone.run(function() {
+ button.setAttribute('onclick', 'return');
+ expect(button.onclick).not.toBe(null);
+ });
+ });
+
+ describe('should be able to remove eventListener during eventListener callback', function() {
+ it('should be able to remove eventListener during eventListener callback', function() {
+ let logs: string[] = [];
+ const listener1 = function() {
+ button.removeEventListener('click', listener1);
+ logs.push('listener1');
+ };
+ const listener2 = function() { logs.push('listener2'); };
+ const listener3 = {handleEvent: function(event: Event) { logs.push('listener3'); }};
+
+ button.addEventListener('click', listener1);
+ button.addEventListener('click', listener2);
+ button.addEventListener('click', listener3);
+
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(3);
+ expect(logs).toEqual(['listener1', 'listener2', 'listener3']);
+
+ logs = [];
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(2);
+ expect(logs).toEqual(['listener2', 'listener3']);
+
+ button.removeEventListener('click', listener2);
+ button.removeEventListener('click', listener3);
+ });
+
+ it('should be able to remove eventListener during eventListener callback with capture=true',
+ function() {
+ let logs: string[] = [];
+ const listener1 = function() {
+ button.removeEventListener('click', listener1, true);
+ logs.push('listener1');
+ };
+ const listener2 = function() { logs.push('listener2'); };
+ const listener3 = {handleEvent: function(event: Event) { logs.push('listener3'); }};
+
+ button.addEventListener('click', listener1, true);
+ button.addEventListener('click', listener2, true);
+ button.addEventListener('click', listener3, true);
+
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(3);
+ expect(logs).toEqual(['listener1', 'listener2', 'listener3']);
+
+ logs = [];
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(2);
+ expect(logs).toEqual(['listener2', 'listener3']);
+
+ button.removeEventListener('click', listener2, true);
+ button.removeEventListener('click', listener3, true);
+ });
+
+ it('should be able to remove handleEvent eventListener during eventListener callback',
+ function() {
+ let logs: string[] = [];
+ const listener1 = function() { logs.push('listener1'); };
+ const listener2 = function() { logs.push('listener2'); };
+ const listener3 = {
+ handleEvent: function(event: Event) {
+ logs.push('listener3');
+ button.removeEventListener('click', listener3);
+ }
+ };
+
+ button.addEventListener('click', listener1);
+ button.addEventListener('click', listener2);
+ button.addEventListener('click', listener3);
+
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(3);
+ expect(logs).toEqual(['listener1', 'listener2', 'listener3']);
+
+ logs = [];
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(2);
+ expect(logs).toEqual(['listener1', 'listener2']);
+
+ button.removeEventListener('click', listener1);
+ button.removeEventListener('click', listener2);
+ });
+
+ it('should be able to remove handleEvent eventListener during eventListener callback with capture=true',
+ function() {
+ let logs: string[] = [];
+ const listener1 = function() { logs.push('listener1'); };
+ const listener2 = function() { logs.push('listener2'); };
+ const listener3 = {
+ handleEvent: function(event: Event) {
+ logs.push('listener3');
+ button.removeEventListener('click', listener3, true);
+ }
+ };
+
+ button.addEventListener('click', listener1, true);
+ button.addEventListener('click', listener2, true);
+ button.addEventListener('click', listener3, true);
+
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(3);
+ expect(logs).toEqual(['listener1', 'listener2', 'listener3']);
+
+ logs = [];
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(2);
+ expect(logs).toEqual(['listener1', 'listener2']);
+
+ button.removeEventListener('click', listener1, true);
+ button.removeEventListener('click', listener2, true);
+ });
+
+ it('should be able to remove multiple eventListeners during eventListener callback',
+ function() {
+ let logs: string[] = [];
+ const listener1 = function() {
+ logs.push('listener1');
+ button.removeEventListener('click', listener2);
+ button.removeEventListener('click', listener3);
+ };
+ const listener2 = function() { logs.push('listener2'); };
+ const listener3 = {handleEvent: function(event: Event) { logs.push('listener3'); }};
+
+ button.addEventListener('click', listener1);
+ button.addEventListener('click', listener2);
+ button.addEventListener('click', listener3);
+
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(1);
+ expect(logs).toEqual(['listener1']);
+
+ button.removeEventListener('click', listener1);
+ });
+
+ it('should be able to remove multiple eventListeners during eventListener callback with capture=true',
+ function() {
+ let logs: string[] = [];
+ const listener1 = function() {
+ logs.push('listener1');
+ button.removeEventListener('click', listener2, true);
+ button.removeEventListener('click', listener3, true);
+ };
+ const listener2 = function() { logs.push('listener2'); };
+ const listener3 = {handleEvent: function(event: Event) { logs.push('listener3'); }};
+
+ button.addEventListener('click', listener1, true);
+ button.addEventListener('click', listener2, true);
+ button.addEventListener('click', listener3, true);
+
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(1);
+ expect(logs).toEqual(['listener1']);
+
+ button.removeEventListener('click', listener1, true);
+ });
+
+ it('should be able to remove part of other eventListener during eventListener callback',
+ function() {
+ let logs: string[] = [];
+ const listener1 = function() {
+ logs.push('listener1');
+ button.removeEventListener('click', listener2);
+ };
+ const listener2 = function() { logs.push('listener2'); };
+ const listener3 = {handleEvent: function(event: Event) { logs.push('listener3'); }};
+
+ button.addEventListener('click', listener1);
+ button.addEventListener('click', listener2);
+ button.addEventListener('click', listener3);
+
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(2);
+ expect(logs).toEqual(['listener1', 'listener3']);
+
+ button.removeEventListener('click', listener1);
+ button.removeEventListener('click', listener3);
+ });
+
+ it('should be able to remove part of other eventListener during eventListener callback with capture=true',
+ function() {
+ let logs: string[] = [];
+ const listener1 = function() {
+ logs.push('listener1');
+ button.removeEventListener('click', listener2, true);
+ };
+ const listener2 = function() { logs.push('listener2'); };
+ const listener3 = {handleEvent: function(event: Event) { logs.push('listener3'); }};
+
+ button.addEventListener('click', listener1, true);
+ button.addEventListener('click', listener2, true);
+ button.addEventListener('click', listener3, true);
+
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(2);
+ expect(logs).toEqual(['listener1', 'listener3']);
+
+ button.removeEventListener('click', listener1, true);
+ button.removeEventListener('click', listener3, true);
+ });
+
+ it('should be able to remove all beforeward and afterward eventListener during eventListener callback',
+ function() {
+ let logs: string[] = [];
+ const listener1 = function() { logs.push('listener1'); };
+ const listener2 = function() {
+ logs.push('listener2');
+ button.removeEventListener('click', listener1);
+ button.removeEventListener('click', listener3);
+ };
+ const listener3 = {handleEvent: function(event: Event) { logs.push('listener3'); }};
+
+ button.addEventListener('click', listener1);
+ button.addEventListener('click', listener2);
+ button.addEventListener('click', listener3);
+
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(2);
+ expect(logs).toEqual(['listener1', 'listener2']);
+
+ logs = [];
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(1);
+ expect(logs).toEqual(['listener2']);
+
+ button.removeEventListener('click', listener2);
+ });
+
+ it('should be able to remove all beforeward and afterward eventListener during eventListener callback with capture=true',
+ function() {
+ let logs: string[] = [];
+ const listener1 = function() { logs.push('listener1'); };
+ const listener2 = function() {
+ logs.push('listener2');
+ button.removeEventListener('click', listener1, true);
+ button.removeEventListener('click', listener3, true);
+ };
+ const listener3 = {handleEvent: function(event: Event) { logs.push('listener3'); }};
+
+ button.addEventListener('click', listener1, true);
+ button.addEventListener('click', listener2, true);
+ button.addEventListener('click', listener3, true);
+
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(2);
+ expect(logs).toEqual(['listener1', 'listener2']);
+
+ logs = [];
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(1);
+ expect(logs).toEqual(['listener2']);
+
+ button.removeEventListener('click', listener2, true);
+ });
+
+ it('should be able to remove part of beforeward and afterward eventListener during eventListener callback',
+ function() {
+ let logs: string[] = [];
+ const listener1 = function() { logs.push('listener1'); };
+ const listener2 = function() { logs.push('listener2'); };
+ const listener3 = {
+ handleEvent: function(event: Event) {
+ logs.push('listener3');
+ button.removeEventListener('click', listener2);
+ button.removeEventListener('click', listener4);
+ }
+ };
+ const listener4 = function() { logs.push('listener4'); };
+ const listener5 = function() { logs.push('listener5'); };
+
+ button.addEventListener('click', listener1);
+ button.addEventListener('click', listener2);
+ button.addEventListener('click', listener3);
+ button.addEventListener('click', listener4);
+ button.addEventListener('click', listener5);
+
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(4);
+ expect(logs).toEqual(['listener1', 'listener2', 'listener3', 'listener5']);
+
+ logs = [];
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(3);
+ expect(logs).toEqual(['listener1', 'listener3', 'listener5']);
+
+ button.removeEventListener('click', listener1);
+ button.removeEventListener('click', listener3);
+ button.removeEventListener('click', listener5);
+ });
+
+ it('should be able to remove part of beforeward and afterward eventListener during eventListener callback with capture=true',
+ function() {
+ let logs: string[] = [];
+ const listener1 = function() { logs.push('listener1'); };
+ const listener2 = function() { logs.push('listener2'); };
+ const listener3 = {
+ handleEvent: function(event: Event) {
+ logs.push('listener3');
+ button.removeEventListener('click', listener2, true);
+ button.removeEventListener('click', listener4, true);
+ }
+ };
+ const listener4 = function() { logs.push('listener4'); };
+ const listener5 = function() { logs.push('listener5'); };
+
+ button.addEventListener('click', listener1, true);
+ button.addEventListener('click', listener2, true);
+ button.addEventListener('click', listener3, true);
+ button.addEventListener('click', listener4, true);
+ button.addEventListener('click', listener5, true);
+
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(4);
+ expect(logs).toEqual(['listener1', 'listener2', 'listener3', 'listener5']);
+
+ logs = [];
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(3);
+ expect(logs).toEqual(['listener1', 'listener3', 'listener5']);
+
+ button.removeEventListener('click', listener1, true);
+ button.removeEventListener('click', listener3, true);
+ button.removeEventListener('click', listener5, true);
+ });
+
+ it('should be able to remove all beforeward eventListener during eventListener callback',
+ function() {
+ let logs: string[] = [];
+ const listener1 = function() { logs.push('listener1'); };
+ const listener2 = function() { logs.push('listener2'); };
+ const listener3 = {
+ handleEvent: function(event: Event) {
+ logs.push('listener3');
+ button.removeEventListener('click', listener1);
+ button.removeEventListener('click', listener2);
+ }
+ };
+
+ button.addEventListener('click', listener1);
+ button.addEventListener('click', listener2);
+ button.addEventListener('click', listener3);
+
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(3);
+ expect(logs).toEqual(['listener1', 'listener2', 'listener3']);
+
+ logs = [];
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(1);
+ expect(logs).toEqual(['listener3']);
+
+ button.removeEventListener('click', listener3);
+ });
+
+ it('should be able to remove all beforeward eventListener during eventListener callback with capture=true',
+ function() {
+ let logs: string[] = [];
+ const listener1 = function() { logs.push('listener1'); };
+ const listener2 = function() { logs.push('listener2'); };
+ const listener3 = {
+ handleEvent: function(event: Event) {
+ logs.push('listener3');
+ button.removeEventListener('click', listener1, true);
+ button.removeEventListener('click', listener2, true);
+ }
+ };
+
+ button.addEventListener('click', listener1, true);
+ button.addEventListener('click', listener2, true);
+ button.addEventListener('click', listener3, true);
+
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(3);
+ expect(logs).toEqual(['listener1', 'listener2', 'listener3']);
+
+ logs = [];
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(1);
+ expect(logs).toEqual(['listener3']);
+
+ button.removeEventListener('click', listener3, true);
+ });
+
+ it('should be able to remove part of beforeward eventListener during eventListener callback',
+ function() {
+ let logs: string[] = [];
+ const listener1 = function() { logs.push('listener1'); };
+ const listener2 = function() { logs.push('listener2'); };
+ const listener3 = {
+ handleEvent: function(event: Event) {
+ logs.push('listener3');
+ button.removeEventListener('click', listener1);
+ }
+ };
+
+ button.addEventListener('click', listener1);
+ button.addEventListener('click', listener2);
+ button.addEventListener('click', listener3);
+
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(3);
+ expect(logs).toEqual(['listener1', 'listener2', 'listener3']);
+
+ logs = [];
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(2);
+ expect(logs).toEqual(['listener2', 'listener3']);
+
+ button.removeEventListener('click', listener2);
+ button.removeEventListener('click', listener3);
+ });
+
+ it('should be able to remove part of beforeward eventListener during eventListener callback with capture=true',
+ function() {
+ let logs: string[] = [];
+ const listener1 = function() { logs.push('listener1'); };
+ const listener2 = function() { logs.push('listener2'); };
+ const listener3 = {
+ handleEvent: function(event: Event) {
+ logs.push('listener3');
+ button.removeEventListener('click', listener1, true);
+ }
+ };
+
+ button.addEventListener('click', listener1, true);
+ button.addEventListener('click', listener2, true);
+ button.addEventListener('click', listener3, true);
+
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(3);
+ expect(logs).toEqual(['listener1', 'listener2', 'listener3']);
+
+ logs = [];
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(2);
+ expect(logs).toEqual(['listener2', 'listener3']);
+
+ button.removeEventListener('click', listener2, true);
+ button.removeEventListener('click', listener3, true);
+ });
+
+ it('should be able to remove all eventListeners during first eventListener callback',
+ function() {
+ let logs: string[] = [];
+ const listener1 = function() {
+ (button as any).removeAllListeners('click');
+ logs.push('listener1');
+ };
+ const listener2 = function() { logs.push('listener2'); };
+ const listener3 = {handleEvent: function(event: Event) { logs.push('listener3'); }};
+
+ button.addEventListener('click', listener1);
+ button.addEventListener('click', listener2);
+ button.addEventListener('click', listener3);
+
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(1);
+ expect(logs).toEqual(['listener1']);
+
+ logs = [];
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(0);
+ });
+
+ it('should be able to remove all eventListeners during first eventListener callback with capture=true',
+ function() {
+ let logs: string[] = [];
+ const listener1 = function() {
+ (button as any).removeAllListeners('click');
+ logs.push('listener1');
+ };
+ const listener2 = function() { logs.push('listener2'); };
+ const listener3 = {handleEvent: function(event: Event) { logs.push('listener3'); }};
+
+ button.addEventListener('click', listener1, true);
+ button.addEventListener('click', listener2, true);
+ button.addEventListener('click', listener3, true);
+
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(1);
+ expect(logs).toEqual(['listener1']);
+
+ logs = [];
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(0);
+ });
+
+ it('should be able to remove all eventListeners during middle eventListener callback',
+ function() {
+ let logs: string[] = [];
+ const listener1 = function() { logs.push('listener1'); };
+ const listener2 = function() {
+ (button as any).removeAllListeners('click');
+ logs.push('listener2');
+ };
+ const listener3 = {handleEvent: function(event: Event) { logs.push('listener3'); }};
+
+ button.addEventListener('click', listener1);
+ button.addEventListener('click', listener2);
+ button.addEventListener('click', listener3);
+
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(2);
+ expect(logs).toEqual(['listener1', 'listener2']);
+
+ logs = [];
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(0);
+ });
+
+ it('should be able to remove all eventListeners during middle eventListener callback with capture=true',
+ function() {
+ let logs: string[] = [];
+ const listener1 = function() { logs.push('listener1'); };
+ const listener2 = function() {
+ (button as any).removeAllListeners('click');
+ logs.push('listener2');
+ };
+ const listener3 = {handleEvent: function(event: Event) { logs.push('listener3'); }};
+
+ button.addEventListener('click', listener1, true);
+ button.addEventListener('click', listener2, true);
+ button.addEventListener('click', listener3, true);
+
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(2);
+ expect(logs).toEqual(['listener1', 'listener2']);
+
+ logs = [];
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(0);
+ });
+
+ it('should be able to remove all eventListeners during last eventListener callback',
+ function() {
+ let logs: string[] = [];
+ const listener1 = function() { logs.push('listener1'); };
+ const listener2 = function() { logs.push('listener2'); };
+ const listener3 = {
+ handleEvent: function(event: Event) {
+ logs.push('listener3');
+ (button as any).removeAllListeners('click');
+ }
+ };
+
+ button.addEventListener('click', listener1);
+ button.addEventListener('click', listener2);
+ button.addEventListener('click', listener3);
+
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(3);
+ expect(logs).toEqual(['listener1', 'listener2', 'listener3']);
+
+ logs = [];
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(0);
+ });
+
+ it('should be able to remove all eventListeners during last eventListener callback with capture=true',
+ function() {
+ let logs: string[] = [];
+ const listener1 = function() { logs.push('listener1'); };
+ const listener2 = function() { logs.push('listener2'); };
+ const listener3 = {
+ handleEvent: function(event: Event) {
+ logs.push('listener3');
+ (button as any).removeAllListeners('click');
+ }
+ };
+
+ button.addEventListener('click', listener1, true);
+ button.addEventListener('click', listener2, true);
+ button.addEventListener('click', listener3, true);
+
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(3);
+ expect(logs).toEqual(['listener1', 'listener2', 'listener3']);
+
+ logs = [];
+ button.dispatchEvent(clickEvent);
+ expect(logs.length).toBe(0);
+ });
+ });
+
+ it('should be able to get eventListeners of specified event form EventTarget', function() {
+ const listener1 = function() {};
+ const listener2 = function() {};
+ const listener3 = {handleEvent: function(event: Event) {}};
+ const listener4 = function() {};
+
+ button.addEventListener('click', listener1);
+ button.addEventListener('click', listener2);
+ button.addEventListener('click', listener3);
+ button.addEventListener('mouseover', listener4);
+
+ const listeners = (button as any).eventListeners('click');
+ expect(listeners.length).toBe(3);
+ expect(listeners).toEqual([listener1, listener2, listener3]);
+ button.removeEventListener('click', listener1);
+ button.removeEventListener('click', listener2);
+ button.removeEventListener('click', listener3);
+ });
+
+ it('should be able to get all eventListeners form EventTarget without eventName', function() {
+ const listener1 = function() {};
+ const listener2 = function() {};
+ const listener3 = {handleEvent: function(event: Event) {}};
+
+ button.addEventListener('click', listener1);
+ button.addEventListener('mouseover', listener2);
+ button.addEventListener('mousehover', listener3);
+
+ const listeners = (button as any).eventListeners();
+ expect(listeners.length).toBe(3);
+ expect(listeners).toEqual([listener1, listener2, listener3]);
+ button.removeEventListener('click', listener1);
+ button.removeEventListener('mouseover', listener2);
+ button.removeEventListener('mousehover', listener3);
+ });
+
+ it('should be able to remove all listeners of specified event form EventTarget', function() {
+ let logs: string[] = [];
+ const listener1 = function() { logs.push('listener1'); };
+ const listener2 = function() { logs.push('listener2'); };
+ const listener3 = {handleEvent: function(event: Event) { logs.push('listener3'); }};
+ const listener4 = function() { logs.push('listener4'); };
+
+ button.addEventListener('mouseover', listener1);
+ button.addEventListener('mouseover', listener2);
+ button.addEventListener('mouseover', listener3);
+ button.addEventListener('click', listener4);
+
+ (button as any).removeAllListeners('mouseover');
+ const listeners = (button as any).eventListeners('mouseove');
+ expect(listeners.length).toBe(0);
+
+ const mouseEvent = document.createEvent('Event');
+ mouseEvent.initEvent('mouseover', true, true);
+
+ button.dispatchEvent(mouseEvent);
+ expect(logs).toEqual([]);
+
+ button.dispatchEvent(clickEvent);
+ expect(logs).toEqual(['listener4']);
+
+ button.removeEventListener('click', listener4);
+ });
+
+ it('should be able to remove all listeners of specified event form EventTarget with capture=true',
+ function() {
+ let logs: string[] = [];
+ const listener1 = function() { logs.push('listener1'); };
+ const listener2 = function() { logs.push('listener2'); };
+ const listener3 = {handleEvent: function(event: Event) { logs.push('listener3'); }};
+ const listener4 = function() { logs.push('listener4'); };
+
+ button.addEventListener('mouseover', listener1, true);
+ button.addEventListener('mouseover', listener2, true);
+ button.addEventListener('mouseover', listener3, true);
+ button.addEventListener('click', listener4, true);
+
+ (button as any).removeAllListeners('mouseover');
+ const listeners = (button as any).eventListeners('mouseove');
+ expect(listeners.length).toBe(0);
+
+ const mouseEvent = document.createEvent('Event');
+ mouseEvent.initEvent('mouseover', true, true);
+
+ button.dispatchEvent(mouseEvent);
+ expect(logs).toEqual([]);
+
+ button.dispatchEvent(clickEvent);
+ expect(logs).toEqual(['listener4']);
+
+ button.removeEventListener('click', listener4);
+ });
+
+ it('should be able to remove all listeners of specified event form EventTarget with mixed capture',
+ function() {
+ let logs: string[] = [];
+ const listener1 = function() { logs.push('listener1'); };
+ const listener2 = function() { logs.push('listener2'); };
+ const listener3 = {handleEvent: function(event: Event) { logs.push('listener3'); }};
+ const listener4 = function() { logs.push('listener4'); };
+
+ button.addEventListener('mouseover', listener1, true);
+ button.addEventListener('mouseover', listener2, false);
+ button.addEventListener('mouseover', listener3, true);
+ button.addEventListener('click', listener4, true);
+
+ (button as any).removeAllListeners('mouseover');
+ const listeners = (button as any).eventListeners('mouseove');
+ expect(listeners.length).toBe(0);
+
+ const mouseEvent = document.createEvent('Event');
+ mouseEvent.initEvent('mouseover', true, true);
+
+ button.dispatchEvent(mouseEvent);
+ expect(logs).toEqual([]);
+
+ button.dispatchEvent(clickEvent);
+ expect(logs).toEqual(['listener4']);
+
+ button.removeEventListener('click', listener4);
+ });
+
+ it('should be able to remove all listeners of all events form EventTarget', function() {
+ let logs: string[] = [];
+ const listener1 = function() { logs.push('listener1'); };
+ const listener2 = function() { logs.push('listener2'); };
+ const listener3 = {handleEvent: function(event: Event) { logs.push('listener3'); }};
+ const listener4 = function() { logs.push('listener4'); };
+
+ button.addEventListener('mouseover', listener1);
+ button.addEventListener('mouseover', listener2);
+ button.addEventListener('mouseover', listener3);
+ button.addEventListener('click', listener4);
+
+ (button as any).removeAllListeners();
+ const listeners = (button as any).eventListeners('mouseover');
+ expect(listeners.length).toBe(0);
+
+ const mouseEvent = document.createEvent('Event');
+ mouseEvent.initEvent('mouseover', true, true);
+
+ button.dispatchEvent(mouseEvent);
+ expect(logs).toEqual([]);
+
+ button.dispatchEvent(clickEvent);
+ expect(logs).toEqual([]);
+ });
+
+ it('should be able to remove listener which was added outside of zone ', function() {
+ let logs: string[] = [];
+ const listener1 = function() { logs.push('listener1'); };
+ const listener2 = function() { logs.push('listener2'); };
+ const listener3 = {handleEvent: function(event: Event) { logs.push('listener3'); }};
+ const listener4 = function() { logs.push('listener4'); };
+
+ button.addEventListener('mouseover', listener1);
+ (button as any)[Zone.__symbol__('addEventListener')]('mouseover', listener2);
+ button.addEventListener('click', listener3);
+ (button as any)[Zone.__symbol__('addEventListener')]('click', listener4);
+
+ button.removeEventListener('mouseover', listener1);
+ button.removeEventListener('mouseover', listener2);
+ button.removeEventListener('click', listener3);
+ button.removeEventListener('click', listener4);
+ const listeners = (button as any).eventListeners('mouseover');
+ expect(listeners.length).toBe(0);
+
+ const mouseEvent = document.createEvent('Event');
+ mouseEvent.initEvent('mouseover', true, true);
+
+ button.dispatchEvent(mouseEvent);
+ expect(logs).toEqual([]);
+
+ button.dispatchEvent(clickEvent);
+ expect(logs).toEqual([]);
+ });
+
+ it('should be able to remove all listeners which were added inside of zone ', function() {
+ let logs: string[] = [];
+ const listener1 = function() { logs.push('listener1'); };
+ const listener2 = function() { logs.push('listener2'); };
+ const listener3 = {handleEvent: function(event: Event) { logs.push('listener3'); }};
+ const listener4 = function() { logs.push('listener4'); };
+
+ button.addEventListener('mouseover', listener1);
+ (button as any)[Zone.__symbol__('addEventListener')]('mouseover', listener2);
+ button.addEventListener('click', listener3);
+ (button as any)[Zone.__symbol__('addEventListener')]('click', listener4);
+
+ (button as any).removeAllListeners();
+ const listeners = (button as any).eventListeners('mouseover');
+ expect(listeners.length).toBe(0);
+
+ const mouseEvent = document.createEvent('Event');
+ mouseEvent.initEvent('mouseover', true, true);
+
+ button.dispatchEvent(mouseEvent);
+ expect(logs).toEqual(['listener2']);
+
+ button.dispatchEvent(clickEvent);
+ expect(logs).toEqual(['listener2', 'listener4']);
+ });
+
+ it('should bypass addEventListener of FunctionWrapper and __BROWSERTOOLS_CONSOLE_SAFEFUNC of IE/Edge',
+ ifEnvSupports(ieOrEdge, function() {
+ const hookSpy = jasmine.createSpy('hook');
+ const zone = rootZone.fork({
+ name: 'spy',
+ onScheduleTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
+ task: Task): any => {
+ hookSpy();
+ return parentZoneDelegate.scheduleTask(targetZone, task);
+ }
+ });
+ let logs: string[] = [];
+
+ const listener1 = function() { logs.push(Zone.current.name); };
+
+ (listener1 as any).toString = function() { return '[object FunctionWrapper]'; };
+
+ const listener2 = function() { logs.push(Zone.current.name); };
+
+ (listener2 as any).toString = function() {
+ return 'function __BROWSERTOOLS_CONSOLE_SAFEFUNC() { [native code] }';
+ };
+
+ zone.run(() => {
+ button.addEventListener('click', listener1);
+ button.addEventListener('click', listener2);
+ });
+
+ button.dispatchEvent(clickEvent);
+
+ expect(hookSpy).not.toHaveBeenCalled();
+ expect(logs).toEqual(['ProxyZone', 'ProxyZone']);
+ logs = [];
+
+ button.removeEventListener('click', listener1);
+ button.removeEventListener('click', listener2);
+
+ button.dispatchEvent(clickEvent);
+
+ expect(hookSpy).not.toHaveBeenCalled();
+ expect(logs).toEqual([]);
+ }));
+ });
+
+ describe('unhandle promise rejection', () => {
+ const AsyncTestZoneSpec = (Zone as any)['AsyncTestZoneSpec'];
+ const asyncTest = function(testFn: Function) {
+ return (done: Function) => {
+ let asyncTestZone: Zone = Zone.current.fork(
+ new AsyncTestZoneSpec(done, (error: Error) => { fail(error); }, 'asyncTest'));
+ asyncTestZone.run(testFn);
+ };
+ };
+
+ it('should support window.addEventListener(unhandledrejection)', asyncTest(() => {
+ if (!promiseUnhandleRejectionSupport()) {
+ return;
+ }
+ (Zone as any)[zoneSymbol('ignoreConsoleErrorUncaughtError')] = true;
+ Zone.root.fork({name: 'promise'}).run(function() {
+ const listener = (evt: any) => {
+ window.removeEventListener('unhandledrejection', listener);
+ expect(evt.type).toEqual('unhandledrejection');
+ expect(evt.promise.constructor.name).toEqual('Promise');
+ expect(evt.reason.message).toBe('promise error');
+ };
+ window.addEventListener('unhandledrejection', listener);
+ new Promise((resolve, reject) => { throw new Error('promise error'); });
+ });
+ }));
+
+ it('should support window.addEventListener(rejectionhandled)', asyncTest(() => {
+ if (!promiseUnhandleRejectionSupport()) {
+ return;
+ }
+ (Zone as any)[zoneSymbol('ignoreConsoleErrorUncaughtError')] = true;
+ Zone.root.fork({name: 'promise'}).run(function() {
+ const listener = (evt: any) => {
+ window.removeEventListener('unhandledrejection', listener);
+ p.catch(reason => {});
+ };
+ window.addEventListener('unhandledrejection', listener);
+
+ const handledListener = (evt: any) => {
+ window.removeEventListener('rejectionhandled', handledListener);
+ expect(evt.type).toEqual('rejectionhandled');
+ expect(evt.promise.constructor.name).toEqual('Promise');
+ expect(evt.reason.message).toBe('promise error');
+ };
+
+ window.addEventListener('rejectionhandled', handledListener);
+ const p = new Promise((resolve, reject) => { throw new Error('promise error'); });
+ });
+ }));
+
+ it('should support multiple window.addEventListener(unhandledrejection)', asyncTest(() => {
+ if (!promiseUnhandleRejectionSupport()) {
+ return;
+ }
+ (Zone as any)[zoneSymbol('ignoreConsoleErrorUncaughtError')] = true;
+ Zone.root.fork({name: 'promise'}).run(function() {
+ const listener1 = (evt: any) => {
+ window.removeEventListener('unhandledrejection', listener1);
+ expect(evt.type).toEqual('unhandledrejection');
+ expect(evt.promise.constructor.name).toEqual('Promise');
+ expect(evt.reason.message).toBe('promise error');
+ };
+ const listener2 = (evt: any) => {
+ window.removeEventListener('unhandledrejection', listener2);
+ expect(evt.type).toEqual('unhandledrejection');
+ expect(evt.promise.constructor.name).toEqual('Promise');
+ expect(evt.reason.message).toBe('promise error');
+ };
+ window.addEventListener('unhandledrejection', listener1);
+ window.addEventListener('unhandledrejection', listener2);
+ new Promise((resolve, reject) => { throw new Error('promise error'); });
+ });
+ }));
+ });
+
+ // @JiaLiPassion, Edge 15, the behavior is not the same with Chrome
+ // wait for fix.
+ xit('IntersectionObserver should run callback in zone',
+ ifEnvSupportsWithDone('IntersectionObserver', (done: Function) => {
+ const div = document.createElement('div');
+ document.body.appendChild(div);
+ const options: any = {threshold: 0.5};
+
+ const zone = Zone.current.fork({name: 'intersectionObserverZone'});
+
+ zone.run(() => {
+ const observer = new IntersectionObserver(() => {
+ expect(Zone.current.name).toEqual(zone.name);
+ observer.unobserve(div);
+ done();
+ }, options);
+ observer.observe(div);
+ });
+ div.style.display = 'none';
+ div.style.visibility = 'block';
+ }));
+
+ it('HTMLCanvasElement.toBlob should be a ZoneAware MacroTask',
+ ifEnvSupportsWithDone(supportCanvasTest, (done: Function) => {
+ const canvas = document.createElement('canvas');
+ const d = canvas.width;
+ const ctx = canvas.getContext('2d') !;
+ ctx.beginPath();
+ ctx.moveTo(d / 2, 0);
+ ctx.lineTo(d, d);
+ ctx.lineTo(0, d);
+ ctx.closePath();
+ ctx.fillStyle = 'yellow';
+ ctx.fill();
+
+ const scheduleSpy = jasmine.createSpy('scheduleSpy');
+ const zone: Zone = Zone.current.fork({
+ name: 'canvas',
+ onScheduleTask:
+ (delegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task) => {
+ scheduleSpy();
+ return delegate.scheduleTask(targetZone, task);
+ }
+ });
+
+ zone.run(() => {
+ const canvasData = canvas.toDataURL();
+ canvas.toBlob(function(blob) {
+ expect(Zone.current.name).toEqual('canvas');
+ expect(scheduleSpy).toHaveBeenCalled();
+
+ const reader = new FileReader();
+ reader.readAsDataURL(blob !);
+ reader.onloadend = function() {
+ const base64data = reader.result;
+ expect(base64data).toEqual(canvasData);
+ done();
+ };
+ });
+ });
+ }));
+
+ describe(
+ 'ResizeObserver', ifEnvSupports('ResizeObserver', () => {
+ it('ResizeObserver callback should be in zone', (done) => {
+ const ResizeObserver = (window as any)['ResizeObserver'];
+ const div = document.createElement('div');
+ const zone = Zone.current.fork({name: 'observer'});
+ const observer = new ResizeObserver((entries: any, ob: any) => {
+ expect(Zone.current.name).toEqual(zone.name);
+
+ expect(entries.length).toBe(1);
+ expect(entries[0].target).toBe(div);
+ done();
+ });
+
+ zone.run(() => { observer.observe(div); });
+
+ document.body.appendChild(div);
+ });
+
+ it('ResizeObserver callback should be able to in different zones which when they were observed',
+ (done) => {
+ const ResizeObserver = (window as any)['ResizeObserver'];
+ const div1 = document.createElement('div');
+ const div2 = document.createElement('div');
+ const zone = Zone.current.fork({name: 'observer'});
+ let count = 0;
+ const observer = new ResizeObserver((entries: any, ob: any) => {
+ entries.forEach((entry: any) => {
+ if (entry.target === div1) {
+ expect(Zone.current.name).toEqual(zone.name);
+ } else {
+ expect(Zone.current.name).toEqual('');
+ }
+ });
+ count++;
+ if (count === 2) {
+ done();
+ }
+ });
+
+ zone.run(() => { observer.observe(div1); });
+ Zone.root.run(() => { observer.observe(div2); });
+
+ document.body.appendChild(div1);
+ document.body.appendChild(div2);
+ });
+ }));
+
+ xdescribe('getUserMedia', () => {
+ it('navigator.mediaDevices.getUserMedia should in zone',
+ ifEnvSupportsWithDone(
+ () => {
+ return !isEdge() && navigator && navigator.mediaDevices &&
+ typeof navigator.mediaDevices.getUserMedia === 'function';
+ },
+ (done: Function) => {
+ const zone = Zone.current.fork({name: 'media'});
+ zone.run(() => {
+ const constraints = {audio: true, video: {width: 1280, height: 720}};
+
+ navigator.mediaDevices.getUserMedia(constraints)
+ .then(function(mediaStream) {
+ expect(Zone.current.name).toEqual(zone.name);
+ done();
+ })
+ .catch(function(err) {
+ console.log(err.name + ': ' + err.message);
+ expect(Zone.current.name).toEqual(zone.name);
+ done();
+ });
+ });
+ }));
+
+ it('navigator.getUserMedia should in zone',
+ ifEnvSupportsWithDone(
+ () => {
+ return !isEdge() && navigator && typeof navigator.getUserMedia === 'function';
+ },
+ (done: Function) => {
+ const zone = Zone.current.fork({name: 'media'});
+ zone.run(() => {
+ const constraints = {audio: true, video: {width: 1280, height: 720}};
+ navigator.getUserMedia(
+ constraints,
+ () => {
+ expect(Zone.current.name).toEqual(zone.name);
+ done();
+ },
+ () => {
+ expect(Zone.current.name).toEqual(zone.name);
+ done();
+ });
+ });
+ }));
+ });
+ });
+});
diff --git a/packages/zone.js/test/browser/custom-element.spec.js b/packages/zone.js/test/browser/custom-element.spec.js
new file mode 100644
index 0000000000..a5456c48eb
--- /dev/null
+++ b/packages/zone.js/test/browser/custom-element.spec.js
@@ -0,0 +1,96 @@
+/**
+ * @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
+ */
+
+/*
+ * check that document.registerElement(name, { prototype: proto });
+ * is properly patched
+ */
+
+function customElementsSupport() {
+ return 'registerElement' in document;
+}
+customElementsSupport.message = 'window.customElements';
+
+describe('customElements', function() {
+ const testZone = Zone.current.fork({name: 'test'});
+ const bridge = {
+ connectedCallback: () => {},
+ disconnectedCallback: () => {},
+ adoptedCallback: () => {},
+ attributeChangedCallback: () => {}
+ };
+
+ class TestCustomElement extends HTMLElement {
+ constructor() { super(); }
+
+ static get observedAttributes() { return ['attr1', 'attr2']; }
+
+ connectedCallback() { return bridge.connectedCallback(); }
+
+ disconnectedCallback() { return bridge.disconnectedCallback(); }
+
+ attributeChangedCallback(attrName, oldVal, newVal) {
+ return bridge.attributeChangedCallback(attrName, oldVal, newVal);
+ }
+
+ adoptedCallback() { return bridge.adoptedCallback(); }
+ }
+
+ testZone.run(() => { customElements.define('x-test', TestCustomElement); });
+
+ let elt;
+
+ beforeEach(() => {
+ bridge.connectedCallback = () => {};
+ bridge.disconnectedCallback = () => {};
+ bridge.attributeChangedCallback = () => {};
+ bridge.adoptedCallback = () => {};
+ });
+
+ afterEach(() => {
+ if (elt) {
+ document.body.removeChild(elt);
+ elt = null;
+ }
+ });
+
+ it('should work with connectedCallback', function(done) {
+ bridge.connectedCallback = function() {
+ expect(Zone.current.name).toBe(testZone.name);
+ done();
+ };
+
+ elt = document.createElement('x-test');
+ document.body.appendChild(elt);
+ });
+
+ it('should work with disconnectedCallback', function(done) {
+ bridge.disconnectedCallback = function() {
+ expect(Zone.current.name).toBe(testZone.name);
+ done();
+ };
+
+ elt = document.createElement('x-test');
+ document.body.appendChild(elt);
+ document.body.removeChild(elt);
+ elt = null;
+ });
+
+ it('should work with attributeChanged', function(done) {
+ bridge.attributeChangedCallback = function(attrName, oldVal, newVal) {
+ expect(Zone.current.name).toBe(testZone.name);
+ expect(attrName).toEqual('attr1');
+ expect(newVal).toEqual('value1');
+ done();
+ };
+
+ elt = document.createElement('x-test');
+ document.body.appendChild(elt);
+ elt.setAttribute('attr1', 'value1');
+ });
+});
diff --git a/packages/zone.js/test/browser/define-property.spec.ts b/packages/zone.js/test/browser/define-property.spec.ts
new file mode 100644
index 0000000000..f9032fa108
--- /dev/null
+++ b/packages/zone.js/test/browser/define-property.spec.ts
@@ -0,0 +1,27 @@
+/**
+ * @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
+ */
+
+describe('defineProperty', function() {
+ it('should not throw when defining length on an array', function() {
+ const someArray: any[] = [];
+ expect(() => Object.defineProperty(someArray, 'length', {value: 2, writable: false}))
+ .not.toThrow();
+ });
+
+ it('should not throw error when try to defineProperty with a frozen desc', function() {
+ const obj = {};
+ const desc = Object.freeze({value: null, writable: true});
+ Object.defineProperty(obj, 'prop', desc);
+ });
+
+ it('should not throw error when try to defineProperty with a frozen obj', function() {
+ const obj = {};
+ Object.freeze(obj);
+ Object.defineProperty(obj, 'prop', {configurable: true, writable: true, value: 'value'});
+ });
+});
\ No newline at end of file
diff --git a/packages/zone.js/test/browser/element.spec.ts b/packages/zone.js/test/browser/element.spec.ts
new file mode 100644
index 0000000000..540c0c275d
--- /dev/null
+++ b/packages/zone.js/test/browser/element.spec.ts
@@ -0,0 +1,312 @@
+/**
+ * @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
+ */
+
+import {ifEnvSupports} from '../test-util';
+
+describe('element', function() {
+ let button: HTMLButtonElement;
+
+ beforeEach(function() {
+ button = document.createElement('button');
+ document.body.appendChild(button);
+ });
+
+ afterEach(function() { document.body.removeChild(button); });
+
+ // https://github.com/angular/zone.js/issues/190
+ it('should work when addEventListener / removeEventListener are called in the global context',
+ function() {
+ const clickEvent = document.createEvent('Event');
+ let callCount = 0;
+
+ clickEvent.initEvent('click', true, true);
+
+ const listener = function(event: Event) {
+ callCount++;
+ expect(event).toBe(clickEvent);
+ };
+
+ // `this` would be null inside the method when `addEventListener` is called from strict mode
+ // it would be `window`:
+ // - when called from non strict-mode,
+ // - when `window.addEventListener` is called explicitly.
+ addEventListener('click', listener);
+
+ button.dispatchEvent(clickEvent);
+ expect(callCount).toEqual(1);
+
+ removeEventListener('click', listener);
+ button.dispatchEvent(clickEvent);
+ expect(callCount).toEqual(1);
+ });
+
+ it('should work with addEventListener when called with a function listener', function() {
+ const clickEvent = document.createEvent('Event');
+ clickEvent.initEvent('click', true, true);
+
+ button.addEventListener('click', function(event) { expect(event).toBe(clickEvent as any); });
+
+ button.dispatchEvent(clickEvent);
+ });
+
+ it('should not call microtasks early when an event is invoked', function(done) {
+ let log = '';
+ button.addEventListener('click', () => {
+ Zone.current.scheduleMicroTask('test', () => log += 'microtask;');
+ log += 'click;';
+ });
+ button.click();
+
+ expect(log).toEqual('click;');
+ done();
+ });
+
+ it('should call microtasks early when an event is invoked', function(done) {
+ /*
+ * In this test we escape the Zone using unpatched setTimeout.
+ * This way the eventTask invoked from click will think it is the top most
+ * task and eagerly drain the microtask queue.
+ *
+ * THIS IS THE WRONG BEHAVIOR!
+ *
+ * But there is no easy way for the task to know if it is the top most task.
+ *
+ * Given that this can only arise when someone is emulating clicks on DOM in a synchronous
+ * fashion we have few choices:
+ * 1. Ignore as this is unlikely to be a problem outside of tests.
+ * 2. Monkey patch the event methods to increment the _numberOfNestedTaskFrames and prevent
+ * eager drainage.
+ * 3. Pay the cost of throwing an exception in event tasks and verifying that we are the
+ * top most frame.
+ *
+ * For now we are choosing to ignore it and assume that this arises in tests only.
+ * As an added measure we make sure that all jasmine tests always run in a task. See: jasmine.ts
+ */
+ (window as any)[(Zone as any).__symbol__('setTimeout')](() => {
+ let log = '';
+ button.addEventListener('click', () => {
+ Zone.current.scheduleMicroTask('test', () => log += 'microtask;');
+ log += 'click;';
+ });
+ button.click();
+
+ expect(log).toEqual('click;microtask;');
+ done();
+ });
+ });
+
+ it('should work with addEventListener when called with an EventListener-implementing listener',
+ function() {
+ const eventListener = {
+ x: 5,
+ handleEvent: function(event: Event) {
+ // Test that context is preserved
+ expect(this.x).toBe(5);
+
+ expect(event).toBe(clickEvent);
+ }
+ };
+
+ const clickEvent = document.createEvent('Event');
+ clickEvent.initEvent('click', true, true);
+
+ button.addEventListener('click', eventListener);
+
+ button.dispatchEvent(clickEvent);
+ });
+
+ it('should respect removeEventListener when called with a function listener', function() {
+ let log = '';
+ const logFunction = function logFunction() { log += 'a'; };
+
+ button.addEventListener('click', logFunction);
+ button.addEventListener('focus', logFunction);
+ button.click();
+ expect(log).toEqual('a');
+ const focusEvent = document.createEvent('Event');
+ focusEvent.initEvent('focus', true, true);
+ button.dispatchEvent(focusEvent);
+ expect(log).toEqual('aa');
+
+ button.removeEventListener('click', logFunction);
+ button.click();
+ expect(log).toEqual('aa');
+ });
+
+ it('should respect removeEventListener with an EventListener-implementing listener', function() {
+ const eventListener = {x: 5, handleEvent: jasmine.createSpy('handleEvent')};
+
+ button.addEventListener('click', eventListener);
+ button.removeEventListener('click', eventListener);
+
+ button.click();
+
+ expect(eventListener.handleEvent).not.toHaveBeenCalled();
+ });
+
+ it('should have no effect while calling addEventListener without listener', function() {
+ const onAddEventListenerSpy = jasmine.createSpy('addEventListener');
+ const eventListenerZone =
+ Zone.current.fork({name: 'eventListenerZone', onScheduleTask: onAddEventListenerSpy});
+ expect(function() {
+ eventListenerZone.run(function() {
+ button.addEventListener('click', null as any);
+ button.addEventListener('click', undefined as any);
+ });
+ }).not.toThrowError();
+ expect(onAddEventListenerSpy).not.toHaveBeenCalledWith();
+ });
+
+ it('should have no effect while calling removeEventListener without listener', function() {
+ const onAddEventListenerSpy = jasmine.createSpy('removeEventListener');
+ const eventListenerZone =
+ Zone.current.fork({name: 'eventListenerZone', onScheduleTask: onAddEventListenerSpy});
+ expect(function() {
+ eventListenerZone.run(function() {
+ button.removeEventListener('click', null as any);
+ button.removeEventListener('click', undefined as any);
+ });
+ }).not.toThrowError();
+ expect(onAddEventListenerSpy).not.toHaveBeenCalledWith();
+ });
+
+
+ it('should only add a listener once for a given set of arguments', function() {
+ const log: string[] = [];
+ const clickEvent = document.createEvent('Event');
+
+ function listener() { log.push('listener'); }
+
+ clickEvent.initEvent('click', true, true);
+
+ button.addEventListener('click', listener);
+ button.addEventListener('click', listener);
+ button.addEventListener('click', listener);
+
+ button.dispatchEvent(clickEvent);
+ expect(log).toEqual(['listener']);
+
+ button.removeEventListener('click', listener);
+
+ button.dispatchEvent(clickEvent);
+ expect(log).toEqual(['listener']);
+ });
+
+ it('should correctly handler capturing versus nonCapturing eventListeners', function() {
+ const log: string[] = [];
+ const clickEvent = document.createEvent('Event');
+
+ function capturingListener() { log.push('capturingListener'); }
+
+ function bubblingListener() { log.push('bubblingListener'); }
+
+ clickEvent.initEvent('click', true, true);
+
+ document.body.addEventListener('click', capturingListener, true);
+ document.body.addEventListener('click', bubblingListener);
+
+ button.dispatchEvent(clickEvent);
+
+ expect(log).toEqual(['capturingListener', 'bubblingListener']);
+ });
+
+ it('should correctly handler a listener that is both capturing and nonCapturing', function() {
+ const log: string[] = [];
+ const clickEvent = document.createEvent('Event');
+
+ function listener() { log.push('listener'); }
+
+ clickEvent.initEvent('click', true, true);
+
+ document.body.addEventListener('click', listener, true);
+ document.body.addEventListener('click', listener);
+
+ button.dispatchEvent(clickEvent);
+
+ document.body.removeEventListener('click', listener, true);
+ document.body.removeEventListener('click', listener);
+
+ button.dispatchEvent(clickEvent);
+
+ expect(log).toEqual(['listener', 'listener']);
+ });
+
+ describe('onclick', function() {
+ function supportsOnClick() {
+ const div = document.createElement('div');
+ const clickPropDesc = Object.getOwnPropertyDescriptor(div, 'onclick');
+ return !(
+ EventTarget && div instanceof EventTarget && clickPropDesc &&
+ clickPropDesc.value === null);
+ }
+ (supportsOnClick).message = 'Supports Element#onclick patching';
+
+
+ ifEnvSupports(supportsOnClick, function() {
+ it('should spawn new child zones', function() {
+ let run = false;
+ button.onclick = function() { run = true; };
+
+ button.click();
+ expect(run).toBeTruthy();
+ });
+ });
+
+
+ it('should only allow one onclick handler', function() {
+ let log = '';
+ button.onclick = function() { log += 'a'; };
+ button.onclick = function() { log += 'b'; };
+
+ button.click();
+ expect(log).toEqual('b');
+ });
+
+
+ it('should handler removing onclick', function() {
+ let log = '';
+ button.onclick = function() { log += 'a'; };
+ button.onclick = null as any;
+
+ button.click();
+ expect(log).toEqual('');
+ });
+
+ it('should be able to deregister the same event twice', function() {
+ const listener = (event: Event) => {};
+ document.body.addEventListener('click', listener, false);
+ document.body.removeEventListener('click', listener, false);
+ document.body.removeEventListener('click', listener, false);
+ });
+ });
+
+ describe('onEvent default behavior', function() {
+ let checkbox: HTMLInputElement;
+ beforeEach(function() {
+ checkbox = document.createElement('input');
+ checkbox.type = 'checkbox';
+ document.body.appendChild(checkbox);
+ });
+
+ afterEach(function() { document.body.removeChild(checkbox); });
+
+ it('should be possible to prevent default behavior by returning false', function() {
+ checkbox.onclick = function() { return false; };
+
+ checkbox.click();
+ expect(checkbox.checked).toBe(false);
+ });
+
+ it('should have no effect on default behavior when not returning anything', function() {
+ checkbox.onclick = function() {};
+
+ checkbox.click();
+ expect(checkbox.checked).toBe(true);
+ });
+ });
+});
diff --git a/packages/zone.js/test/browser/geolocation.spec.manual.ts b/packages/zone.js/test/browser/geolocation.spec.manual.ts
new file mode 100644
index 0000000000..17e1c6a5ad
--- /dev/null
+++ b/packages/zone.js/test/browser/geolocation.spec.manual.ts
@@ -0,0 +1,38 @@
+/**
+ * @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
+ */
+
+import {ifEnvSupports} from '../test-util';
+
+function supportsGeolocation() {
+ return 'geolocation' in navigator;
+}
+(supportsGeolocation).message = 'Geolocation';
+
+describe('Geolocation', ifEnvSupports(supportsGeolocation, function() {
+ const testZone = Zone.current.fork({name: 'geotest'});
+
+ it('should work for getCurrentPosition', function(done) {
+ testZone.run(function() {
+ navigator.geolocation.getCurrentPosition(function(pos) {
+ expect(Zone.current).toBe(testZone);
+ done();
+ });
+ });
+ }, 10000);
+
+ it('should work for watchPosition', function(done) {
+ testZone.run(function() {
+ let watchId: number;
+ watchId = navigator.geolocation.watchPosition(function(pos) {
+ expect(Zone.current).toBe(testZone);
+ navigator.geolocation.clearWatch(watchId);
+ done();
+ });
+ });
+ }, 10000);
+ }));
diff --git a/packages/zone.js/test/browser/registerElement.spec.ts b/packages/zone.js/test/browser/registerElement.spec.ts
new file mode 100644
index 0000000000..3b3ffdf89c
--- /dev/null
+++ b/packages/zone.js/test/browser/registerElement.spec.ts
@@ -0,0 +1,164 @@
+/**
+ * @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
+ */
+
+/*
+ * check that document.registerElement(name, { prototype: proto });
+ * is properly patched
+ */
+
+import {ifEnvSupports} from '../test-util';
+
+function registerElement() {
+ return ('registerElement' in document) && (typeof customElements === 'undefined');
+}
+(registerElement).message = 'document.registerElement';
+
+describe(
+ 'document.registerElement', ifEnvSupports(registerElement, function() {
+ // register a custom element for each callback
+ const callbackNames = ['created', 'attached', 'detached', 'attributeChanged'];
+ const callbacks: any = {};
+ const testZone = Zone.current.fork({name: 'test'});
+ let customElements;
+
+ customElements = testZone.run(function() {
+ callbackNames.forEach(function(callbackName) {
+ const fullCallbackName = callbackName + 'Callback';
+ const proto = Object.create(HTMLElement.prototype);
+ (proto as any)[fullCallbackName] = function(arg: any) { callbacks[callbackName](arg); };
+ (document).registerElement('x-' + callbackName.toLowerCase(), {prototype: proto});
+ });
+ });
+
+ it('should work with createdCallback', function(done) {
+ callbacks.created = function() {
+ expect(Zone.current).toBe(testZone);
+ done();
+ };
+
+ document.createElement('x-created');
+ });
+
+
+ it('should work with attachedCallback', function(done) {
+ callbacks.attached = function() {
+ expect(Zone.current).toBe(testZone);
+ done();
+ };
+
+ const elt = document.createElement('x-attached');
+ document.body.appendChild(elt);
+ document.body.removeChild(elt);
+ });
+
+
+ it('should work with detachedCallback', function(done) {
+ callbacks.detached = function() {
+ expect(Zone.current).toBe(testZone);
+ done();
+ };
+
+ const elt = document.createElement('x-detached');
+ document.body.appendChild(elt);
+ document.body.removeChild(elt);
+ });
+
+
+ it('should work with attributeChanged', function(done) {
+ callbacks.attributeChanged = function() {
+ expect(Zone.current).toBe(testZone);
+ done();
+ };
+
+ const elt = document.createElement('x-attributechanged');
+ elt.id = 'bar';
+ });
+
+
+ it('should work with non-writable, non-configurable prototypes created with defineProperty',
+ function(done) {
+ testZone.run(function() {
+ const proto = Object.create(HTMLElement.prototype);
+
+ Object.defineProperty(
+ proto, 'createdCallback',
+ {writable: false, configurable: false, value: checkZone});
+
+ (document).registerElement('x-prop-desc', {prototype: proto});
+
+ function checkZone() {
+ expect(Zone.current).toBe(testZone);
+ done();
+ }
+ });
+
+ const elt = document.createElement('x-prop-desc');
+ });
+
+
+ it('should work with non-writable, non-configurable prototypes created with defineProperties',
+ function(done) {
+ testZone.run(function() {
+ const proto = Object.create(HTMLElement.prototype);
+
+ Object.defineProperties(
+ proto,
+ {createdCallback: {writable: false, configurable: false, value: checkZone}});
+
+ (document).registerElement('x-props-desc', {prototype: proto});
+
+ function checkZone() {
+ expect(Zone.current).toBe(testZone);
+ done();
+ }
+ });
+
+ const elt = document.createElement('x-props-desc');
+ });
+
+ it('should not throw with frozen prototypes ', function() {
+ testZone.run(function() {
+ const proto = Object.create(HTMLElement.prototype, Object.freeze({
+ createdCallback:
+ {value: () => {}, writable: true, configurable: true}
+ }));
+
+ Object.defineProperty(
+ proto, 'createdCallback', {writable: false, configurable: false});
+
+ expect(function() {
+ (document).registerElement('x-frozen-desc', {prototype: proto});
+ }).not.toThrow();
+ });
+ });
+
+
+ it('should check bind callback if not own property', function(done) {
+ testZone.run(function() {
+ const originalProto = {createdCallback: checkZone};
+
+ const secondaryProto = Object.create(originalProto);
+ expect(secondaryProto.createdCallback).toBe(originalProto.createdCallback);
+
+ (document).registerElement('x-inherited-callback', {prototype: secondaryProto});
+ expect(secondaryProto.createdCallback).not.toBe(originalProto.createdCallback);
+
+ function checkZone() {
+ expect(Zone.current).toBe(testZone);
+ done();
+ }
+
+ const elt = document.createElement('x-inherited-callback');
+ });
+ });
+
+
+ it('should not throw if no options passed to registerElement', function() {
+ expect(function() { (document).registerElement('x-no-opts'); }).not.toThrow();
+ });
+ }));
diff --git a/packages/zone.js/test/browser/requestAnimationFrame.spec.ts b/packages/zone.js/test/browser/requestAnimationFrame.spec.ts
new file mode 100644
index 0000000000..9c7788c3e6
--- /dev/null
+++ b/packages/zone.js/test/browser/requestAnimationFrame.spec.ts
@@ -0,0 +1,52 @@
+/**
+ * @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
+ */
+
+import {ifEnvSupports} from '../test-util';
+declare const window: any;
+
+describe('requestAnimationFrame', function() {
+ const functions =
+ ['requestAnimationFrame', 'webkitRequestAnimationFrame', 'mozRequestAnimationFrame'];
+
+ functions.forEach(function(fnName) {
+ describe(fnName, ifEnvSupports(fnName, function() {
+ const originalTimeout: number = (jasmine).DEFAULT_TIMEOUT_INTERVAL;
+ beforeEach(() => { (jasmine).DEFAULT_TIMEOUT_INTERVAL = 10000; });
+
+ afterEach(() => { (jasmine).DEFAULT_TIMEOUT_INTERVAL = originalTimeout; });
+ const rAF = window[fnName];
+
+ it('should be tolerant of invalid arguments', function() {
+ // rAF throws an error on invalid arguments, so expect that.
+ expect(function() { rAF(null); }).toThrow();
+ });
+
+ it('should bind to same zone when called recursively', function(done) {
+ Zone.current.fork({name: 'TestZone'}).run(() => {
+ let frames = 0;
+ let previousTimeStamp = 0;
+
+ function frameCallback(timestamp: number) {
+ expect(timestamp).toMatch(/^[\d.]+$/);
+ // expect previous <= current
+ expect(previousTimeStamp).not.toBeGreaterThan(timestamp);
+ previousTimeStamp = timestamp;
+
+ if (frames++ > 15) {
+ (jasmine).DEFAULT_TIMEOUT_INTERVAL = originalTimeout;
+ return done();
+ }
+ rAF(frameCallback);
+ }
+
+ rAF(frameCallback);
+ });
+ });
+ }));
+ });
+});
diff --git a/packages/zone.js/test/browser_entry_point.ts b/packages/zone.js/test/browser_entry_point.ts
new file mode 100644
index 0000000000..1057086883
--- /dev/null
+++ b/packages/zone.js/test/browser_entry_point.ts
@@ -0,0 +1,30 @@
+/**
+ * @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
+ */
+import '../lib/common/error-rewrite';
+
+// import 'core-js/features/set';
+// import 'core-js/features/map';
+// List all tests here:
+import './common_tests';
+import './browser/browser.spec';
+import './browser/define-property.spec';
+import './browser/element.spec';
+import './browser/FileReader.spec';
+// import './browser/geolocation.spec.manual';
+import './browser/HTMLImports.spec';
+import './browser/MutationObserver.spec';
+import './browser/registerElement.spec';
+import './browser/requestAnimationFrame.spec';
+import './browser/WebSocket.spec';
+import './browser/XMLHttpRequest.spec';
+import './browser/MediaQuery.spec';
+import './browser/Notification.spec';
+import './browser/Worker.spec';
+import './mocha-patch.spec';
+import './jasmine-patch.spec';
+import './extra/cordova.spec';
diff --git a/packages/zone.js/test/browser_es2015_entry_point.ts b/packages/zone.js/test/browser_es2015_entry_point.ts
new file mode 100644
index 0000000000..d66e77c97f
--- /dev/null
+++ b/packages/zone.js/test/browser_es2015_entry_point.ts
@@ -0,0 +1,9 @@
+/**
+ * @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
+ */
+
+import './browser/custom-element.spec';
diff --git a/packages/zone.js/test/browser_symbol_setup.ts b/packages/zone.js/test/browser_symbol_setup.ts
new file mode 100644
index 0000000000..81cbcc1638
--- /dev/null
+++ b/packages/zone.js/test/browser_symbol_setup.ts
@@ -0,0 +1,3 @@
+(window as any).global = window;
+// Change default symbol prefix for testing to ensure no hard-coded references.
+(window as any)['__Zone_symbol_prefix'] = '_test__';
diff --git a/packages/zone.js/test/closure/zone.closure.ts b/packages/zone.js/test/closure/zone.closure.ts
new file mode 100644
index 0000000000..00d0d42143
--- /dev/null
+++ b/packages/zone.js/test/closure/zone.closure.ts
@@ -0,0 +1,130 @@
+/**
+ * @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
+ */
+import '../../dist/zone-node';
+const testClosureFunction = () => {
+ const logs: string[] = [];
+ // call all Zone exposed functions
+ const testZoneSpec: ZoneSpec = {
+ name: 'closure',
+ properties: {},
+ onFork: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
+ zoneSpec: ZoneSpec) => { return parentZoneDelegate.fork(targetZone, zoneSpec); },
+
+ onIntercept:
+ (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, delegate: Function,
+ source: string) => { return parentZoneDelegate.intercept(targetZone, delegate, source); },
+
+ onInvoke: function(
+ parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, delegate: Function,
+ applyThis?: any, applyArgs?: any[], source?: string) {
+ return parentZoneDelegate.invoke(targetZone, delegate, applyThis, applyArgs, source);
+ },
+
+ onHandleError: function(
+ parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, error: any) {
+ return parentZoneDelegate.handleError(targetZone, error);
+ },
+
+ onScheduleTask: function(
+ parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task) {
+ return parentZoneDelegate.scheduleTask(targetZone, task);
+ },
+
+ onInvokeTask: function(
+ parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task,
+ applyThis?: any, applyArgs?: any[]) {
+ return parentZoneDelegate.invokeTask(targetZone, task, applyThis, applyArgs);
+ },
+
+ onCancelTask: function(
+ parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task) {
+ return parentZoneDelegate.cancelTask(targetZone, task);
+ },
+
+ onHasTask: function(
+ delegate: ZoneDelegate, current: Zone, target: Zone, hasTaskState: HasTaskState) {
+ return delegate.hasTask(target, hasTaskState);
+ }
+ };
+
+ const testZone: Zone = Zone.current.fork(testZoneSpec);
+ testZone.runGuarded(() => {
+ testZone.run(() => {
+ const properties = testZoneSpec.properties;
+ properties !['key'] = 'value';
+ const keyZone = Zone.current.getZoneWith('key');
+
+ logs.push('current' + Zone.current.name);
+ logs.push('parent' + Zone.current.parent !.name);
+ logs.push('getZoneWith' + keyZone !.name);
+ logs.push('get' + keyZone !.get('key'));
+ logs.push('root' + Zone.root.name);
+ Object.keys((Zone as any).prototype).forEach(key => { logs.push(key); });
+ Object.keys(testZoneSpec).forEach(key => { logs.push(key); });
+
+ const task = Zone.current.scheduleMicroTask('testTask', () => {}, undefined, () => {});
+ Object.keys(task).forEach(key => { logs.push(key); });
+ });
+ });
+
+ const expectedResult = [
+ 'currentclosure',
+ 'parent',
+ 'getZoneWithclosure',
+ 'getvalue',
+ 'root',
+ 'parent',
+ 'name',
+ 'get',
+ 'getZoneWith',
+ 'fork',
+ 'wrap',
+ 'run',
+ 'runGuarded',
+ 'runTask',
+ 'scheduleTask',
+ 'scheduleMicroTask',
+ 'scheduleMacroTask',
+ 'scheduleEventTask',
+ 'cancelTask',
+ '_updateTaskCount',
+ 'name',
+ 'properties',
+ 'onFork',
+ 'onIntercept',
+ 'onInvoke',
+ 'onHandleError',
+ 'onScheduleTask',
+ 'onInvokeTask',
+ 'onCancelTask',
+ 'onHasTask',
+ '_zone',
+ 'runCount',
+ '_zoneDelegates',
+ '_state',
+ 'type',
+ 'source',
+ 'data',
+ 'scheduleFn',
+ 'cancelFn',
+ 'callback',
+ 'invoke'
+ ];
+
+ let result: boolean = true;
+ for (let i = 0; i < expectedResult.length; i++) {
+ if (expectedResult[i] !== logs[i]) {
+ console.log('Not Equals', expectedResult[i], logs[i]);
+ result = false;
+ }
+ }
+ process['exit'](result ? 0 : 1);
+};
+process['on']('uncaughtException', (err: any) => { process['exit'](1); });
+
+testClosureFunction();
diff --git a/packages/zone.js/test/common/Error.spec.ts b/packages/zone.js/test/common/Error.spec.ts
new file mode 100644
index 0000000000..656cba717f
--- /dev/null
+++ b/packages/zone.js/test/common/Error.spec.ts
@@ -0,0 +1,425 @@
+/**
+ * @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
+ */
+import {isBrowser} from '../../lib/common/utils';
+import {isSafari, zoneSymbol} from '../test-util';
+
+// simulate @angular/facade/src/error.ts
+class BaseError extends Error {
+ /** @internal **/
+ _nativeError: Error;
+
+ constructor(message: string) {
+ super(message);
+ const nativeError = new Error(message) as any as Error;
+ this._nativeError = nativeError;
+ }
+
+ get message() { return this._nativeError.message; }
+ set message(message) { this._nativeError.message = message; }
+ get name() { return this._nativeError.name; }
+ get stack() { return (this._nativeError as any).stack; }
+ set stack(value) { (this._nativeError as any).stack = value; }
+ toString() { return this._nativeError.toString(); }
+}
+
+class WrappedError extends BaseError {
+ originalError: any;
+
+ constructor(message: string, error: any) {
+ super(`${message} caused by: ${error instanceof Error ? error.message : error}`);
+ this.originalError = error;
+ }
+
+ get stack() {
+ return ((this.originalError instanceof Error ? this.originalError : this._nativeError) as any)
+ .stack;
+ }
+}
+
+class TestError extends WrappedError {
+ constructor(message: string, error: any) {
+ super(`${message} caused by: ${error instanceof Error ? error.message : error}`, error);
+ }
+
+ get message() { return 'test ' + this.originalError.message; }
+}
+
+class TestMessageError extends WrappedError {
+ constructor(message: string, error: any) {
+ super(`${message} caused by: ${error instanceof Error ? error.message : error}`, error);
+ }
+
+ get message() { return 'test ' + this.originalError.message; }
+
+ set message(value) { this.originalError.message = value; }
+}
+
+describe('ZoneAwareError', () => {
+ // If the environment does not supports stack rewrites, then these tests will fail
+ // and there is no point in running them.
+ const _global: any = typeof window !== 'undefined' ? window : global;
+ let config: any;
+ const __karma__ = _global.__karma__;
+ if (typeof __karma__ !== 'undefined') {
+ config = __karma__ && (__karma__ as any).config;
+ } else if (typeof process !== 'undefined') {
+ config = process.env;
+ }
+ const policy = (config && config['errorpolicy']) || 'default';
+ if (!(Error as any)['stackRewrite'] && policy !== 'disable') return;
+
+ it('should keep error prototype chain correctly', () => {
+ class MyError extends Error {}
+ const myError = new MyError();
+ expect(myError instanceof Error).toBe(true);
+ expect(myError instanceof MyError).toBe(true);
+ expect(myError.stack).not.toBe(undefined);
+ });
+
+ it('should instanceof error correctly', () => {
+ let myError = Error('myError');
+ expect(myError instanceof Error).toBe(true);
+ let myError1 = Error.call(undefined, 'myError');
+ expect(myError1 instanceof Error).toBe(true);
+ let myError2 = Error.call(global, 'myError');
+ expect(myError2 instanceof Error).toBe(true);
+ let myError3 = Error.call({}, 'myError');
+ expect(myError3 instanceof Error).toBe(true);
+ let myError4 = Error.call({test: 'test'}, 'myError');
+ expect(myError4 instanceof Error).toBe(true);
+ });
+
+ it('should return error itself from constructor', () => {
+ class MyError1 extends Error {
+ constructor() {
+ const err: any = super('MyError1');
+ this.message = err.message;
+ }
+ }
+ let myError1 = new MyError1();
+ expect(myError1.message).toEqual('MyError1');
+ expect(myError1.name).toEqual('Error');
+ });
+
+ it('should return error by calling error directly', () => {
+ let myError = Error('myError');
+ expect(myError.message).toEqual('myError');
+ let myError1 = Error.call(undefined, 'myError');
+ expect(myError1.message).toEqual('myError');
+ let myError2 = Error.call(global, 'myError');
+ expect(myError2.message).toEqual('myError');
+ let myError3 = Error.call({}, 'myError');
+ expect(myError3.message).toEqual('myError');
+ });
+
+ it('should have browser specified property', () => {
+ let myError = new Error('myError');
+ if (Object.prototype.hasOwnProperty.call(Error.prototype, 'description')) {
+ // in IE, error has description property
+ expect((myError).description).toEqual('myError');
+ }
+ if (Object.prototype.hasOwnProperty.call(Error.prototype, 'fileName')) {
+ // in firefox, error has fileName property
+ expect((myError).fileName).toBeTruthy();
+ }
+ });
+
+ it('should not use child Error class get/set in ZoneAwareError constructor', () => {
+ const func = () => {
+ const error = new BaseError('test');
+ expect(error.message).toEqual('test');
+ };
+
+ expect(func).not.toThrow();
+ });
+
+ it('should behave correctly with wrapped error', () => {
+ const error = new TestError('originalMessage', new Error('error message'));
+ expect(error.message).toEqual('test error message');
+ error.originalError.message = 'new error message';
+ expect(error.message).toEqual('test new error message');
+
+ const error1 = new TestMessageError('originalMessage', new Error('error message'));
+ expect(error1.message).toEqual('test error message');
+ error1.message = 'new error message';
+ expect(error1.message).toEqual('test new error message');
+ });
+
+ it('should copy customized NativeError properties to ZoneAwareError', () => {
+ const spy = jasmine.createSpy('errorCustomFunction');
+ const NativeError = (global as any)[(Zone as any).__symbol__('Error')];
+ NativeError.customFunction = function(args: any) { spy(args); };
+ expect((Error as any)['customProperty']).toBe('customProperty');
+ expect(typeof(Error as any)['customFunction']).toBe('function');
+ (Error as any)['customFunction']('test');
+ expect(spy).toHaveBeenCalledWith('test');
+ });
+
+ it('should always have stack property even without throw', () => {
+ // in IE, the stack will be undefined without throw
+ // in ZoneAwareError, we will make stack always be
+ // there event without throw
+ const error = new Error('test');
+ const errorWithoutNew = Error('test');
+ expect(error.stack !.split('\n').length > 0).toBeTruthy();
+ expect(errorWithoutNew.stack !.split('\n').length > 0).toBeTruthy();
+ });
+
+ it('should show zone names in stack frames and remove extra frames', () => {
+ if (policy === 'disable' || !(Error as any)['stackRewrite']) {
+ return;
+ }
+ if (isBrowser && isSafari()) {
+ return;
+ }
+ const rootZone = Zone.root;
+ const innerZone = rootZone.fork({name: 'InnerZone'});
+
+ rootZone.run(testFn);
+ function testFn() {
+ let outside: any;
+ let inside: any;
+ let outsideWithoutNew: any;
+ let insideWithoutNew: any;
+ try {
+ throw new Error('Outside');
+ } catch (e) {
+ outside = e;
+ }
+ try {
+ throw Error('Outside');
+ } catch (e) {
+ outsideWithoutNew = e;
+ }
+ innerZone.run(function insideRun() {
+ try {
+ throw new Error('Inside');
+ } catch (e) {
+ inside = e;
+ }
+ try {
+ throw Error('Inside');
+ } catch (e) {
+ insideWithoutNew = e;
+ }
+ });
+
+ if (policy === 'lazy') {
+ outside.stack = outside.zoneAwareStack;
+ outsideWithoutNew.stack = outsideWithoutNew.zoneAwareStack;
+ inside.stack = inside.zoneAwareStack;
+ insideWithoutNew.stack = insideWithoutNew.zoneAwareStack;
+ }
+
+ expect(outside.stack).toEqual(outside.zoneAwareStack);
+ expect(outsideWithoutNew.stack).toEqual(outsideWithoutNew.zoneAwareStack);
+ expect(inside !.stack).toEqual(inside !.zoneAwareStack);
+ expect(insideWithoutNew !.stack).toEqual(insideWithoutNew !.zoneAwareStack);
+ expect(typeof inside !.originalStack).toEqual('string');
+ expect(typeof insideWithoutNew !.originalStack).toEqual('string');
+ const outsideFrames = outside.stack !.split(/\n/);
+ const insideFrames = inside !.stack !.split(/\n/);
+ const outsideWithoutNewFrames = outsideWithoutNew !.stack !.split(/\n/);
+ const insideWithoutNewFrames = insideWithoutNew !.stack !.split(/\n/);
+
+ // throw away first line if it contains the error
+ if (/Outside/.test(outsideFrames[0])) {
+ outsideFrames.shift();
+ }
+ if (/Error /.test(outsideFrames[0])) {
+ outsideFrames.shift();
+ }
+
+ if (/Outside/.test(outsideWithoutNewFrames[0])) {
+ outsideWithoutNewFrames.shift();
+ }
+ if (/Error /.test(outsideWithoutNewFrames[0])) {
+ outsideWithoutNewFrames.shift();
+ }
+
+ if (/Inside/.test(insideFrames[0])) {
+ insideFrames.shift();
+ }
+ if (/Error /.test(insideFrames[0])) {
+ insideFrames.shift();
+ }
+
+ if (/Inside/.test(insideWithoutNewFrames[0])) {
+ insideWithoutNewFrames.shift();
+ }
+ if (/Error /.test(insideWithoutNewFrames[0])) {
+ insideWithoutNewFrames.shift();
+ }
+ expect(outsideFrames[0]).toMatch(/testFn.*[]/);
+
+ expect(insideFrames[0]).toMatch(/insideRun.*[InnerZone]]/);
+ expect(insideFrames[1]).toMatch(/testFn.*[]]/);
+
+ expect(outsideWithoutNewFrames[0]).toMatch(/testFn.*[]/);
+
+ expect(insideWithoutNewFrames[0]).toMatch(/insideRun.*[InnerZone]]/);
+ expect(insideWithoutNewFrames[1]).toMatch(/testFn.*[]]/);
+ }
+ });
+
+ const zoneAwareFrames = [
+ 'Zone.run', 'Zone.runGuarded', 'Zone.scheduleEventTask', 'Zone.scheduleMicroTask',
+ 'Zone.scheduleMacroTask', 'Zone.runTask', 'ZoneDelegate.scheduleTask',
+ 'ZoneDelegate.invokeTask', 'zoneAwareAddListener', 'Zone.prototype.run',
+ 'Zone.prototype.runGuarded', 'Zone.prototype.scheduleEventTask',
+ 'Zone.prototype.scheduleMicroTask', 'Zone.prototype.scheduleMacroTask',
+ 'Zone.prototype.runTask', 'ZoneDelegate.prototype.scheduleTask',
+ 'ZoneDelegate.prototype.invokeTask', 'ZoneTask.invokeTask'
+ ];
+
+ function assertStackDoesNotContainZoneFrames(err: any) {
+ const frames = policy === 'lazy' ? err.zoneAwareStack.split('\n') : err.stack.split('\n');
+ if (policy === 'disable') {
+ let hasZoneStack = false;
+ for (let i = 0; i < frames.length; i++) {
+ if (hasZoneStack) {
+ break;
+ }
+ hasZoneStack = zoneAwareFrames.filter(f => frames[i].indexOf(f) !== -1).length > 0;
+ }
+ if (!hasZoneStack) {
+ console.log('stack', err.originalStack);
+ }
+ expect(hasZoneStack).toBe(true);
+ } else {
+ for (let i = 0; i < frames.length; i++) {
+ expect(zoneAwareFrames.filter(f => frames[i].indexOf(f) !== -1)).toEqual([]);
+ }
+ }
+ };
+
+ const errorZoneSpec = {
+ name: 'errorZone',
+ done: <(() => void)|null>null,
+ onHandleError:
+ (parentDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, error: Error) => {
+ assertStackDoesNotContainZoneFrames(error);
+ setTimeout(() => { errorZoneSpec.done && errorZoneSpec.done(); }, 0);
+ return false;
+ }
+ };
+
+ const errorZone = Zone.root.fork(errorZoneSpec);
+
+ const assertStackDoesNotContainZoneFramesTest = function(testFn: Function) {
+ return function(done: () => void) {
+ errorZoneSpec.done = done;
+ errorZone.run(testFn);
+ };
+ };
+
+ describe('Error stack', () => {
+ it('Error with new which occurs in setTimeout callback should not have zone frames visible',
+ assertStackDoesNotContainZoneFramesTest(
+ () => { setTimeout(() => { throw new Error('timeout test error'); }, 10); }));
+
+ it('Error without new which occurs in setTimeout callback should not have zone frames visible',
+ assertStackDoesNotContainZoneFramesTest(
+ () => { setTimeout(() => { throw Error('test error'); }, 10); }));
+
+ it('Error with new which cause by promise rejection should not have zone frames visible',
+ (done) => {
+ const p = new Promise(
+ (resolve, reject) => { setTimeout(() => { reject(new Error('test error')); }); });
+ p.catch(err => {
+ assertStackDoesNotContainZoneFrames(err);
+ done();
+ });
+ });
+
+ it('Error without new which cause by promise rejection should not have zone frames visible',
+ (done) => {
+ const p = new Promise(
+ (resolve, reject) => { setTimeout(() => { reject(Error('test error')); }); });
+ p.catch(err => {
+ assertStackDoesNotContainZoneFrames(err);
+ done();
+ });
+ });
+
+ it('Error with new which occurs in eventTask callback should not have zone frames visible',
+ assertStackDoesNotContainZoneFramesTest(() => {
+ const task = Zone.current.scheduleEventTask('errorEvent', () => {
+ throw new Error('test error');
+ }, undefined, () => null, undefined);
+ task.invoke();
+ }));
+
+ it('Error without new which occurs in eventTask callback should not have zone frames visible',
+ assertStackDoesNotContainZoneFramesTest(() => {
+ const task = Zone.current.scheduleEventTask(
+ 'errorEvent', () => { throw Error('test error'); }, undefined, () => null, undefined);
+ task.invoke();
+ }));
+
+ it('Error with new which occurs in longStackTraceZone should not have zone frames and longStackTraceZone frames visible',
+ assertStackDoesNotContainZoneFramesTest(() => {
+ const task = Zone.current.fork((Zone as any)['longStackTraceZoneSpec'])
+ .scheduleEventTask('errorEvent', () => {
+ throw new Error('test error');
+ }, undefined, () => null, undefined);
+ task.invoke();
+ }));
+
+ it('Error without new which occurs in longStackTraceZone should not have zone frames and longStackTraceZone frames visible',
+ assertStackDoesNotContainZoneFramesTest(() => {
+ const task = Zone.current.fork((Zone as any)['longStackTraceZoneSpec'])
+ .scheduleEventTask('errorEvent', () => {
+ throw Error('test error');
+ }, undefined, () => null, undefined);
+ task.invoke();
+ }));
+
+ it('stack frames of the callback in user customized zoneSpec should be kept',
+ assertStackDoesNotContainZoneFramesTest(() => {
+ const task = Zone.current.fork((Zone as any)['longStackTraceZoneSpec'])
+ .fork({
+ name: 'customZone',
+ onScheduleTask: (parentDelegate, currentZone, targetZone, task) => {
+ return parentDelegate.scheduleTask(targetZone, task);
+ },
+ onHandleError: (parentDelegate, currentZone, targetZone, error) => {
+ parentDelegate.handleError(targetZone, error);
+ const containsCustomZoneSpecStackTrace =
+ error.stack.indexOf('onScheduleTask') !== -1;
+ expect(containsCustomZoneSpecStackTrace).toBeTruthy();
+ return false;
+ }
+ })
+ .scheduleEventTask('errorEvent', () => {
+ throw new Error('test error');
+ }, undefined, () => null, undefined);
+ task.invoke();
+ }));
+
+ it('should be able to generate zone free stack even NativeError stack is readonly', function() {
+ const _global: any =
+ typeof window === 'object' && window || typeof self === 'object' && self || global;
+ const NativeError = _global[zoneSymbol('Error')];
+ const desc = Object.getOwnPropertyDescriptor(NativeError.prototype, 'stack');
+ if (desc) {
+ const originalSet: ((value: any) => void)|undefined = desc.set;
+ // make stack readonly
+ desc.set = null as any;
+
+ try {
+ const error = new Error('test error');
+ expect(error.stack).toBeTruthy();
+ assertStackDoesNotContainZoneFrames(error);
+ } finally {
+ desc.set = originalSet;
+ }
+ }
+ });
+ });
+});
diff --git a/packages/zone.js/test/common/Promise.spec.ts b/packages/zone.js/test/common/Promise.spec.ts
new file mode 100644
index 0000000000..7165b184be
--- /dev/null
+++ b/packages/zone.js/test/common/Promise.spec.ts
@@ -0,0 +1,521 @@
+/**
+ * @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
+ */
+
+import {isNode, zoneSymbol} from '../../lib/common/utils';
+import {ifEnvSupports} from '../test-util';
+
+declare const global: any;
+
+class MicroTaskQueueZoneSpec implements ZoneSpec {
+ name: string = 'MicroTaskQueue';
+ queue: MicroTask[] = [];
+ properties = {queue: this.queue, flush: this.flush.bind(this)};
+
+ flush() {
+ while (this.queue.length) {
+ const task = this.queue.shift();
+ task !.invoke();
+ }
+ }
+
+ onScheduleTask(delegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task): any {
+ this.queue.push(task as MicroTask);
+ }
+}
+
+function flushMicrotasks() {
+ Zone.current.get('flush')();
+}
+
+class TestRejection {
+ prop1?: string;
+ prop2?: string;
+}
+
+describe(
+ 'Promise', ifEnvSupports('Promise', function() {
+ if (!global.Promise) return;
+ let log: string[];
+ let queueZone: Zone;
+ let testZone: Zone;
+ let pZone: Zone;
+
+ beforeEach(() => {
+ testZone = Zone.current.fork({name: 'TestZone'});
+
+ pZone = Zone.current.fork({
+ name: 'promise-zone',
+ onScheduleTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
+ task: Task): any => {
+ log.push('scheduleTask');
+ parentZoneDelegate.scheduleTask(targetZone, task);
+ }
+ });
+
+ queueZone = Zone.current.fork(new MicroTaskQueueZoneSpec());
+
+ log = [];
+ });
+
+ xit('should allow set es6 Promise after load ZoneAwarePromise', (done) => {
+ const ES6Promise = require('es6-promise').Promise;
+ const NativePromise = global[zoneSymbol('Promise')];
+
+ try {
+ global['Promise'] = ES6Promise;
+ Zone.assertZonePatched();
+ expect(global[zoneSymbol('Promise')]).toBe(ES6Promise);
+ const promise = Promise.resolve(0);
+ console.log('promise', promise);
+ promise
+ .then(value => {
+ expect(value).toBe(0);
+ done();
+ })
+ .catch(error => { fail(error); });
+ } finally {
+ global['Promise'] = NativePromise;
+ Zone.assertZonePatched();
+ expect(global[zoneSymbol('Promise')]).toBe(NativePromise);
+ }
+ });
+
+ it('should pretend to be a native code',
+ () => { expect(String(Promise).indexOf('[native code]') >= 0).toBe(true); });
+
+ it('should use native toString for promise instance', () => {
+ expect(Object.prototype.toString.call(Promise.resolve())).toEqual('[object Promise]');
+ });
+
+ it('should make sure that new Promise is instance of Promise', () => {
+ expect(Promise.resolve(123) instanceof Promise).toBe(true);
+ expect(new Promise(() => null) instanceof Promise).toBe(true);
+ });
+
+ xit('should ensure that Promise this is instanceof Promise', () => {
+ expect(() => {
+ Promise.call({}, () => null);
+ }).toThrowError('Must be an instanceof Promise.');
+ });
+
+ xit('should allow subclassing', () => {
+ class MyPromise extends Promise {
+ constructor(fn: any) { super(fn); }
+ }
+ expect(new MyPromise(null).then(() => null) instanceof MyPromise).toBe(true);
+ });
+
+ it('should intercept scheduling of resolution and then', (done) => {
+ pZone.run(() => {
+ let p: Promise =
+ new Promise(function(resolve, reject) { expect(resolve('RValue')).toBe(undefined); });
+ expect(log).toEqual([]);
+ expect(p instanceof Promise).toBe(true);
+ p = p.then((v) => {
+ log.push(v);
+ expect(v).toBe('RValue');
+ expect(log).toEqual(['scheduleTask', 'RValue']);
+ return 'second value';
+ });
+ expect(p instanceof Promise).toBe(true);
+ expect(log).toEqual(['scheduleTask']);
+ p = p.then((v) => {
+ log.push(v);
+ expect(log).toEqual(['scheduleTask', 'RValue', 'scheduleTask', 'second value']);
+ done();
+ });
+ expect(p instanceof Promise).toBe(true);
+ expect(log).toEqual(['scheduleTask']);
+ });
+ });
+
+ it('should allow sync resolution of promises', () => {
+ queueZone.run(() => {
+ const flush = Zone.current.get('flush');
+ const queue = Zone.current.get('queue');
+ const p = new Promise(function(resolve, reject) { resolve('RValue'); })
+ .then((v: string) => {
+ log.push(v);
+ return 'second value';
+ })
+ .then((v: string) => { log.push(v); });
+ expect(queue.length).toEqual(1);
+ expect(log).toEqual([]);
+ flush();
+ expect(log).toEqual(['RValue', 'second value']);
+ });
+ });
+
+ it('should allow sync resolution of promises returning promises', () => {
+ queueZone.run(() => {
+ const flush = Zone.current.get('flush');
+ const queue = Zone.current.get('queue');
+ const p =
+ new Promise(function(resolve, reject) { resolve(Promise.resolve('RValue')); })
+ .then((v: string) => {
+ log.push(v);
+ return Promise.resolve('second value');
+ })
+ .then((v: string) => { log.push(v); });
+ expect(queue.length).toEqual(1);
+ expect(log).toEqual([]);
+ flush();
+ expect(log).toEqual(['RValue', 'second value']);
+ });
+ });
+
+ describe('Promise API', function() {
+ it('should work with .then', function(done) {
+ let resolve: Function|null = null;
+
+ testZone.run(function() {
+ new Promise(function(resolveFn) { resolve = resolveFn; }).then(function() {
+ expect(Zone.current).toBe(testZone);
+ done();
+ });
+ });
+
+ resolve !();
+ });
+
+ it('should work with .catch', function(done) {
+ let reject: (() => void)|null = null;
+
+ testZone.run(function() {
+ new Promise(function(resolveFn, rejectFn) { reject = rejectFn; })['catch'](function() {
+ expect(Zone.current).toBe(testZone);
+ done();
+ });
+ });
+
+
+ expect(reject !()).toBe(undefined);
+ });
+
+ it('should work with .finally with resolved promise', function(done) {
+ let resolve: Function|null = null;
+
+ testZone.run(function() {
+ (new Promise(function(resolveFn) { resolve = resolveFn; }) as any).finally(function() {
+ expect(arguments.length).toBe(0);
+ expect(Zone.current).toBe(testZone);
+ done();
+ });
+ });
+
+ resolve !('value');
+ });
+
+ it('should work with .finally with rejected promise', function(done) {
+ let reject: Function|null = null;
+
+ testZone.run(function() {
+ (new Promise(function(_, rejectFn) { reject = rejectFn; }) as any).finally(function() {
+ expect(arguments.length).toBe(0);
+ expect(Zone.current).toBe(testZone);
+ done();
+ });
+ });
+
+ reject !('error');
+ });
+
+ it('should work with Promise.resolve', () => {
+ queueZone.run(() => {
+ let value: any = null;
+ Promise.resolve('resolveValue').then((v) => value = v);
+ expect(Zone.current.get('queue').length).toEqual(1);
+ flushMicrotasks();
+ expect(value).toEqual('resolveValue');
+ });
+ });
+
+ it('should work with Promise.reject', () => {
+ queueZone.run(() => {
+ let value: any = null;
+ Promise.reject('rejectReason')['catch']((v) => value = v);
+ expect(Zone.current.get('queue').length).toEqual(1);
+ flushMicrotasks();
+ expect(value).toEqual('rejectReason');
+ });
+ });
+
+ describe('reject', () => {
+ it('should reject promise', () => {
+ queueZone.run(() => {
+ let value: any = null;
+ Promise.reject('rejectReason')['catch']((v) => value = v);
+ flushMicrotasks();
+ expect(value).toEqual('rejectReason');
+ });
+ });
+
+ it('should re-reject promise', () => {
+ queueZone.run(() => {
+ let value: any = null;
+ Promise.reject('rejectReason')['catch']((v) => { throw v; })['catch'](
+ (v) => value = v);
+ flushMicrotasks();
+ expect(value).toEqual('rejectReason');
+ });
+ });
+
+ it('should reject and recover promise', () => {
+ queueZone.run(() => {
+ let value: any = null;
+ Promise.reject('rejectReason')['catch']((v) => v).then((v) => value = v);
+ flushMicrotasks();
+ expect(value).toEqual('rejectReason');
+ });
+ });
+
+ it('should reject if chained promise does not catch promise', () => {
+ queueZone.run(() => {
+ let value: any = null;
+ Promise.reject('rejectReason')
+ .then((v) => fail('should not get here'))
+ .then(null, (v) => value = v);
+ flushMicrotasks();
+ expect(value).toEqual('rejectReason');
+ });
+ });
+
+ it('should output error to console if ignoreConsoleErrorUncaughtError is false',
+ (done) => {
+ Zone.current.fork({name: 'promise-error'}).run(() => {
+ (Zone as any)[Zone.__symbol__('ignoreConsoleErrorUncaughtError')] = false;
+ const originalConsoleError = console.error;
+ console.error = jasmine.createSpy('consoleErr');
+ const p = new Promise((resolve, reject) => { throw new Error('promise error'); });
+ setTimeout(() => {
+ expect(console.error).toHaveBeenCalled();
+ console.error = originalConsoleError;
+ done();
+ }, 10);
+ });
+ });
+
+ it('should not output error to console if ignoreConsoleErrorUncaughtError is true',
+ (done) => {
+ Zone.current.fork({name: 'promise-error'}).run(() => {
+ (Zone as any)[Zone.__symbol__('ignoreConsoleErrorUncaughtError')] = true;
+ const originalConsoleError = console.error;
+ console.error = jasmine.createSpy('consoleErr');
+ const p = new Promise((resolve, reject) => { throw new Error('promise error'); });
+ setTimeout(() => {
+ expect(console.error).not.toHaveBeenCalled();
+ console.error = originalConsoleError;
+ (Zone as any)[Zone.__symbol__('ignoreConsoleErrorUncaughtError')] = false;
+ done();
+ }, 10);
+ });
+ });
+
+ it('should notify Zone.onHandleError if no one catches promise', (done) => {
+ let promiseError: Error|null = null;
+ let zone: Zone|null = null;
+ let task: Task|null = null;
+ let error: Error|null = null;
+ queueZone
+ .fork({
+ name: 'promise-error',
+ onHandleError: (delegate: ZoneDelegate, current: Zone, target: Zone, error: any):
+ boolean => {
+ promiseError = error;
+ delegate.handleError(target, error);
+ return false;
+ }
+ })
+ .run(() => {
+ zone = Zone.current;
+ task = Zone.currentTask;
+ error = new Error('rejectedErrorShouldBeHandled');
+ try {
+ // throw so that the stack trace is captured
+ throw error;
+ } catch (e) {
+ }
+ Promise.reject(error);
+ expect(promiseError).toBe(null);
+ });
+ setTimeout((): any => null);
+ setTimeout(() => {
+ expect(promiseError !.message)
+ .toBe(
+ 'Uncaught (in promise): ' + error +
+ (error !.stack ? '\n' + error !.stack : ''));
+ expect((promiseError as any)['rejection']).toBe(error);
+ expect((promiseError as any)['zone']).toBe(zone);
+ expect((promiseError as any)['task']).toBe(task);
+ done();
+ });
+ });
+
+ it('should print readable information when throw a not error object', (done) => {
+ let promiseError: Error|null = null;
+ let zone: Zone|null = null;
+ let task: Task|null = null;
+ let rejectObj: TestRejection;
+ queueZone
+ .fork({
+ name: 'promise-error',
+ onHandleError: (delegate: ZoneDelegate, current: Zone, target: Zone, error: any):
+ boolean => {
+ promiseError = error;
+ delegate.handleError(target, error);
+ return false;
+ }
+ })
+ .run(() => {
+ zone = Zone.current;
+ task = Zone.currentTask;
+ rejectObj = new TestRejection();
+ rejectObj.prop1 = 'value1';
+ rejectObj.prop2 = 'value2';
+ Promise.reject(rejectObj);
+ expect(promiseError).toBe(null);
+ });
+ setTimeout((): any => null);
+ setTimeout(() => {
+ expect(promiseError !.message)
+ .toMatch(/Uncaught \(in promise\):.*: {"prop1":"value1","prop2":"value2"}/);
+ done();
+ });
+ });
+ });
+
+ describe('Promise.race', () => {
+ it('should reject the value', () => {
+ queueZone.run(() => {
+ let value: any = null;
+ (Promise as any).race([
+ Promise.reject('rejection1'), 'v1'
+ ])['catch']((v: any) => value = v);
+ // expect(Zone.current.get('queue').length).toEqual(2);
+ flushMicrotasks();
+ expect(value).toEqual('rejection1');
+ });
+ });
+
+ it('should resolve the value', () => {
+ queueZone.run(() => {
+ let value: any = null;
+ (Promise as any)
+ .race([Promise.resolve('resolution'), 'v1'])
+ .then((v: any) => value = v);
+ // expect(Zone.current.get('queue').length).toEqual(2);
+ flushMicrotasks();
+ expect(value).toEqual('resolution');
+ });
+ });
+ });
+
+ describe('Promise.all', () => {
+ it('should reject the value', () => {
+ queueZone.run(() => {
+ let value: any = null;
+ Promise.all([Promise.reject('rejection'), 'v1'])['catch']((v: any) => value = v);
+ // expect(Zone.current.get('queue').length).toEqual(2);
+ flushMicrotasks();
+ expect(value).toEqual('rejection');
+ });
+ });
+
+ it('should resolve the value', () => {
+ queueZone.run(() => {
+ let value: any = null;
+ Promise.all([Promise.resolve('resolution'), 'v1']).then((v: any) => value = v);
+ // expect(Zone.current.get('queue').length).toEqual(2);
+ flushMicrotasks();
+ expect(value).toEqual(['resolution', 'v1']);
+ });
+ });
+
+ it('should resolve with the sync then operation', () => {
+ queueZone.run(() => {
+ let value: any = null;
+ const p1 = {then: function(thenCallback: Function) { return thenCallback('p1'); }};
+ const p2 = {then: function(thenCallback: Function) { return thenCallback('p2'); }};
+ Promise.all([p1, 'v1', p2]).then((v: any) => value = v);
+ // expect(Zone.current.get('queue').length).toEqual(2);
+ flushMicrotasks();
+ expect(value).toEqual(['p1', 'v1', 'p2']);
+ });
+ });
+
+ it('should resolve generators',
+ ifEnvSupports(
+ () => { return isNode; },
+ () => {
+ const generators: any = function* () {
+ yield Promise.resolve(1);
+ yield Promise.resolve(2);
+ return;
+ };
+ queueZone.run(() => {
+ let value: any = null;
+ Promise.all(generators()).then(val => { value = val; });
+ // expect(Zone.current.get('queue').length).toEqual(2);
+ flushMicrotasks();
+ expect(value).toEqual([1, 2]);
+ });
+ }));
+ });
+ });
+
+ describe('Promise subclasses', function() {
+ class MyPromise {
+ private _promise: Promise;
+ constructor(init: any) { this._promise = new Promise(init); }
+
+ catch(onrejected?: ((reason: any) => TResult | PromiseLike)|
+ undefined|null): Promise {
+ return this._promise.catch.call(this._promise, onrejected);
+ };
+
+ then(
+ onfulfilled?: ((value: T) => TResult1 | PromiseLike)|undefined|null,
+ onrejected?: ((reason: any) => TResult2 | PromiseLike)|undefined|
+ null): Promise {
+ return this._promise.then.call(this._promise, onfulfilled, onrejected);
+ };
+ }
+
+ const setPrototypeOf = (Object as any).setPrototypeOf || function(obj: any, proto: any) {
+ obj.__proto__ = proto;
+ return obj;
+ };
+
+ setPrototypeOf(MyPromise.prototype, Promise.prototype);
+
+ it('should reject if the Promise subclass rejects', function() {
+ const myPromise =
+ new MyPromise(function(resolve: any, reject: any): void { reject('foo'); });
+
+ return Promise.resolve()
+ .then(function() { return myPromise; })
+ .then(
+ function() { throw new Error('Unexpected resolution'); },
+ function(result) { expect(result).toBe('foo'); });
+ });
+
+ function testPromiseSubClass(done?: Function) {
+ const myPromise =
+ new MyPromise(function(resolve: any, reject: Function) { resolve('foo'); });
+
+ return Promise.resolve().then(function() { return myPromise; }).then(function(result) {
+ expect(result).toBe('foo');
+ done && done();
+ });
+ }
+
+ it('should resolve if the Promise subclass resolves', jasmine ? function(done) {
+ testPromiseSubClass(done);
+ } : function() { testPromiseSubClass(); });
+ });
+ }));
diff --git a/packages/zone.js/test/common/fetch.spec.ts b/packages/zone.js/test/common/fetch.spec.ts
new file mode 100644
index 0000000000..8df75786cd
--- /dev/null
+++ b/packages/zone.js/test/common/fetch.spec.ts
@@ -0,0 +1,205 @@
+/**
+ * @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
+ */
+import {ifEnvSupports, ifEnvSupportsWithDone, isFirefox, isSafari} from '../test-util';
+
+declare const global: any;
+
+describe(
+ 'fetch', ifEnvSupports('fetch', function() {
+ let testZone: Zone;
+ beforeEach(() => { testZone = Zone.current.fork({name: 'TestZone'}); });
+ it('should work for text response', function(done) {
+ testZone.run(function() {
+ global['fetch']('/base/angular/packages/zone.js/test/assets/sample.json')
+ .then(function(response: any) {
+ const fetchZone = Zone.current;
+ expect(fetchZone.name).toBe(testZone.name);
+
+ response.text().then(function(text: string) {
+ expect(Zone.current.name).toBe(fetchZone.name);
+ expect(text.trim()).toEqual('{"hello": "world"}');
+ done();
+ });
+ });
+ });
+ });
+
+ it('should work for json response', function(done) {
+ testZone.run(function() {
+ global['fetch']('/base/angular/packages/zone.js/test/assets/sample.json')
+ .then(function(response: any) {
+ const fetchZone = Zone.current;
+ expect(fetchZone.name).toBe(testZone.name);
+
+ response.json().then(function(obj: any) {
+ expect(Zone.current.name).toBe(fetchZone.name);
+ expect(obj.hello).toEqual('world');
+ done();
+ });
+ });
+ });
+ });
+
+ it('should work for blob response', function(done) {
+ testZone.run(function() {
+ global['fetch']('/base/angular/packages/zone.js/test/assets/sample.json')
+ .then(function(response: any) {
+ const fetchZone = Zone.current;
+ expect(fetchZone.name).toBe(testZone.name);
+
+ // Android 4.3- doesn't support response.blob()
+ if (response.blob) {
+ response.blob().then(function(blob: any) {
+ expect(Zone.current.name).toBe(fetchZone.name);
+ expect(blob instanceof Blob).toEqual(true);
+ done();
+ });
+ } else {
+ done();
+ }
+ });
+ });
+ });
+
+ it('should work for arrayBuffer response', function(done) {
+ testZone.run(function() {
+ global['fetch']('/base/angular/packages/zone.js/test/assets/sample.json')
+ .then(function(response: any) {
+ const fetchZone = Zone.current;
+ expect(fetchZone.name).toBe(testZone.name);
+
+ // Android 4.3- doesn't support response.arrayBuffer()
+ if (response.arrayBuffer) {
+ response.arrayBuffer().then(function(blob: any) {
+ expect(Zone.current).toBe(fetchZone);
+ expect(blob instanceof ArrayBuffer).toEqual(true);
+ done();
+ });
+ } else {
+ done();
+ }
+ });
+ });
+ });
+
+ it('should throw error when send crendential',
+ ifEnvSupportsWithDone(isFirefox, function(done: DoneFn) {
+ testZone.run(function() {
+ global['fetch']('http://user:password@example.com')
+ .then(
+ function(response: any) { fail('should not success'); },
+ (error: any) => {
+ expect(Zone.current.name).toEqual(testZone.name);
+ expect(error.constructor.name).toEqual('TypeError');
+ done();
+ });
+ });
+ }));
+
+ describe('macroTask', () => {
+ const logs: string[] = [];
+ let fetchZone: Zone;
+ let fetchTask: any = null;
+ beforeEach(() => {
+ logs.splice(0);
+ fetchZone = Zone.current.fork({
+ name: 'fetch',
+ onScheduleTask: (delegate: ZoneDelegate, curr: Zone, target: Zone, task: Task) => {
+ if (task.type !== 'eventTask') {
+ logs.push(`scheduleTask:${task.source}:${task.type}`);
+ }
+ if (task.source === 'fetch') {
+ fetchTask = task;
+ }
+ return delegate.scheduleTask(target, task);
+ },
+ onInvokeTask: (delegate: ZoneDelegate, curr: Zone, target: Zone, task: Task,
+ applyThis: any, applyArgs: any) => {
+ if (task.type !== 'eventTask') {
+ logs.push(`invokeTask:${task.source}:${task.type}`);
+ }
+ return delegate.invokeTask(target, task, applyThis, applyArgs);
+ },
+ onCancelTask: (delegate: ZoneDelegate, curr: Zone, target: Zone, task: Task) => {
+ if (task.type !== 'eventTask') {
+ logs.push(`cancelTask:${task.source}:${task.type}`);
+ }
+ return delegate.cancelTask(target, task);
+ }
+ });
+ });
+ it('fetch should be considered as macroTask', (done: DoneFn) => {
+ fetchZone.run(() => {
+ global['fetch']('/base/angular/packages/zone.js/test/assets/sample.json')
+ .then(function(response: any) {
+ expect(Zone.current.name).toBe(fetchZone.name);
+ expect(logs).toEqual([
+ 'scheduleTask:fetch:macroTask', 'scheduleTask:Promise.then:microTask',
+ 'invokeTask:Promise.then:microTask', 'invokeTask:fetch:macroTask',
+ 'scheduleTask:Promise.then:microTask', 'invokeTask:Promise.then:microTask'
+ ]);
+ done();
+ });
+ });
+ });
+
+ it('cancel fetch should invoke onCancelTask',
+ ifEnvSupportsWithDone('AbortController', (done: DoneFn) => {
+ if (isSafari) {
+ // safari not work with AbortController
+ done();
+ return;
+ }
+ fetchZone.run(() => {
+ const AbortController = global['AbortController'];
+ const abort = new AbortController();
+ const signal = abort.signal;
+ global['fetch']('/base/angular/packages/zone.js/test/assets/sample.json', {signal})
+ .then(function(response: any) { fail('should not get response'); })
+ .catch(function(error: any) {
+ expect(error.name).toEqual('AbortError');
+ expect(logs).toEqual([
+ 'scheduleTask:fetch:macroTask', 'cancelTask:fetch:macroTask',
+ 'scheduleTask:Promise.then:microTask', 'invokeTask:Promise.then:microTask',
+ 'scheduleTask:Promise.then:microTask', 'invokeTask:Promise.then:microTask',
+ 'scheduleTask:Promise.then:microTask', 'invokeTask:Promise.then:microTask'
+ ]);
+ done();
+ });
+ abort.abort();
+ });
+ }));
+
+ it('cancel fetchTask should trigger abort',
+ ifEnvSupportsWithDone('AbortController', (done: DoneFn) => {
+ if (isSafari) {
+ // safari not work with AbortController
+ done();
+ return;
+ }
+ fetchZone.run(() => {
+ const AbortController = global['AbortController'];
+ const abort = new AbortController();
+ const signal = abort.signal;
+ global['fetch']('/base/angular/packages/zone.js/test/assets/sample.json', {signal})
+ .then(function(response: any) { fail('should not get response'); })
+ .catch(function(error: any) {
+ expect(error.name).toEqual('AbortError');
+ expect(logs).toEqual([
+ 'scheduleTask:fetch:macroTask', 'cancelTask:fetch:macroTask',
+ 'scheduleTask:Promise.then:microTask', 'invokeTask:Promise.then:microTask',
+ 'scheduleTask:Promise.then:microTask', 'invokeTask:Promise.then:microTask',
+ 'scheduleTask:Promise.then:microTask', 'invokeTask:Promise.then:microTask'
+ ]);
+ done();
+ });
+ fetchTask.zone.cancelTask(fetchTask);
+ });
+ }));
+ });
+ }));
diff --git a/packages/zone.js/test/common/microtasks.spec.ts b/packages/zone.js/test/common/microtasks.spec.ts
new file mode 100644
index 0000000000..2e70c78aad
--- /dev/null
+++ b/packages/zone.js/test/common/microtasks.spec.ts
@@ -0,0 +1,100 @@
+/**
+ * @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
+ */
+
+describe('Microtasks', function() {
+ if (!global.Promise) return;
+
+ function scheduleFn(task: Task) { Promise.resolve().then(task.invoke); }
+
+ it('should execute microtasks enqueued in the root zone', function(done) {
+ const log: number[] = [];
+
+ Zone.current.scheduleMicroTask('test', () => log.push(1), undefined, scheduleFn);
+ Zone.current.scheduleMicroTask('test', () => log.push(2), undefined, scheduleFn);
+ Zone.current.scheduleMicroTask('test', () => log.push(3), undefined, scheduleFn);
+
+ setTimeout(function() {
+ expect(log).toEqual([1, 2, 3]);
+ done();
+ }, 10);
+ });
+
+ it('should correctly scheduleMacroTask microtasks vs macrotasks', function(done) {
+ const log = ['+root'];
+
+ Zone.current.scheduleMicroTask('test', () => log.push('root.mit'), undefined, scheduleFn);
+
+ setTimeout(function() {
+ log.push('+mat1');
+ Zone.current.scheduleMicroTask('test', () => log.push('mat1.mit'), undefined, scheduleFn);
+ log.push('-mat1');
+ }, 10);
+
+ setTimeout(function() { log.push('mat2'); }, 30);
+
+ setTimeout(function() {
+ expect(log).toEqual(['+root', '-root', 'root.mit', '+mat1', '-mat1', 'mat1.mit', 'mat2']);
+ done();
+ }, 40);
+
+ log.push('-root');
+ });
+
+ it('should execute Promise wrapCallback in the zone where they are scheduled', function(done) {
+ const resolvedPromise = Promise.resolve(null);
+
+ const testZone = Zone.current.fork({name: ''});
+
+ testZone.run(function() {
+ resolvedPromise.then(function() {
+ expect(Zone.current.name).toBe(testZone.name);
+ done();
+ });
+ });
+ });
+
+ it('should execute Promise wrapCallback in the zone where they are scheduled even if resolved ' +
+ 'in different zone.',
+ function(done) {
+ let resolve: Function;
+ const promise = new Promise(function(rs) { resolve = rs; });
+
+ const testZone = Zone.current.fork({name: 'test'});
+
+ testZone.run(function() {
+ promise.then(function() {
+ expect(Zone.current).toBe(testZone);
+ done();
+ });
+ });
+
+ Zone.current.fork({name: 'test'}).run(function() { resolve(null); });
+ });
+
+ describe('Promise', function() {
+ it('should go through scheduleTask', function(done) {
+ let called = false;
+ const testZone = Zone.current.fork({
+ name: 'test',
+ onScheduleTask: function(delegate: ZoneDelegate, current: Zone, target: Zone, task: Task):
+ Task {
+ called = true;
+ delegate.scheduleTask(target, task);
+ return task;
+ }
+ });
+
+ testZone.run(function() {
+ Promise.resolve('value').then(function() {
+ expect(called).toEqual(true);
+ done();
+ });
+ });
+ });
+ });
+});
diff --git a/packages/zone.js/test/common/setInterval.spec.ts b/packages/zone.js/test/common/setInterval.spec.ts
new file mode 100644
index 0000000000..cee6d8eabc
--- /dev/null
+++ b/packages/zone.js/test/common/setInterval.spec.ts
@@ -0,0 +1,89 @@
+/**
+ * @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
+ */
+
+'use strict';
+import {isNode, zoneSymbol} from '../../lib/common/utils';
+declare const global: any;
+const wtfMock = global.wtfMock;
+
+describe('setInterval', function() {
+ it('should work with setInterval', function(done) {
+ let cancelId: any;
+ const testZone = Zone.current.fork((Zone as any)['wtfZoneSpec']).fork({name: 'TestZone'});
+ testZone.run(() => {
+ let intervalCount = 0;
+ let timeoutRunning = false;
+ const intervalFn = function() {
+ intervalCount++;
+ expect(Zone.current.name).toEqual(('TestZone'));
+ if (timeoutRunning) {
+ return;
+ }
+ timeoutRunning = true;
+ global[zoneSymbol('setTimeout')](function() {
+ const intervalUnitLog = [
+ '> Zone:invokeTask:setInterval("::ProxyZone::WTF::TestZone")',
+ '< Zone:invokeTask:setInterval'
+ ];
+ let intervalLog: string[] = [];
+ for (let i = 0; i < intervalCount; i++) {
+ intervalLog = intervalLog.concat(intervalUnitLog);
+ }
+ expect(wtfMock.log[0]).toEqual('# Zone:fork("::ProxyZone::WTF", "TestZone")');
+ expect(wtfMock.log[1])
+ .toEqual('> Zone:invoke:unit-test("::ProxyZone::WTF::TestZone")');
+ expect(wtfMock.log[2])
+ .toContain(
+ '# Zone:schedule:macroTask:setInterval("::ProxyZone::WTF::TestZone"');
+ expect(wtfMock.log[3]).toEqual('< Zone:invoke:unit-test');
+ expect(wtfMock.log.splice(4)).toEqual(intervalLog);
+ clearInterval(cancelId);
+ done();
+ });
+ };
+ expect(Zone.current.name).toEqual(('TestZone'));
+ cancelId = setInterval(intervalFn, 10);
+ if (isNode) {
+ expect(typeof cancelId.ref).toEqual(('function'));
+ expect(typeof cancelId.unref).toEqual(('function'));
+ }
+
+ expect(wtfMock.log[0]).toEqual('# Zone:fork("::ProxyZone::WTF", "TestZone")');
+ expect(wtfMock.log[1]).toEqual('> Zone:invoke:unit-test("::ProxyZone::WTF::TestZone")');
+ expect(wtfMock.log[2])
+ .toContain('# Zone:schedule:macroTask:setInterval("::ProxyZone::WTF::TestZone"');
+ }, null, undefined, 'unit-test');
+ });
+
+ it('should not cancel the task after invoke the setInterval callback', (done) => {
+ const logs: HasTaskState[] = [];
+ const zone = Zone.current.fork({
+ name: 'interval',
+ onHasTask:
+ (delegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, hasTask: HasTaskState) => {
+ logs.push(hasTask);
+ return delegate.hasTask(targetZone, hasTask);
+ }
+ });
+
+ zone.run(() => {
+ const timerId = setInterval(() => {}, 100);
+ (global as any)[Zone.__symbol__('setTimeout')](() => {
+ expect(logs.length > 0).toBeTruthy();
+ expect(logs).toEqual(
+ [{microTask: false, macroTask: true, eventTask: false, change: 'macroTask'}]);
+ clearInterval(timerId);
+ expect(logs).toEqual([
+ {microTask: false, macroTask: true, eventTask: false, change: 'macroTask'},
+ {microTask: false, macroTask: false, eventTask: false, change: 'macroTask'}
+ ]);
+ done();
+ }, 300);
+ });
+ });
+});
diff --git a/packages/zone.js/test/common/setTimeout.spec.ts b/packages/zone.js/test/common/setTimeout.spec.ts
new file mode 100644
index 0000000000..8c1f7c6789
--- /dev/null
+++ b/packages/zone.js/test/common/setTimeout.spec.ts
@@ -0,0 +1,123 @@
+/**
+ * @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
+ */
+
+import {isNode, zoneSymbol} from '../../lib/common/utils';
+declare const global: any;
+const wtfMock = global.wtfMock;
+
+describe('setTimeout', function() {
+ it('should intercept setTimeout', function(done) {
+ let cancelId: any;
+ const testZone = Zone.current.fork((Zone as any)['wtfZoneSpec']).fork({name: 'TestZone'});
+ testZone.run(() => {
+ const timeoutFn = function() {
+ expect(Zone.current.name).toEqual(('TestZone'));
+ global[zoneSymbol('setTimeout')](function() {
+ expect(wtfMock.log[0]).toEqual('# Zone:fork("::ProxyZone::WTF", "TestZone")');
+ expect(wtfMock.log[1])
+ .toEqual('> Zone:invoke:unit-test("::ProxyZone::WTF::TestZone")');
+ expect(wtfMock.log[2])
+ .toContain('# Zone:schedule:macroTask:setTimeout("::ProxyZone::WTF::TestZone"');
+ expect(wtfMock.log[3]).toEqual('< Zone:invoke:unit-test');
+ expect(wtfMock.log[4])
+ .toEqual('> Zone:invokeTask:setTimeout("::ProxyZone::WTF::TestZone")');
+ expect(wtfMock.log[5]).toEqual('< Zone:invokeTask:setTimeout');
+ done();
+ });
+ };
+ expect(Zone.current.name).toEqual(('TestZone'));
+ cancelId = setTimeout(timeoutFn, 3);
+ if (isNode) {
+ expect(typeof cancelId.ref).toEqual(('function'));
+ expect(typeof cancelId.unref).toEqual(('function'));
+ }
+ expect(wtfMock.log[0]).toEqual('# Zone:fork("::ProxyZone::WTF", "TestZone")');
+ expect(wtfMock.log[1]).toEqual('> Zone:invoke:unit-test("::ProxyZone::WTF::TestZone")');
+ expect(wtfMock.log[2])
+ .toContain('# Zone:schedule:macroTask:setTimeout("::ProxyZone::WTF::TestZone"');
+ }, null, undefined, 'unit-test');
+ });
+
+ it('should allow canceling of fns registered with setTimeout', function(done) {
+ const testZone = Zone.current.fork((Zone as any)['wtfZoneSpec']).fork({name: 'TestZone'});
+ testZone.run(() => {
+ const spy = jasmine.createSpy('spy');
+ const cancelId = setTimeout(spy, 0);
+ clearTimeout(cancelId);
+ setTimeout(function() {
+ expect(spy).not.toHaveBeenCalled();
+ done();
+ }, 1);
+ });
+ });
+
+ it('should allow cancelation of fns registered with setTimeout after invocation', function(done) {
+ const testZone = Zone.current.fork((Zone as any)['wtfZoneSpec']).fork({name: 'TestZone'});
+ testZone.run(() => {
+ const spy = jasmine.createSpy('spy');
+ const cancelId = setTimeout(spy, 0);
+ setTimeout(function() {
+ expect(spy).toHaveBeenCalled();
+ setTimeout(function() {
+ clearTimeout(cancelId);
+ done();
+ });
+ }, 1);
+ });
+ });
+
+ it('should allow cancelation of fns while the task is being executed', function(done) {
+ const spy = jasmine.createSpy('spy');
+ const cancelId = setTimeout(() => {
+ clearTimeout(cancelId);
+ done();
+ }, 0);
+ });
+
+ it('should allow cancelation of fns registered with setTimeout during invocation',
+ function(done) {
+ const testZone = Zone.current.fork((Zone as any)['wtfZoneSpec']).fork({name: 'TestZone'});
+ testZone.run(() => {
+ const cancelId = setTimeout(function() {
+ clearTimeout(cancelId);
+ done();
+ }, 0);
+ });
+ });
+
+ it('should return the original timeout Id', function() {
+ // Node returns complex object from setTimeout, ignore this test.
+ if (isNode) return;
+ const cancelId = setTimeout(() => {}, 0);
+ expect(typeof cancelId).toEqual('number');
+ });
+
+ it('should allow cancelation by numeric timeout Id', function(done) {
+ // Node returns complex object from setTimeout, ignore this test.
+ if (isNode) {
+ done();
+ return;
+ }
+
+ const testZone = Zone.current.fork((Zone as any)['wtfZoneSpec']).fork({name: 'TestZone'});
+ testZone.run(() => {
+ const spy = jasmine.createSpy('spy');
+ const cancelId = setTimeout(spy, 0);
+ clearTimeout(cancelId);
+ setTimeout(function() {
+ expect(spy).not.toHaveBeenCalled();
+ done();
+ }, 1);
+ });
+ });
+
+ it('should pass invalid values through', function() {
+ clearTimeout(null as any);
+ clearTimeout({});
+ });
+});
diff --git a/packages/zone.js/test/common/task.spec.ts b/packages/zone.js/test/common/task.spec.ts
new file mode 100644
index 0000000000..a37365cfbc
--- /dev/null
+++ b/packages/zone.js/test/common/task.spec.ts
@@ -0,0 +1,965 @@
+/**
+ * @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 noop = function() {};
+let log: {zone: string, taskZone: undefined | string, toState: TaskState, fromState: TaskState}[] =
+ [];
+const detectTask = Zone.current.scheduleMacroTask('detectTask', noop, undefined, noop, noop);
+const originalTransitionTo = detectTask.constructor.prototype._transitionTo;
+// patch _transitionTo of ZoneTask to add log for test
+const logTransitionTo: Function = function(
+ toState: TaskState, fromState1: TaskState, fromState2?: TaskState) {
+ log.push({
+ zone: Zone.current.name,
+ taskZone: this.zone && this.zone.name,
+ toState: toState,
+ fromState: this._state
+ });
+ originalTransitionTo.apply(this, arguments);
+};
+
+function testFnWithLoggedTransitionTo(testFn: Function) {
+ return function() {
+ detectTask.constructor.prototype._transitionTo = logTransitionTo;
+ testFn.apply(this, arguments);
+ detectTask.constructor.prototype._transitionTo = originalTransitionTo;
+ };
+}
+
+describe('task lifecycle', () => {
+ describe('event task lifecycle', () => {
+ beforeEach(() => { log = []; });
+
+ it('task should transit from notScheduled to scheduling then to scheduled state when scheduleTask',
+ testFnWithLoggedTransitionTo(() => {
+ Zone.current.fork({name: 'testEventTaskZone'}).run(() => {
+ Zone.current.scheduleEventTask('testEventTask', noop, undefined, noop, noop);
+ });
+ expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; }))
+ .toEqual([
+ {toState: 'scheduling', fromState: 'notScheduled'},
+ {toState: 'scheduled', fromState: 'scheduling'}
+ ]);
+ }));
+
+ it('task should transit from scheduling to unknown when zoneSpec onScheduleTask callback throw error',
+ testFnWithLoggedTransitionTo(() => {
+ Zone.current
+ .fork({
+ name: 'testEventTaskZone',
+ onScheduleTask: (delegate, currZone, targetZone, task) => {
+ throw Error('error in onScheduleTask');
+ }
+ })
+ .run(() => {
+ try {
+ Zone.current.scheduleEventTask('testEventTask', noop, undefined, noop, noop);
+ } catch (err) {
+ }
+ });
+ expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; }))
+ .toEqual([
+ {toState: 'scheduling', fromState: 'notScheduled'},
+ {toState: 'unknown', fromState: 'scheduling'}
+ ]);
+ }));
+
+ it('task should transit from scheduled to running when task is invoked then from running to scheduled after invoke',
+ testFnWithLoggedTransitionTo(() => {
+ Zone.current.fork({name: 'testEventTaskZone'}).run(() => {
+ const task =
+ Zone.current.scheduleEventTask('testEventTask', noop, undefined, noop, noop);
+ task.invoke();
+ });
+ expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; }))
+ .toEqual([
+ {toState: 'scheduling', fromState: 'notScheduled'},
+ {toState: 'scheduled', fromState: 'scheduling'},
+ {toState: 'running', fromState: 'scheduled'},
+ {toState: 'scheduled', fromState: 'running'}
+ ]);
+ }));
+
+ it('task should transit from scheduled to canceling then from canceling to notScheduled when task is canceled before running',
+ testFnWithLoggedTransitionTo(() => {
+ Zone.current.fork({name: 'testEventTaskZone'}).run(() => {
+ const task =
+ Zone.current.scheduleEventTask('testEventTask', noop, undefined, noop, noop);
+ Zone.current.cancelTask(task);
+ });
+ expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; }))
+ .toEqual([
+ {toState: 'scheduling', fromState: 'notScheduled'},
+ {toState: 'scheduled', fromState: 'scheduling'},
+ {toState: 'canceling', fromState: 'scheduled'},
+ {toState: 'notScheduled', fromState: 'canceling'}
+ ]);
+ }));
+
+ it('task should transit from running to canceling then from canceling to notScheduled when task is canceled in running state',
+ testFnWithLoggedTransitionTo(() => {
+ Zone.current.fork({name: 'testEventTaskZone'}).run(() => {
+ const task = Zone.current.scheduleEventTask(
+ 'testEventTask', () => { Zone.current.cancelTask(task); }, undefined, noop, noop);
+ task.invoke();
+ });
+ expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; }))
+ .toEqual([
+ {toState: 'scheduling', fromState: 'notScheduled'},
+ {toState: 'scheduled', fromState: 'scheduling'},
+ {toState: 'running', fromState: 'scheduled'},
+ {toState: 'canceling', fromState: 'running'},
+ {toState: 'notScheduled', fromState: 'canceling'}
+ ]);
+ }));
+
+ it('task should transit from running to scheduled when task.callback throw error',
+ testFnWithLoggedTransitionTo(() => {
+ Zone.current.fork({name: 'testEventTaskZone'}).run(() => {
+ const task = Zone.current.scheduleEventTask(
+ 'testEventTask', () => { throw Error('invoke error'); }, undefined, noop, noop);
+ try {
+ task.invoke();
+ } catch (err) {
+ }
+ });
+ expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; }))
+ .toEqual([
+ {toState: 'scheduling', fromState: 'notScheduled'},
+ {toState: 'scheduled', fromState: 'scheduling'},
+ {toState: 'running', fromState: 'scheduled'},
+ {toState: 'scheduled', fromState: 'running'}
+ ]);
+ }));
+
+ it('task should transit from canceling to unknown when zoneSpec.onCancelTask throw error before task running',
+ testFnWithLoggedTransitionTo(() => {
+ Zone.current.fork({name: 'testEventTaskZone'}).run(() => {
+ const task = Zone.current.scheduleEventTask(
+ 'testEventTask', noop, undefined, noop, () => { throw Error('cancel task'); });
+ try {
+ Zone.current.cancelTask(task);
+ } catch (err) {
+ }
+ });
+ expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; }))
+ .toEqual([
+ {toState: 'scheduling', fromState: 'notScheduled'},
+ {toState: 'scheduled', fromState: 'scheduling'},
+ {toState: 'canceling', fromState: 'scheduled'},
+ {toState: 'unknown', fromState: 'canceling'}
+ ]);
+ }));
+
+ it('task should transit from canceling to unknown when zoneSpec.onCancelTask throw error in running state',
+ testFnWithLoggedTransitionTo(() => {
+ Zone.current.fork({name: 'testEventTaskZone'}).run(() => {
+ const task = Zone.current.scheduleEventTask(
+ 'testEventTask', noop, undefined, noop, () => { throw Error('cancel task'); });
+ try {
+ Zone.current.cancelTask(task);
+ } catch (err) {
+ }
+ });
+ expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; }))
+ .toEqual([
+ {toState: 'scheduling', fromState: 'notScheduled'},
+ {toState: 'scheduled', fromState: 'scheduling'},
+ {toState: 'canceling', fromState: 'scheduled'},
+ {toState: 'unknown', fromState: 'canceling'}
+ ]);
+ }));
+
+ it('task should transit from notScheduled to scheduled if zoneSpec.onHasTask throw error when scheduleTask',
+ testFnWithLoggedTransitionTo(() => {
+ Zone.current
+ .fork({
+ name: 'testEventTaskZone',
+ onHasTask: (delegate, currZone, targetZone, hasTaskState) => {
+ throw Error('hasTask Error');
+ }
+ })
+ .run(() => {
+ try {
+ Zone.current.scheduleEventTask('testEventTask', noop, undefined, noop, noop);
+ } catch (err) {
+ }
+ });
+ expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; }))
+ .toEqual([
+ {toState: 'scheduling', fromState: 'notScheduled'},
+ {toState: 'scheduled', fromState: 'scheduling'}
+ ]);
+ }));
+
+ it('task should transit to notScheduled state if zoneSpec.onHasTask throw error when task is canceled',
+ testFnWithLoggedTransitionTo(() => {
+ let task: Task;
+ Zone.current
+ .fork({
+ name: 'testEventTaskZone',
+ onHasTask: (delegate, currZone, targetZone, hasTaskState) => {
+ if (task && task.state === 'canceling') {
+ throw Error('hasTask Error');
+ }
+ }
+ })
+ .run(() => {
+ try {
+ task =
+ Zone.current.scheduleEventTask('testEventTask', noop, undefined, noop, noop);
+ Zone.current.cancelTask(task);
+ } catch (err) {
+ }
+ });
+ expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; }))
+ .toEqual([
+ {toState: 'scheduling', fromState: 'notScheduled'},
+ {toState: 'scheduled', fromState: 'scheduling'},
+ {toState: 'canceling', fromState: 'scheduled'},
+ {toState: 'notScheduled', fromState: 'canceling'}
+ ]);
+ }));
+ });
+
+ describe('non periodical macroTask lifecycle', () => {
+ beforeEach(() => { log = []; });
+
+ it('task should transit from notScheduled to scheduling then to scheduled state when scheduleTask',
+ testFnWithLoggedTransitionTo(() => {
+ Zone.current.fork({name: 'testMacroTaskZone'}).run(() => {
+ Zone.current.scheduleMacroTask('testMacroTask', noop, undefined, noop, noop);
+ });
+ expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; }))
+ .toEqual([
+ {toState: 'scheduling', fromState: 'notScheduled'},
+ {toState: 'scheduled', fromState: 'scheduling'}
+ ]);
+ }));
+
+ it('task should transit from scheduling to unknown when zoneSpec onScheduleTask callback throw error',
+ testFnWithLoggedTransitionTo(() => {
+ Zone.current
+ .fork({
+ name: 'testMacroTaskZone',
+ onScheduleTask: (delegate, currZone, targetZone, task) => {
+ throw Error('error in onScheduleTask');
+ }
+ })
+ .run(() => {
+ try {
+ Zone.current.scheduleMacroTask('testMacroTask', noop, undefined, noop, noop);
+ } catch (err) {
+ }
+ });
+ expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; }))
+ .toEqual([
+ {toState: 'scheduling', fromState: 'notScheduled'},
+ {toState: 'unknown', fromState: 'scheduling'}
+ ]);
+ }));
+
+ it('task should transit from scheduled to running when task is invoked then from running to noScheduled after invoke',
+ testFnWithLoggedTransitionTo(() => {
+ Zone.current.fork({name: 'testMacroTaskZone'}).run(() => {
+ const task =
+ Zone.current.scheduleMacroTask('testMacroTask', noop, undefined, noop, noop);
+ task.invoke();
+ });
+ expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; }))
+ .toEqual([
+ {toState: 'scheduling', fromState: 'notScheduled'},
+ {toState: 'scheduled', fromState: 'scheduling'},
+ {toState: 'running', fromState: 'scheduled'},
+ {toState: 'notScheduled', fromState: 'running'}
+ ]);
+ }));
+
+ it('task should transit from scheduled to canceling then from canceling to notScheduled when task is canceled before running',
+ testFnWithLoggedTransitionTo(() => {
+ Zone.current.fork({name: 'testMacroTaskZone'}).run(() => {
+ const task =
+ Zone.current.scheduleMacroTask('testMacrotask', noop, undefined, noop, noop);
+ Zone.current.cancelTask(task);
+ });
+ expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; }))
+ .toEqual([
+ {toState: 'scheduling', fromState: 'notScheduled'},
+ {toState: 'scheduled', fromState: 'scheduling'},
+ {toState: 'canceling', fromState: 'scheduled'},
+ {toState: 'notScheduled', fromState: 'canceling'}
+ ]);
+ }));
+
+ it('task should transit from running to canceling then from canceling to notScheduled when task is canceled in running state',
+ testFnWithLoggedTransitionTo(() => {
+ Zone.current.fork({name: 'testMacroTaskZone'}).run(() => {
+ const task = Zone.current.scheduleMacroTask(
+ 'testMacroTask', () => { Zone.current.cancelTask(task); }, undefined, noop, noop);
+ task.invoke();
+ });
+ expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; }))
+ .toEqual([
+ {toState: 'scheduling', fromState: 'notScheduled'},
+ {toState: 'scheduled', fromState: 'scheduling'},
+ {toState: 'running', fromState: 'scheduled'},
+ {toState: 'canceling', fromState: 'running'},
+ {toState: 'notScheduled', fromState: 'canceling'}
+ ]);
+ }));
+
+ it('task should transit from running to noScheduled when task.callback throw error',
+ testFnWithLoggedTransitionTo(() => {
+ Zone.current.fork({name: 'testMacroTaskZone'}).run(() => {
+ const task = Zone.current.scheduleMacroTask(
+ 'testMacroTask', () => { throw Error('invoke error'); }, undefined, noop, noop);
+ try {
+ task.invoke();
+ } catch (err) {
+ }
+ });
+ expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; }))
+ .toEqual([
+ {toState: 'scheduling', fromState: 'notScheduled'},
+ {toState: 'scheduled', fromState: 'scheduling'},
+ {toState: 'running', fromState: 'scheduled'},
+ {toState: 'notScheduled', fromState: 'running'}
+ ]);
+ }));
+
+ it('task should transit from canceling to unknown when zoneSpec.onCancelTask throw error before task running',
+ testFnWithLoggedTransitionTo(() => {
+ Zone.current.fork({name: 'testMacroTaskZone'}).run(() => {
+ const task = Zone.current.scheduleMacroTask(
+ 'testMacroTask', noop, undefined, noop, () => { throw Error('cancel task'); });
+ try {
+ Zone.current.cancelTask(task);
+ } catch (err) {
+ }
+ });
+ expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; }))
+ .toEqual([
+ {toState: 'scheduling', fromState: 'notScheduled'},
+ {toState: 'scheduled', fromState: 'scheduling'},
+ {toState: 'canceling', fromState: 'scheduled'},
+ {toState: 'unknown', fromState: 'canceling'}
+ ]);
+ }));
+
+ it('task should transit from canceling to unknown when zoneSpec.onCancelTask throw error in running state',
+ testFnWithLoggedTransitionTo(() => {
+ Zone.current.fork({name: 'testMacroTaskZone'}).run(() => {
+ const task = Zone.current.scheduleMacroTask(
+ 'testMacroTask', noop, undefined, noop, () => { throw Error('cancel task'); });
+ try {
+ Zone.current.cancelTask(task);
+ } catch (err) {
+ }
+ });
+ expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; }))
+ .toEqual([
+ {toState: 'scheduling', fromState: 'notScheduled'},
+ {toState: 'scheduled', fromState: 'scheduling'},
+ {toState: 'canceling', fromState: 'scheduled'},
+ {toState: 'unknown', fromState: 'canceling'}
+ ]);
+ }));
+
+ it('task should transit from notScheduled to scheduling then to scheduled if zoneSpec.onHasTask throw error when scheduleTask',
+ testFnWithLoggedTransitionTo(() => {
+ Zone.current
+ .fork({
+ name: 'testMacroTaskZone',
+ onHasTask: (delegate, currZone, targetZone, hasTaskState) => {
+ throw Error('hasTask Error');
+ }
+ })
+ .run(() => {
+ try {
+ Zone.current.scheduleMacroTask('testMacroTask', noop, undefined, noop, noop);
+ } catch (err) {
+ }
+ });
+ expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; }))
+ .toEqual([
+ {toState: 'scheduling', fromState: 'notScheduled'},
+ {toState: 'scheduled', fromState: 'scheduling'}
+ ]);
+ }));
+
+ it('task should transit to notScheduled state if zoneSpec.onHasTask throw error after task.callback being invoked',
+ testFnWithLoggedTransitionTo(() => {
+ let task: Task;
+ Zone.current
+ .fork({
+ name: 'testMacroTaskZone',
+ onHasTask: (delegate, currZone, targetZone, hasTaskState) => {
+ if (task && task.state === 'running') {
+ throw Error('hasTask Error');
+ }
+ }
+ })
+ .run(() => {
+ try {
+ task =
+ Zone.current.scheduleMacroTask('testMacroTask', noop, undefined, noop, noop);
+ task.invoke();
+ } catch (err) {
+ }
+ });
+ expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; }))
+ .toEqual([
+ {toState: 'scheduling', fromState: 'notScheduled'},
+ {toState: 'scheduled', fromState: 'scheduling'},
+ {toState: 'running', fromState: 'scheduled'},
+ {toState: 'notScheduled', fromState: 'running'}
+ ]);
+ }));
+
+ it('task should transit to notScheduled state if zoneSpec.onHasTask throw error when task is canceled before running',
+ testFnWithLoggedTransitionTo(() => {
+ let task: Task;
+ Zone.current
+ .fork({
+ name: 'testMacroTaskZone',
+ onHasTask: (delegate, currZone, targetZone, hasTaskState) => {
+ if (task && task.state === 'canceling') {
+ throw Error('hasTask Error');
+ }
+ }
+ })
+ .run(() => {
+ try {
+ task =
+ Zone.current.scheduleMacroTask('testMacroTask', noop, undefined, noop, noop);
+ Zone.current.cancelTask(task);
+ } catch (err) {
+ }
+ });
+ expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; }))
+ .toEqual([
+ {toState: 'scheduling', fromState: 'notScheduled'},
+ {toState: 'scheduled', fromState: 'scheduling'},
+ {toState: 'canceling', fromState: 'scheduled'},
+ {toState: 'notScheduled', fromState: 'canceling'}
+ ]);
+ }));
+ });
+
+ describe('periodical macroTask lifecycle', () => {
+ let task: Task|null;
+ beforeEach(() => {
+ log = [];
+ task = null;
+ });
+ afterEach(() => {
+ task && task.state !== 'notScheduled' && task.state !== 'canceling' &&
+ task.state !== 'unknown' && task.zone.cancelTask(task);
+ });
+
+ it('task should transit from notScheduled to scheduling then to scheduled state when scheduleTask',
+ testFnWithLoggedTransitionTo(() => {
+ Zone.current.fork({name: 'testPeriodicalTaskZone'}).run(() => {
+ task = Zone.current.scheduleMacroTask(
+ 'testPeriodicalTask', noop, {isPeriodic: true}, noop, noop);
+ });
+ expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; }))
+ .toEqual([
+ {toState: 'scheduling', fromState: 'notScheduled'},
+ {toState: 'scheduled', fromState: 'scheduling'}
+ ]);
+ }));
+
+ it('task should transit from scheduling to unknown when zoneSpec onScheduleTask callback throw error',
+ testFnWithLoggedTransitionTo(() => {
+ Zone.current
+ .fork({
+ name: 'testPeriodicalTaskZone',
+ onScheduleTask: (delegate, currZone, targetZone, task) => {
+ throw Error('error in onScheduleTask');
+ }
+ })
+ .run(() => {
+ try {
+ task = Zone.current.scheduleMacroTask(
+ 'testPeriodicalTask', noop, {isPeriodic: true}, noop, noop);
+ } catch (err) {
+ }
+ });
+ expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; }))
+ .toEqual([
+ {toState: 'scheduling', fromState: 'notScheduled'},
+ {toState: 'unknown', fromState: 'scheduling'}
+ ]);
+ }));
+
+ it('task should transit from scheduled to running when task is invoked then from running to scheduled after invoke',
+ testFnWithLoggedTransitionTo(() => {
+ Zone.current.fork({name: 'testPeriodicalTaskZone'}).run(() => {
+ task = Zone.current.scheduleMacroTask(
+ 'testPeriodicalTask', noop, {isPeriodic: true}, noop, noop);
+ task.invoke();
+ });
+ expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; }))
+ .toEqual([
+ {toState: 'scheduling', fromState: 'notScheduled'},
+ {toState: 'scheduled', fromState: 'scheduling'},
+ {toState: 'running', fromState: 'scheduled'},
+ {toState: 'scheduled', fromState: 'running'}
+ ]);
+ }));
+
+ it('task should transit from scheduled to canceling then from canceling to notScheduled when task is canceled before running',
+ testFnWithLoggedTransitionTo(() => {
+ Zone.current.fork({name: 'testPeriodicalTaskZone'}).run(() => {
+ task = Zone.current.scheduleMacroTask(
+ 'testPeriodicalTask', noop, {isPeriodic: true}, noop, noop);
+ Zone.current.cancelTask(task);
+ });
+ expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; }))
+ .toEqual([
+ {toState: 'scheduling', fromState: 'notScheduled'},
+ {toState: 'scheduled', fromState: 'scheduling'},
+ {toState: 'canceling', fromState: 'scheduled'},
+ {toState: 'notScheduled', fromState: 'canceling'}
+ ]);
+ }));
+
+ it('task should transit from running to canceling then from canceling to notScheduled when task is canceled in running state',
+ testFnWithLoggedTransitionTo(() => {
+ Zone.current.fork({name: 'testPeriodicalTaskZone'}).run(() => {
+ task = Zone.current.scheduleMacroTask('testPeriodicalTask', () => {
+ Zone.current.cancelTask(task !);
+ }, {isPeriodic: true}, noop, noop);
+ task.invoke();
+ });
+ expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; }))
+ .toEqual([
+ {toState: 'scheduling', fromState: 'notScheduled'},
+ {toState: 'scheduled', fromState: 'scheduling'},
+ {toState: 'running', fromState: 'scheduled'},
+ {toState: 'canceling', fromState: 'running'},
+ {toState: 'notScheduled', fromState: 'canceling'}
+ ]);
+ }));
+
+ it('task should transit from running to scheduled when task.callback throw error',
+ testFnWithLoggedTransitionTo(() => {
+ Zone.current.fork({name: 'testPeriodicalTaskZone'}).run(() => {
+ task = Zone.current.scheduleMacroTask('testPeriodicalTask', () => {
+ throw Error('invoke error');
+ }, {isPeriodic: true}, noop, noop);
+ try {
+ task.invoke();
+ } catch (err) {
+ }
+ });
+ expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; }))
+ .toEqual([
+ {toState: 'scheduling', fromState: 'notScheduled'},
+ {toState: 'scheduled', fromState: 'scheduling'},
+ {toState: 'running', fromState: 'scheduled'},
+ {toState: 'scheduled', fromState: 'running'}
+ ]);
+ }));
+
+ it('task should transit from canceling to unknown when zoneSpec.onCancelTask throw error before task running',
+ testFnWithLoggedTransitionTo(() => {
+ Zone.current.fork({name: 'testPeriodicalTaskZone'}).run(() => {
+ task = Zone.current.scheduleMacroTask(
+ 'testPeriodicalTask', noop, {isPeriodic: true}, noop,
+ () => { throw Error('cancel task'); });
+ try {
+ Zone.current.cancelTask(task);
+ } catch (err) {
+ }
+ });
+ expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; }))
+ .toEqual([
+ {toState: 'scheduling', fromState: 'notScheduled'},
+ {toState: 'scheduled', fromState: 'scheduling'},
+ {toState: 'canceling', fromState: 'scheduled'},
+ {toState: 'unknown', fromState: 'canceling'}
+ ]);
+ }));
+
+ it('task should transit from canceling to unknown when zoneSpec.onCancelTask throw error in running state',
+ testFnWithLoggedTransitionTo(() => {
+ Zone.current.fork({name: 'testPeriodicalTaskZone'}).run(() => {
+ task = Zone.current.scheduleMacroTask(
+ 'testPeriodicalTask', noop, {isPeriodic: true}, noop,
+ () => { throw Error('cancel task'); });
+ try {
+ Zone.current.cancelTask(task);
+ } catch (err) {
+ }
+ });
+ expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; }))
+ .toEqual([
+ {toState: 'scheduling', fromState: 'notScheduled'},
+ {toState: 'scheduled', fromState: 'scheduling'},
+ {toState: 'canceling', fromState: 'scheduled'},
+ {toState: 'unknown', fromState: 'canceling'}
+ ]);
+ }));
+
+ it('task should transit from notScheduled to scheduled if zoneSpec.onHasTask throw error when scheduleTask',
+ testFnWithLoggedTransitionTo(() => {
+ Zone.current
+ .fork({
+ name: 'testPeriodicalTaskZone',
+ onHasTask: (delegate, currZone, targetZone, hasTaskState) => {
+ throw Error('hasTask Error');
+ }
+ })
+ .run(() => {
+ try {
+ task = Zone.current.scheduleMacroTask(
+ 'testPeriodicalTask', noop, {isPeriodic: true}, noop, noop);
+ } catch (err) {
+ }
+ });
+ expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; }))
+ .toEqual([
+ {toState: 'scheduling', fromState: 'notScheduled'},
+ {toState: 'scheduled', fromState: 'scheduling'}
+ ]);
+ }));
+
+ it('task should transit to notScheduled state if zoneSpec.onHasTask throw error when task is canceled',
+ testFnWithLoggedTransitionTo(() => {
+ Zone.current
+ .fork({
+ name: 'testPeriodicalTaskZone',
+ onHasTask: (delegate, currZone, targetZone, hasTaskState) => {
+ if (task && task.state === 'canceling') {
+ throw Error('hasTask Error');
+ }
+ }
+ })
+ .run(() => {
+ try {
+ task = Zone.current.scheduleMacroTask(
+ 'testPeriodicalTask', noop, {isPeriodic: true}, noop, noop);
+ Zone.current.cancelTask(task);
+ } catch (err) {
+ }
+ });
+ expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; }))
+ .toEqual([
+ {toState: 'scheduling', fromState: 'notScheduled'},
+ {toState: 'scheduled', fromState: 'scheduling'},
+ {toState: 'canceling', fromState: 'scheduled'},
+ {toState: 'notScheduled', fromState: 'canceling'}
+ ]);
+ }));
+ });
+
+ describe('microTask lifecycle', () => {
+ beforeEach(() => { log = []; });
+
+ it('task should transit from notScheduled to scheduling then to scheduled state when scheduleTask',
+ testFnWithLoggedTransitionTo(() => {
+ Zone.current.fork({name: 'testMicroTaskZone'}).run(() => {
+ Zone.current.scheduleMicroTask('testMicroTask', noop, undefined, noop);
+ });
+ expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; }))
+ .toEqual([
+ {toState: 'scheduling', fromState: 'notScheduled'},
+ {toState: 'scheduled', fromState: 'scheduling'}
+ ]);
+ }));
+
+ it('task should transit from scheduling to unknown when zoneSpec onScheduleTask callback throw error',
+ testFnWithLoggedTransitionTo(() => {
+ Zone.current
+ .fork({
+ name: 'testMicroTaskZone',
+ onScheduleTask: (delegate, currZone, targetZone, task) => {
+ throw Error('error in onScheduleTask');
+ }
+ })
+ .run(() => {
+ try {
+ Zone.current.scheduleMicroTask('testMicroTask', noop, undefined, noop);
+ } catch (err) {
+ }
+ });
+ expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; }))
+ .toEqual([
+ {toState: 'scheduling', fromState: 'notScheduled'},
+ {toState: 'unknown', fromState: 'scheduling'}
+ ]);
+ }));
+
+ it('task should transit from scheduled to running when task is invoked then from running to noScheduled after invoke',
+ testFnWithLoggedTransitionTo(() => {
+ Zone.current.fork({name: 'testMicroTaskZone'}).run(() => {
+ const task = Zone.current.scheduleMicroTask('testMicroTask', noop, undefined, noop);
+ task.invoke();
+ });
+ expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; }))
+ .toEqual([
+ {toState: 'scheduling', fromState: 'notScheduled'},
+ {toState: 'scheduled', fromState: 'scheduling'},
+ {toState: 'running', fromState: 'scheduled'},
+ {toState: 'notScheduled', fromState: 'running'}
+ ]);
+ }));
+
+ it('should throw error when try to cancel a microTask', testFnWithLoggedTransitionTo(() => {
+ Zone.current.fork({name: 'testMicroTaskZone'}).run(() => {
+ const task = Zone.current.scheduleMicroTask('testMicroTask', () => {}, undefined, noop);
+ expect(() => { Zone.current.cancelTask(task); }).toThrowError('Task is not cancelable');
+ });
+ }));
+
+ it('task should transit from running to notScheduled when task.callback throw error',
+ testFnWithLoggedTransitionTo(() => {
+ Zone.current.fork({name: 'testMicroTaskZone'}).run(() => {
+ const task = Zone.current.scheduleMicroTask(
+ 'testMicroTask', () => { throw Error('invoke error'); }, undefined, noop);
+ try {
+ task.invoke();
+ } catch (err) {
+ }
+ });
+ expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; }))
+ .toEqual([
+ {toState: 'scheduling', fromState: 'notScheduled'},
+ {toState: 'scheduled', fromState: 'scheduling'},
+ {toState: 'running', fromState: 'scheduled'},
+ {toState: 'notScheduled', fromState: 'running'}
+ ]);
+ }));
+
+ it('task should transit from notScheduled to scheduling then to scheduled if zoneSpec.onHasTask throw error when scheduleTask',
+ testFnWithLoggedTransitionTo(() => {
+ Zone.current
+ .fork({
+ name: 'testMicroTaskZone',
+ onHasTask: (delegate, currZone, targetZone, hasTaskState) => {
+ throw Error('hasTask Error');
+ }
+ })
+ .run(() => {
+ try {
+ Zone.current.scheduleMicroTask('testMicroTask', noop, undefined, noop);
+ } catch (err) {
+ }
+ });
+ expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; }))
+ .toEqual([
+ {toState: 'scheduling', fromState: 'notScheduled'},
+ {toState: 'scheduled', fromState: 'scheduling'}
+ ]);
+ }));
+
+ it('task should transit to notScheduled state if zoneSpec.onHasTask throw error after task.callback being invoked',
+ testFnWithLoggedTransitionTo(() => {
+ let task: Task;
+ Zone.current
+ .fork({
+ name: 'testMicroTaskZone',
+ onHasTask: (delegate, currZone, targetZone, hasTaskState) => {
+ if (task && task.state === 'running') {
+ throw Error('hasTask Error');
+ }
+ }
+ })
+ .run(() => {
+ try {
+ task = Zone.current.scheduleMicroTask('testMicroTask', noop, undefined, noop);
+ task.invoke();
+ } catch (err) {
+ }
+ });
+ expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; }))
+ .toEqual([
+ {toState: 'scheduling', fromState: 'notScheduled'},
+ {toState: 'scheduled', fromState: 'scheduling'},
+ {toState: 'running', fromState: 'scheduled'},
+ {toState: 'notScheduled', fromState: 'running'}
+ ]);
+ }));
+
+ it('task should not run if task transite to notScheduled state which was canceled',
+ testFnWithLoggedTransitionTo(() => {
+ let task: Task;
+ Zone.current.fork({name: 'testCancelZone'}).run(() => {
+ const task =
+ Zone.current.scheduleEventTask('testEventTask', noop, undefined, noop, noop);
+ Zone.current.cancelTask(task);
+ task.invoke();
+ });
+ expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; }))
+ .toEqual([
+ {toState: 'scheduling', fromState: 'notScheduled'},
+ {toState: 'scheduled', fromState: 'scheduling'},
+ {toState: 'canceling', fromState: 'scheduled'},
+ {toState: 'notScheduled', fromState: 'canceling'}
+ ]);
+ }));
+ });
+
+ describe('reschedule zone', () => {
+ let callbackLogs: ({pos: string, method: string, zone: string, task: string} | HasTaskState)[];
+ const newZone = Zone.root.fork({
+ name: 'new',
+ onScheduleTask: (delegate, currZone, targetZone, task) => {
+ callbackLogs.push(
+ {pos: 'before', method: 'onScheduleTask', zone: currZone.name, task: task.zone.name});
+ return delegate.scheduleTask(targetZone, task);
+ },
+ onInvokeTask: (delegate, currZone, targetZone, task, applyThis, applyArgs) => {
+ callbackLogs.push(
+ {pos: 'before', method: 'onInvokeTask', zone: currZone.name, task: task.zone.name});
+ return delegate.invokeTask(targetZone, task, applyThis, applyArgs);
+ },
+ onCancelTask: (delegate, currZone, targetZone, task) => {
+ callbackLogs.push(
+ {pos: 'before', method: 'onCancelTask', zone: currZone.name, task: task.zone.name});
+ return delegate.cancelTask(targetZone, task);
+ },
+ onHasTask: (delegate, currZone, targetZone, hasTaskState) => {
+ (hasTaskState as any)['zone'] = targetZone.name;
+ callbackLogs.push(hasTaskState);
+ return delegate.hasTask(targetZone, hasTaskState);
+ }
+ });
+ const zone = Zone.root.fork({
+ name: 'original',
+ onScheduleTask: (delegate, currZone, targetZone, task) => {
+ callbackLogs.push(
+ {pos: 'before', method: 'onScheduleTask', zone: currZone.name, task: task.zone.name});
+ task.cancelScheduleRequest();
+ task = newZone.scheduleTask(task);
+ callbackLogs.push(
+ {pos: 'after', method: 'onScheduleTask', zone: currZone.name, task: task.zone.name});
+ return task;
+ },
+ onInvokeTask: (delegate, currZone, targetZone, task, applyThis, applyArgs) => {
+ callbackLogs.push(
+ {pos: 'before', method: 'onInvokeTask', zone: currZone.name, task: task.zone.name});
+ return delegate.invokeTask(targetZone, task, applyThis, applyArgs);
+ },
+ onCancelTask: (delegate, currZone, targetZone, task) => {
+ callbackLogs.push(
+ {pos: 'before', method: 'onCancelTask', zone: currZone.name, task: task.zone.name});
+ return delegate.cancelTask(targetZone, task);
+ },
+ onHasTask: (delegate, currZone, targetZone, hasTaskState) => {
+ (hasTaskState)['zone'] = targetZone.name;
+ callbackLogs.push(hasTaskState);
+ return delegate.hasTask(targetZone, hasTaskState);
+ }
+ });
+
+ beforeEach(() => { callbackLogs = []; });
+
+ it('should be able to reschedule zone when in scheduling state, after that, task will completely go to new zone, has nothing to do with original one',
+ testFnWithLoggedTransitionTo(() => {
+ zone.run(() => {
+ const t = Zone.current.scheduleMacroTask(
+ 'testRescheduleZoneTask', noop, undefined, noop, noop);
+ t.invoke();
+ });
+
+ expect(callbackLogs).toEqual([
+ {pos: 'before', method: 'onScheduleTask', zone: 'original', task: 'original'},
+ {pos: 'before', method: 'onScheduleTask', zone: 'new', task: 'new'},
+ {microTask: false, macroTask: true, eventTask: false, change: 'macroTask', zone: 'new'},
+ {pos: 'after', method: 'onScheduleTask', zone: 'original', task: 'new'},
+ {pos: 'before', method: 'onInvokeTask', zone: 'new', task: 'new'}, {
+ microTask: false,
+ macroTask: false,
+ eventTask: false,
+ change: 'macroTask',
+ zone: 'new'
+ }
+ ]);
+ }));
+
+ it('should not be able to reschedule task in notScheduled / running / canceling state',
+ testFnWithLoggedTransitionTo(() => {
+ Zone.current.fork({name: 'rescheduleNotScheduled'}).run(() => {
+ const t = Zone.current.scheduleMacroTask(
+ 'testRescheduleZoneTask', noop, undefined, noop, noop);
+ Zone.current.cancelTask(t);
+ expect(() => { t.cancelScheduleRequest(); })
+ .toThrow(Error(
+ `macroTask 'testRescheduleZoneTask': can not transition to ` +
+ `'notScheduled', expecting state 'scheduling', was 'notScheduled'.`));
+ });
+
+ Zone.current
+ .fork({
+ name: 'rescheduleRunning',
+ onInvokeTask: (delegate, currZone, targetZone, task, applyThis, applyArgs) => {
+ expect(() => { task.cancelScheduleRequest(); })
+ .toThrow(Error(
+ `macroTask 'testRescheduleZoneTask': can not transition to ` +
+ `'notScheduled', expecting state 'scheduling', was 'running'.`));
+ }
+ })
+ .run(() => {
+ const t = Zone.current.scheduleMacroTask(
+ 'testRescheduleZoneTask', noop, undefined, noop, noop);
+ t.invoke();
+ });
+
+ Zone.current
+ .fork({
+ name: 'rescheduleCanceling',
+ onCancelTask: (delegate, currZone, targetZone, task) => {
+ expect(() => { task.cancelScheduleRequest(); })
+ .toThrow(Error(
+ `macroTask 'testRescheduleZoneTask': can not transition to ` +
+ `'notScheduled', expecting state 'scheduling', was 'canceling'.`));
+ }
+ })
+ .run(() => {
+ const t = Zone.current.scheduleMacroTask(
+ 'testRescheduleZoneTask', noop, undefined, noop, noop);
+ Zone.current.cancelTask(t);
+ });
+ }));
+
+ it('can not reschedule a task to a zone which is the descendants of the original zone',
+ testFnWithLoggedTransitionTo(() => {
+ const originalZone = Zone.root.fork({
+ name: 'originalZone',
+ onScheduleTask: (delegate, currZone, targetZone, task) => {
+ callbackLogs.push({
+ pos: 'before',
+ method: 'onScheduleTask',
+ zone: currZone.name,
+ task: task.zone.name
+ });
+ task.cancelScheduleRequest();
+ task = rescheduleZone.scheduleTask(task);
+ callbackLogs.push({
+ pos: 'after',
+ method: 'onScheduleTask',
+ zone: currZone.name,
+ task: task.zone.name
+ });
+ return task;
+ }
+ });
+ const rescheduleZone = originalZone.fork({name: 'rescheduleZone'});
+ expect(() => {
+ originalZone.run(() => {
+ Zone.current.scheduleMacroTask('testRescheduleZoneTask', noop, undefined, noop, noop);
+ });
+ })
+ .toThrowError(
+ 'can not reschedule task to rescheduleZone which is descendants of the original zone originalZone');
+ }));
+ });
+});
diff --git a/packages/zone.js/test/common/toString.spec.ts b/packages/zone.js/test/common/toString.spec.ts
new file mode 100644
index 0000000000..839ecbbdf9
--- /dev/null
+++ b/packages/zone.js/test/common/toString.spec.ts
@@ -0,0 +1,88 @@
+/**
+ * @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
+ */
+
+import {zoneSymbol} from '../../lib/common/utils';
+import {ifEnvSupports} from '../test-util';
+
+const g: any =
+ typeof window !== 'undefined' && window || typeof self !== 'undefined' && self || global;
+describe('global function patch', () => {
+ describe('isOriginal', () => {
+ it('setTimeout toString should be the same with non patched setTimeout', () => {
+ expect(Function.prototype.toString.call(setTimeout))
+ .toEqual(Function.prototype.toString.call(g[zoneSymbol('setTimeout')]));
+ });
+
+ it('MutationObserver toString should be the same with native version',
+ ifEnvSupports('MutationObserver', () => {
+ const nativeMutationObserver = g[zoneSymbol('MutationObserver')];
+ if (typeof nativeMutationObserver === 'function') {
+ expect(Function.prototype.toString.call(g['MutationObserver']))
+ .toEqual(Function.prototype.toString.call(nativeMutationObserver));
+ } else {
+ expect(Function.prototype.toString.call(g['MutationObserver']))
+ .toEqual(Object.prototype.toString.call(nativeMutationObserver));
+ }
+ }));
+ });
+
+ describe('isNative', () => {
+ it('ZoneAwareError toString should look like native',
+ () => { expect(Function.prototype.toString.call(Error)).toContain('[native code]'); });
+
+ it('Function toString should look like native', () => {
+ expect(Function.prototype.toString.call(Function.prototype.toString))
+ .toContain('[native code]');
+ });
+
+ it('EventTarget addEventListener should look like native', ifEnvSupports('HTMLElement', () => {
+ expect(Function.prototype.toString.call(HTMLElement.prototype.addEventListener))
+ .toContain('[native code]');
+ }));
+ });
+});
+
+describe('ZoneTask', () => {
+ it('should return handleId.toString if handleId is available', () => {
+ let macroTask1: any = undefined;
+ let macroTask2: any = undefined;
+ let microTask: any = undefined;
+ const zone = Zone.current.fork({
+ name: 'timer',
+ onScheduleTask: (delegate: ZoneDelegate, curr: Zone, target: Zone, task: Task) => {
+ if (task.type === 'macroTask') {
+ if (!macroTask1) {
+ macroTask1 = task;
+ } else {
+ macroTask2 = task;
+ }
+ } else if (task.type === 'microTask') {
+ microTask = task;
+ }
+ return task;
+ }
+ });
+ zone.run(() => {
+ const id1 = setTimeout(() => {});
+ clearTimeout(id1);
+ const id2 = setTimeout(() => {});
+ clearTimeout(id2);
+ Promise.resolve().then(() => {});
+ const macroTask1Str = macroTask1.toString();
+ const macroTask2Str = macroTask2.toString();
+ expect(typeof macroTask1Str).toEqual('string');
+ expect(macroTask1Str).toEqual(id1.toString());
+ expect(typeof macroTask2Str).toEqual('string');
+ expect(macroTask2Str).toEqual(id2.toString());
+ if (macroTask1.data && typeof macroTask1.data.handleId === 'number') {
+ expect(macroTask1Str).not.toEqual(macroTask2Str);
+ }
+ expect(typeof microTask.toString()).toEqual('string');
+ });
+ });
+});
diff --git a/packages/zone.js/test/common/util.spec.ts b/packages/zone.js/test/common/util.spec.ts
new file mode 100644
index 0000000000..0134bfda10
--- /dev/null
+++ b/packages/zone.js/test/common/util.spec.ts
@@ -0,0 +1,271 @@
+/**
+ * @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
+ */
+
+import {patchMethod, patchProperty, patchPrototype, zoneSymbol} from '../../lib/common/utils';
+
+describe('utils', function() {
+ describe('patchMethod', () => {
+ it('should patch target where the method is defined', () => {
+ let args: any[]|undefined;
+ let self: any;
+ class Type {
+ method(..._args: any[]) {
+ args = _args;
+ self = this;
+ return 'OK';
+ }
+ }
+ const method = Type.prototype.method;
+ let delegateMethod: Function;
+ let delegateSymbol: string;
+
+ const instance = new Type();
+ expect(patchMethod(instance, 'method', (delegate: Function, symbol: string, name: string) => {
+ expect(name).toEqual('method');
+ delegateMethod = delegate;
+ delegateSymbol = symbol;
+ return function(self, args) { return delegate.apply(self, ['patch', args[0]]); };
+ })).toBe(delegateMethod !);
+
+ expect(instance.method('a0')).toEqual('OK');
+ expect(args).toEqual(['patch', 'a0']);
+ expect(self).toBe(instance);
+ expect(delegateMethod !).toBe(method);
+ expect(delegateSymbol !).toEqual(zoneSymbol('method'));
+ expect((Type.prototype as any)[delegateSymbol !]).toBe(method);
+ });
+
+ it('should not double patch', () => {
+ const Type = function() {};
+ const method = Type.prototype.method = function() {};
+ patchMethod(Type.prototype, 'method', (delegate) => {
+ return function(self, args: any[]) { return delegate.apply(self, ['patch', ...args]); };
+ });
+ const pMethod = Type.prototype.method;
+ expect(pMethod).not.toBe(method);
+ patchMethod(Type.prototype, 'method', (delegate) => {
+ return function(self, args) { return delegate.apply(self, ['patch', ...args]); };
+ });
+ expect(pMethod).toBe(Type.prototype.method);
+ });
+
+ it('should not patch property which is not configurable', () => {
+ const TestType = function() {};
+ const originalDefineProperty = (Object as any)[zoneSymbol('defineProperty')];
+ if (originalDefineProperty) {
+ originalDefineProperty(
+ TestType.prototype, 'nonConfigurableProperty',
+ {configurable: false, writable: true, value: 'test'});
+ } else {
+ Object.defineProperty(
+ TestType.prototype, 'nonConfigurableProperty',
+ {configurable: false, writable: true, value: 'test'});
+ }
+ patchProperty(TestType.prototype, 'nonConfigurableProperty');
+ const desc = Object.getOwnPropertyDescriptor(TestType.prototype, 'nonConfigurableProperty');
+ expect(desc !.writable).toBeTruthy();
+ expect(!desc !.get).toBeTruthy();
+ });
+ });
+
+ describe('patchPrototype', () => {
+ it('non configurable property desc should be patched', () => {
+ 'use strict';
+ const TestFunction: any = function() {};
+ const log: string[] = [];
+ Object.defineProperties(TestFunction.prototype, {
+ 'property1': {
+ value: function Property1(callback: Function) { Zone.root.run(callback); },
+ writable: true,
+ configurable: true,
+ enumerable: true
+ },
+ 'property2': {
+ value: function Property2(callback: Function) { Zone.root.run(callback); },
+ writable: true,
+ configurable: false,
+ enumerable: true
+ }
+ });
+
+ const zone = Zone.current.fork({name: 'patch'});
+
+ zone.run(() => {
+ const instance = new TestFunction();
+ instance.property1(() => { log.push('property1' + Zone.current.name); });
+ instance.property2(() => { log.push('property2' + Zone.current.name); });
+ });
+ expect(log).toEqual(['property1', 'property2']);
+ log.length = 0;
+
+ patchPrototype(TestFunction.prototype, ['property1', 'property2']);
+
+ zone.run(() => {
+ const instance = new TestFunction();
+ instance.property1(() => { log.push('property1' + Zone.current.name); });
+ instance.property2(() => { log.push('property2' + Zone.current.name); });
+ });
+ expect(log).toEqual(['property1patch', 'property2patch']);
+ });
+
+ it('non writable property desc should not be patched', () => {
+ 'use strict';
+ const TestFunction: any = function() {};
+ const log: string[] = [];
+ Object.defineProperties(TestFunction.prototype, {
+ 'property1': {
+ value: function Property1(callback: Function) { Zone.root.run(callback); },
+ writable: true,
+ configurable: true,
+ enumerable: true
+ },
+ 'property2': {
+ value: function Property2(callback: Function) { Zone.root.run(callback); },
+ writable: false,
+ configurable: true,
+ enumerable: true
+ }
+ });
+
+ const zone = Zone.current.fork({name: 'patch'});
+
+ zone.run(() => {
+ const instance = new TestFunction();
+ instance.property1(() => { log.push('property1' + Zone.current.name); });
+ instance.property2(() => { log.push('property2' + Zone.current.name); });
+ });
+ expect(log).toEqual(['property1', 'property2']);
+ log.length = 0;
+
+ patchPrototype(TestFunction.prototype, ['property1', 'property2']);
+
+ zone.run(() => {
+ const instance = new TestFunction();
+ instance.property1(() => { log.push('property1' + Zone.current.name); });
+ instance.property2(() => { log.push('property2' + Zone.current.name); });
+ });
+ expect(log).toEqual(['property1patch', 'property2']);
+ });
+
+ it('readonly property desc should not be patched', () => {
+ 'use strict';
+ const TestFunction: any = function() {};
+ const log: string[] = [];
+ Object.defineProperties(TestFunction.prototype, {
+ 'property1': {
+ get: function() {
+ if (!this._property1) {
+ this._property1 = function Property2(callback: Function) { Zone.root.run(callback); };
+ }
+ return this._property1;
+ },
+ set: function(func: Function) { this._property1 = func; },
+ configurable: true,
+ enumerable: true
+ },
+ 'property2': {
+ get: function() {
+ return function Property2(callback: Function) { Zone.root.run(callback); };
+ },
+ configurable: true,
+ enumerable: true
+ }
+ });
+
+ const zone = Zone.current.fork({name: 'patch'});
+
+ zone.run(() => {
+ const instance = new TestFunction();
+ instance.property1(() => { log.push('property1' + Zone.current.name); });
+ instance.property2(() => { log.push('property2' + Zone.current.name); });
+ });
+ expect(log).toEqual(['property1', 'property2']);
+ log.length = 0;
+
+ patchPrototype(TestFunction.prototype, ['property1', 'property2']);
+
+ zone.run(() => {
+ const instance = new TestFunction();
+ instance.property1(() => { log.push('property1' + Zone.current.name); });
+ instance.property2(() => { log.push('property2' + Zone.current.name); });
+ });
+ expect(log).toEqual(['property1patch', 'property2']);
+ });
+
+ it('non writable method should not be patched', () => {
+ 'use strict';
+ const TestFunction: any = function() {};
+ const log: string[] = [];
+ Object.defineProperties(TestFunction.prototype, {
+ 'property2': {
+ value: function Property2(callback: Function) { Zone.root.run(callback); },
+ writable: false,
+ configurable: true,
+ enumerable: true
+ }
+ });
+
+ const zone = Zone.current.fork({name: 'patch'});
+
+ zone.run(() => {
+ const instance = new TestFunction();
+ instance.property2(() => { log.push('property2' + Zone.current.name); });
+ });
+ expect(log).toEqual(['property2']);
+ log.length = 0;
+
+ patchMethod(
+ TestFunction.prototype, 'property2',
+ function(delegate: Function, delegateName: string, name: string) {
+ return function(self: any, args: any) { log.push('patched property2'); };
+ });
+
+ zone.run(() => {
+ const instance = new TestFunction();
+ instance.property2(() => { log.push('property2' + Zone.current.name); });
+ });
+ expect(log).toEqual(['property2']);
+ });
+
+ it('readonly method should not be patched', () => {
+ 'use strict';
+ const TestFunction: any = function() {};
+ const log: string[] = [];
+ Object.defineProperties(TestFunction.prototype, {
+ 'property2': {
+ get: function() {
+ return function Property2(callback: Function) { Zone.root.run(callback); };
+ },
+ configurable: true,
+ enumerable: true
+ }
+ });
+
+ const zone = Zone.current.fork({name: 'patch'});
+
+ zone.run(() => {
+ const instance = new TestFunction();
+ instance.property2(() => { log.push('property2' + Zone.current.name); });
+ });
+ expect(log).toEqual(['property2']);
+ log.length = 0;
+
+ patchMethod(
+ TestFunction.prototype, 'property2',
+ function(delegate: Function, delegateName: string, name: string) {
+ return function(self: any, args: any) { log.push('patched property2'); };
+ });
+
+ zone.run(() => {
+ const instance = new TestFunction();
+ instance.property2(() => { log.push('property2' + Zone.current.name); });
+ });
+ expect(log).toEqual(['property2']);
+ });
+ });
+});
diff --git a/packages/zone.js/test/common/zone.spec.ts b/packages/zone.js/test/common/zone.spec.ts
new file mode 100644
index 0000000000..2f21e1fcc1
--- /dev/null
+++ b/packages/zone.js/test/common/zone.spec.ts
@@ -0,0 +1,388 @@
+/**
+ * @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
+ */
+import {zoneSymbol} from '../../lib/common/utils';
+
+describe('Zone', function() {
+ const rootZone = Zone.current;
+
+ it('should have a name', function() { expect(Zone.current.name).toBeDefined(); });
+
+ describe('hooks', function() {
+ it('should throw if onError is not defined',
+ function() { expect(function() { Zone.current.run(throwError); }).toThrow(); });
+
+
+ it('should fire onError if a function run by a zone throws', function() {
+ const errorSpy = jasmine.createSpy('error');
+ const myZone = Zone.current.fork({name: 'spy', onHandleError: errorSpy});
+
+ expect(errorSpy).not.toHaveBeenCalled();
+
+ expect(function() { myZone.runGuarded(throwError); }).not.toThrow();
+
+ expect(errorSpy).toHaveBeenCalled();
+ });
+
+ it('should send correct currentZone in hook method when in nested zone', function() {
+ const zone = Zone.current;
+ const zoneA = zone.fork({
+ name: 'A',
+ onInvoke: function(
+ parentDelegate, currentZone, targetZone, callback, applyThis, applyArgs, source) {
+ expect(currentZone.name).toEqual('A');
+ return parentDelegate.invoke(targetZone, callback, applyThis, applyArgs, source);
+ }
+ });
+ const zoneB = zoneA.fork({
+ name: 'B',
+ onInvoke: function(
+ parentDelegate, currentZone, targetZone, callback, applyThis, applyArgs, source) {
+ expect(currentZone.name).toEqual('B');
+ return parentDelegate.invoke(targetZone, callback, applyThis, applyArgs, source);
+ }
+ });
+ const zoneC = zoneB.fork({name: 'C'});
+ zoneC.run(function() {});
+ });
+ });
+
+ it('should allow zones to be run from within another zone', function() {
+ const zone = Zone.current;
+ const zoneA = zone.fork({name: 'A'});
+ const zoneB = zone.fork({name: 'B'});
+
+ zoneA.run(function() {
+ zoneB.run(function() { expect(Zone.current).toBe(zoneB); });
+ expect(Zone.current).toBe(zoneA);
+ });
+ expect(Zone.current).toBe(zone);
+ });
+
+
+ describe('wrap', function() {
+ it('should throw if argument is not a function', function() {
+ expect(function() {
+ (Zone.current.wrap)(11);
+ }).toThrowError('Expecting function got: 11');
+ });
+ });
+
+ describe('run out side of current zone', function() {
+ it('should be able to get root zone', function() {
+ Zone.current.fork({name: 'testZone'}).run(function() {
+ expect(Zone.root.name).toEqual('');
+ });
+ });
+
+ it('should be able to get run under rootZone', function() {
+ Zone.current.fork({name: 'testZone'}).run(function() {
+ Zone.root.run(() => { expect(Zone.current.name).toEqual(''); });
+ });
+ });
+
+ it('should be able to get run outside of current zone', function() {
+ Zone.current.fork({name: 'testZone'}).run(function() {
+ Zone.root.fork({name: 'newTestZone'}).run(() => {
+ expect(Zone.current.name).toEqual('newTestZone');
+ expect(Zone.current.parent !.name).toEqual('');
+ });
+ });
+ });
+ });
+
+ describe('get', function() {
+ it('should store properties', function() {
+ const testZone = Zone.current.fork({name: 'A', properties: {key: 'value'}});
+ expect(testZone.get('key')).toEqual('value');
+ expect(testZone.getZoneWith('key')).toEqual(testZone);
+ const childZone = testZone.fork({name: 'B', properties: {key: 'override'}});
+ expect(testZone.get('key')).toEqual('value');
+ expect(testZone.getZoneWith('key')).toEqual(testZone);
+ expect(childZone.get('key')).toEqual('override');
+ expect(childZone.getZoneWith('key')).toEqual(childZone);
+ });
+ });
+
+ describe('task', () => {
+ function noop() {}
+ let log: any[];
+ const zone: Zone = Zone.current.fork({
+ name: 'parent',
+ onHasTask: (delegate: ZoneDelegate, current: Zone, target: Zone, hasTaskState: HasTaskState):
+ void => {
+ (hasTaskState as any)['zone'] = target.name;
+ log.push(hasTaskState);
+ },
+ onScheduleTask: (delegate: ZoneDelegate, current: Zone, target: Zone, task: Task) => {
+ // Do nothing to prevent tasks from being run on VM turn;
+ // Tests run task explicitly.
+ return task;
+ }
+ });
+
+ beforeEach(() => { log = []; });
+
+ it('task can only run in the zone of creation', () => {
+ const task =
+ zone.fork({name: 'createZone'}).scheduleMacroTask('test', noop, undefined, noop, noop);
+ expect(() => { Zone.current.fork({name: 'anotherZone'}).runTask(task); })
+ .toThrowError(
+ 'A task can only be run in the zone of creation! (Creation: createZone; Execution: anotherZone)');
+ task.zone.cancelTask(task);
+ });
+
+ it('task can only cancel in the zone of creation', () => {
+ const task =
+ zone.fork({name: 'createZone'}).scheduleMacroTask('test', noop, undefined, noop, noop);
+ expect(() => { Zone.current.fork({name: 'anotherZone'}).cancelTask(task); })
+ .toThrowError(
+ 'A task can only be cancelled in the zone of creation! (Creation: createZone; Execution: anotherZone)');
+ task.zone.cancelTask(task);
+ });
+
+ it('should prevent double cancellation', () => {
+ const task =
+ zone.scheduleMacroTask('test', () => log.push('macroTask'), undefined, noop, noop);
+ zone.cancelTask(task);
+ try {
+ zone.cancelTask(task);
+ } catch (e) {
+ expect(e.message).toContain(
+ 'macroTask \'test\': can not transition to \'canceling\', expecting state \'scheduled\' or \'running\', was \'notScheduled\'.');
+ }
+ });
+
+ it('should not decrement counters on periodic tasks', () => {
+ zone.run(() => {
+ const task = zone.scheduleMacroTask(
+ 'test', () => log.push('macroTask'), {isPeriodic: true}, noop, noop);
+ zone.runTask(task);
+ zone.runTask(task);
+ zone.cancelTask(task);
+ });
+ expect(log).toEqual([
+ {microTask: false, macroTask: true, eventTask: false, change: 'macroTask', zone: 'parent'},
+ 'macroTask', 'macroTask', {
+ microTask: false,
+ macroTask: false,
+ eventTask: false,
+ change: 'macroTask',
+ zone: 'parent'
+ }
+ ]);
+ });
+
+ it('should notify of queue status change', () => {
+ zone.run(() => {
+ const z = Zone.current;
+ z.runTask(z.scheduleMicroTask('test', () => log.push('microTask')));
+ z.cancelTask(
+ z.scheduleMacroTask('test', () => log.push('macroTask'), undefined, noop, noop));
+ z.cancelTask(
+ z.scheduleEventTask('test', () => log.push('eventTask'), undefined, noop, noop));
+ });
+ expect(log).toEqual([
+ {microTask: true, macroTask: false, eventTask: false, change: 'microTask', zone: 'parent'},
+ 'microTask',
+ {microTask: false, macroTask: false, eventTask: false, change: 'microTask', zone: 'parent'},
+ {microTask: false, macroTask: true, eventTask: false, change: 'macroTask', zone: 'parent'},
+ {microTask: false, macroTask: false, eventTask: false, change: 'macroTask', zone: 'parent'},
+ {microTask: false, macroTask: false, eventTask: true, change: 'eventTask', zone: 'parent'},
+ {
+ microTask: false,
+ macroTask: false,
+ eventTask: false,
+ change: 'eventTask',
+ zone: 'parent'
+ }
+ ]);
+ });
+
+ it('should notify of queue status change on parent task', () => {
+ zone.fork({name: 'child'}).run(() => {
+ const z = Zone.current;
+ z.runTask(z.scheduleMicroTask('test', () => log.push('microTask')));
+ });
+ expect(log).toEqual([
+ {microTask: true, macroTask: false, eventTask: false, change: 'microTask', zone: 'child'},
+ {microTask: true, macroTask: false, eventTask: false, change: 'microTask', zone: 'parent'},
+ 'microTask',
+ {microTask: false, macroTask: false, eventTask: false, change: 'microTask', zone: 'child'},
+ {microTask: false, macroTask: false, eventTask: false, change: 'microTask', zone: 'parent'},
+ ]);
+ });
+
+ it('should allow rescheduling a task on a separate zone', () => {
+ const log: any[] = [];
+ const zone = Zone.current.fork({
+ name: 'test-root',
+ onHasTask:
+ (delegate: ZoneDelegate, current: Zone, target: Zone, hasTaskState: HasTaskState) => {
+ (hasTaskState as any)['zone'] = target.name;
+ log.push(hasTaskState);
+ }
+ });
+ const left = zone.fork({name: 'left'});
+ const right = zone.fork({
+ name: 'right',
+ onScheduleTask: (delegate: ZoneDelegate, current: Zone, target: Zone, task: Task): Task => {
+ log.push(
+ {pos: 'before', method: 'onScheduleTask', zone: current.name, task: task.zone.name});
+ // Cancel the current scheduling of the task
+ task.cancelScheduleRequest();
+ // reschedule on a different zone.
+ task = left.scheduleTask(task);
+ log.push(
+ {pos: 'after', method: 'onScheduleTask', zone: current.name, task: task.zone.name});
+ return task;
+ }
+ });
+ const rchild = right.fork({
+ name: 'rchild',
+ onScheduleTask: (delegate: ZoneDelegate, current: Zone, target: Zone, task: Task): Task => {
+ log.push(
+ {pos: 'before', method: 'onScheduleTask', zone: current.name, task: task.zone.name});
+ task = delegate.scheduleTask(target, task);
+ log.push(
+ {pos: 'after', method: 'onScheduleTask', zone: current.name, task: task.zone.name});
+ expect((task as any)._zoneDelegates.map((zd: ZoneDelegate) => zd.zone.name)).toEqual([
+ 'left', 'test-root', 'ProxyZone'
+ ]);
+ return task;
+ }
+ });
+
+ const task = rchild.scheduleMacroTask('testTask', () => log.push('WORK'), {}, noop, noop);
+ expect(task.zone).toEqual(left);
+ log.push(task.zone.name);
+ task.invoke();
+ expect(log).toEqual([
+ {pos: 'before', method: 'onScheduleTask', zone: 'rchild', task: 'rchild'},
+ {pos: 'before', method: 'onScheduleTask', zone: 'right', task: 'rchild'},
+ {microTask: false, macroTask: true, eventTask: false, change: 'macroTask', zone: 'left'}, {
+ microTask: false,
+ macroTask: true,
+ eventTask: false,
+ change: 'macroTask',
+ zone: 'test-root'
+ },
+ {pos: 'after', method: 'onScheduleTask', zone: 'right', task: 'left'},
+ {pos: 'after', method: 'onScheduleTask', zone: 'rchild', task: 'left'}, 'left', 'WORK',
+ {microTask: false, macroTask: false, eventTask: false, change: 'macroTask', zone: 'left'}, {
+ microTask: false,
+ macroTask: false,
+ eventTask: false,
+ change: 'macroTask',
+ zone: 'test-root'
+ }
+ ]);
+ });
+
+ it('period task should not transit to scheduled state after being cancelled in running state',
+ () => {
+ const zone = Zone.current.fork({name: 'testZone'});
+
+ const task = zone.scheduleMacroTask('testPeriodTask', () => {
+ zone.cancelTask(task);
+ }, {isPeriodic: true}, () => {}, () => {});
+
+ task.invoke();
+ expect(task.state).toBe('notScheduled');
+ });
+
+ it('event task should not transit to scheduled state after being cancelled in running state',
+ () => {
+ const zone = Zone.current.fork({name: 'testZone'});
+
+ const task = zone.scheduleEventTask(
+ 'testEventTask', () => { zone.cancelTask(task); }, undefined, () => {}, () => {});
+
+ task.invoke();
+ expect(task.state).toBe('notScheduled');
+ });
+
+ describe('assert ZoneAwarePromise', () => {
+ it('should not throw when all is OK', () => { Zone.assertZonePatched(); });
+
+ it('should keep ZoneAwarePromise has been patched', () => {
+ class WrongPromise {
+ static resolve(value: any) {}
+
+ then() {}
+ }
+
+ const ZoneAwarePromise = global.Promise;
+ const NativePromise = (global as any)[zoneSymbol('Promise')];
+ global.Promise = WrongPromise;
+ try {
+ expect(ZoneAwarePromise).toBeTruthy();
+ Zone.assertZonePatched();
+ expect(global.Promise).toBe(ZoneAwarePromise);
+ } finally {
+ // restore it.
+ global.Promise = NativePromise;
+ }
+ Zone.assertZonePatched();
+ });
+ });
+ });
+
+ describe('invoking tasks', () => {
+ let log: string[];
+ function noop() {}
+
+
+ beforeEach(() => { log = []; });
+
+ it('should not drain the microtask queue too early', () => {
+ const z = Zone.current;
+ const event = z.scheduleEventTask('test', () => log.push('eventTask'), undefined, noop, noop);
+
+ z.scheduleMicroTask('test', () => log.push('microTask'));
+
+ const macro = z.scheduleMacroTask('test', () => {
+ event.invoke();
+ // At this point, we should not have invoked the microtask.
+ expect(log).toEqual(['eventTask']);
+ }, undefined, noop, noop);
+
+ macro.invoke();
+ });
+
+ it('should convert task to json without cyclic error', () => {
+ const z = Zone.current;
+ const event = z.scheduleEventTask('test', () => {}, undefined, noop, noop);
+ const micro = z.scheduleMicroTask('test', () => {});
+ const macro = z.scheduleMacroTask('test', () => {}, undefined, noop, noop);
+ expect(function() { JSON.stringify(event); }).not.toThrow();
+ expect(function() { JSON.stringify(micro); }).not.toThrow();
+ expect(function() { JSON.stringify(macro); }).not.toThrow();
+ });
+
+ it('should call onHandleError callback when zoneSpec onHasTask throw error', () => {
+ const spy = jasmine.createSpy('error');
+ const hasTaskZone = Zone.current.fork({
+ name: 'hasTask',
+ onHasTask: (delegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
+ hasTasState: HasTaskState) => { throw new Error('onHasTask Error'); },
+ onHandleError:
+ (delegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, error: Error) => {
+ spy(error.message);
+ return delegate.handleError(targetZone, error);
+ }
+ });
+
+ const microTask = hasTaskZone.scheduleMicroTask('test', () => {}, undefined, () => {});
+ expect(spy).toHaveBeenCalledWith('onHasTask Error');
+ });
+ });
+});
+
+function throwError() {
+ throw new Error();
+}
diff --git a/packages/zone.js/test/common_tests.ts b/packages/zone.js/test/common_tests.ts
new file mode 100644
index 0000000000..426762d561
--- /dev/null
+++ b/packages/zone.js/test/common_tests.ts
@@ -0,0 +1,27 @@
+/**
+ * @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
+ */
+
+import './common/microtasks.spec';
+import './common/zone.spec';
+import './common/task.spec';
+import './common/util.spec';
+import './common/Promise.spec';
+import './common/fetch.spec';
+import './common/Error.spec';
+import './common/setInterval.spec';
+import './common/setTimeout.spec';
+import './common/toString.spec';
+import './zone-spec/long-stack-trace-zone.spec';
+import './zone-spec/async-test.spec';
+import './zone-spec/sync-test.spec';
+import './zone-spec/fake-async-test.spec';
+import './zone-spec/proxy.spec';
+import './zone-spec/task-tracking.spec';
+import './rxjs/rxjs.spec';
+
+Error.stackTraceLimit = Number.POSITIVE_INFINITY;
diff --git a/packages/zone.js/test/extra/bluebird.spec.ts b/packages/zone.js/test/extra/bluebird.spec.ts
new file mode 100644
index 0000000000..1f935e4067
--- /dev/null
+++ b/packages/zone.js/test/extra/bluebird.spec.ts
@@ -0,0 +1,703 @@
+/**
+ * @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
+ */
+// test bluebird promise patch
+// this spec will not be integrated with Travis CI, because I don't
+// want to add bluebird into devDependencies, you can run this spec
+// on your local environment
+process.on('unhandledRejection', (reason, p) => {
+ console.log('Unhandled Rejection at:', p, 'reason:', reason);
+ // application specific logging, throwing an error, or other logic here
+});
+
+describe('bluebird promise', () => {
+ let BluebirdPromise: any;
+ beforeAll(() => {
+ BluebirdPromise = require('bluebird');
+ // import bluebird patch
+ require('../../lib/extra/bluebird');
+ const patchBluebird = (Zone as any)[(Zone as any).__symbol__('bluebird')];
+ patchBluebird(BluebirdPromise);
+ });
+
+ let log: string[];
+
+ const zone = Zone.root.fork({
+ name: 'bluebird',
+ onScheduleTask: (delegate, curr, targetZone, task) => {
+ log.push('schedule bluebird task ' + task.source);
+ return delegate.scheduleTask(targetZone, task);
+ },
+ onInvokeTask: (delegate, curr, target, task, applyThis, applyArgs) => {
+ log.push('invoke bluebird task ' + task.source);
+ return delegate.invokeTask(target, task, applyThis, applyArgs);
+ }
+ });
+
+ beforeEach(() => { log = []; });
+
+ it('bluebird promise then method should be in zone and treated as microTask', (done) => {
+ zone.run(() => {
+ const p = new BluebirdPromise(
+ (resolve: any, reject: any) => { setTimeout(() => { resolve('test'); }, 0); });
+ p.then(() => {
+ expect(Zone.current.name).toEqual('bluebird');
+ expect(log.filter(item => item === 'schedule bluebird task Promise.then').length).toBe(1);
+ expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1);
+ done();
+ });
+ });
+ });
+
+ it('bluebird promise catch method should be in zone and treated as microTask', (done) => {
+ zone.run(() => {
+ const p = new BluebirdPromise(
+ (resolve: any, reject: any) => { setTimeout(() => { reject('test'); }, 0); });
+ p.catch(() => {
+ expect(log.filter(item => item === 'schedule bluebird task Promise.then').length).toBe(1);
+ expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1);
+ expect(Zone.current.name).toEqual('bluebird');
+ done();
+ });
+ });
+ });
+
+ it('bluebird promise spread method should be in zone', (done) => {
+ zone.run(() => {
+ BluebirdPromise.all([BluebirdPromise.resolve('test1'), BluebirdPromise.resolve('test2')])
+ .spread((r1: string, r2: string) => {
+ expect(r1).toEqual('test1');
+ expect(r2).toEqual('test2');
+ expect(log.filter(item => item === 'schedule bluebird task Promise.then').length)
+ .toBe(1);
+ expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1);
+ expect(Zone.current.name).toEqual('bluebird');
+ done();
+ });
+ });
+ });
+
+ it('bluebird promise finally method should be in zone', (done) => {
+ zone.run(() => {
+ const p = new BluebirdPromise(
+ (resolve: any, reject: any) => { setTimeout(() => { resolve('test'); }, 0); });
+ p.finally(() => {
+ expect(log.filter(item => item === 'schedule bluebird task Promise.then').length).toBe(1);
+ expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1);
+ expect(Zone.current.name).toEqual('bluebird');
+ done();
+ });
+ });
+ });
+
+ it('bluebird promise join method should be in zone', (done) => {
+ zone.run(() => {
+ BluebirdPromise
+ .join(
+ BluebirdPromise.resolve('test1'), BluebirdPromise.resolve('test2'),
+ (r1: string, r2: string) => {
+ expect(r1).toEqual('test1');
+ expect(r2).toEqual('test2');
+ expect(Zone.current.name).toEqual('bluebird');
+ })
+ .then(() => {
+ expect(Zone.current.name).toEqual('bluebird');
+ expect(log.filter(item => item === 'schedule bluebird task Promise.then').length)
+ .toBe(1);
+ expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1);
+ done();
+ });
+ });
+ });
+
+ it('bluebird promise try method should be in zone', (done) => {
+ zone.run(() => {
+ BluebirdPromise.try(() => { throw new Error('promise error'); }).catch((err: Error) => {
+ expect(log.filter(item => item === 'schedule bluebird task Promise.then').length).toBe(1);
+ expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1);
+ expect(Zone.current.name).toEqual('bluebird');
+ expect(err.message).toEqual('promise error');
+ done();
+ });
+ });
+ });
+
+ it('bluebird promise method method should be in zone', (done) => {
+ zone.run(() => {
+ BluebirdPromise.method(() => { return 'test'; })().then((result: string) => {
+ expect(log.filter(item => item === 'schedule bluebird task Promise.then').length).toBe(1);
+ expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1);
+ expect(Zone.current.name).toEqual('bluebird');
+ expect(result).toEqual('test');
+ done();
+ });
+ });
+ });
+
+ it('bluebird promise resolve method should be in zone', (done) => {
+ zone.run(() => {
+ BluebirdPromise.resolve('test').then((result: string) => {
+ expect(log.filter(item => item === 'schedule bluebird task Promise.then').length).toBe(1);
+ expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1);
+ expect(Zone.current.name).toEqual('bluebird');
+ expect(result).toEqual('test');
+ done();
+ });
+ });
+ });
+
+ it('bluebird promise reject method should be in zone', (done) => {
+ zone.run(() => {
+ BluebirdPromise.reject('error').catch((error: any) => {
+ expect(log.filter(item => item === 'schedule bluebird task Promise.then').length).toBe(1);
+ expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1);
+ expect(Zone.current.name).toEqual('bluebird');
+ expect(error).toEqual('error');
+ done();
+ });
+ });
+ });
+
+ it('bluebird promise all method should be in zone', (done) => {
+ zone.run(() => {
+ BluebirdPromise.all([BluebirdPromise.resolve('test1'), BluebirdPromise.resolve('test2')])
+ .then((r: string[]) => {
+ expect(r[0]).toEqual('test1');
+ expect(r[1]).toEqual('test2');
+ expect(log.filter(item => item === 'schedule bluebird task Promise.then').length)
+ .toBe(1);
+ expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1);
+ expect(Zone.current.name).toEqual('bluebird');
+ done();
+ });
+ });
+ });
+
+ it('bluebird promise props method should be in zone', (done) => {
+ zone.run(() => {
+ BluebirdPromise
+ .props({test1: BluebirdPromise.resolve('test1'), test2: BluebirdPromise.resolve('test2')})
+ .then((r: any) => {
+ expect(r.test1).toEqual('test1');
+ expect(r.test2).toEqual('test2');
+ expect(log.filter(item => item === 'schedule bluebird task Promise.then').length)
+ .toBe(1);
+ expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1);
+ expect(Zone.current.name).toEqual('bluebird');
+ done();
+ });
+ });
+ });
+
+ it('bluebird promise any method should be in zone', (done) => {
+ zone.run(() => {
+ BluebirdPromise.any([BluebirdPromise.resolve('test1'), BluebirdPromise.resolve('test2')])
+ .then((r: any) => {
+ expect(r).toEqual('test1');
+ expect(log.filter(item => item === 'schedule bluebird task Promise.then').length)
+ .toBe(1);
+ expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1);
+ expect(Zone.current.name).toEqual('bluebird');
+ done();
+ });
+ });
+ });
+
+ it('bluebird promise some method should be in zone', (done) => {
+ zone.run(() => {
+ BluebirdPromise.some([BluebirdPromise.resolve('test1'), BluebirdPromise.resolve('test2')], 1)
+ .then((r: any) => {
+ expect(r.length).toBe(1);
+ expect(r[0]).toEqual('test1');
+ expect(log.filter(item => item === 'schedule bluebird task Promise.then').length)
+ .toBe(1);
+ expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1);
+ expect(Zone.current.name).toEqual('bluebird');
+ done();
+ });
+ });
+ });
+
+ it('bluebird promise map method should be in zone', (done) => {
+ zone.run(() => {
+ BluebirdPromise
+ .map(['test1', 'test2'], (value: any) => { return BluebirdPromise.resolve(value); })
+ .then((r: string[]) => {
+ expect(r.length).toBe(2);
+ expect(r[0]).toEqual('test1');
+ expect(r[1]).toEqual('test2');
+ expect(log.filter(item => item === 'schedule bluebird task Promise.then').length)
+ .toBe(1);
+ expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1);
+ expect(Zone.current.name).toEqual('bluebird');
+ done();
+ });
+ });
+ });
+
+ it('bluebird promise reduce method should be in zone', (done) => {
+ zone.run(() => {
+ BluebirdPromise
+ .reduce(
+ [1, 2],
+ (total: string, value: string) => { return BluebirdPromise.resolve(total + value); })
+ .then((r: number) => {
+ expect(r).toBe(3);
+ expect(log.filter(item => item === 'schedule bluebird task Promise.then').length)
+ .toBeTruthy();
+ expect(log.filter(item => item === 'invoke bluebird task Promise.then').length)
+ .toBeTruthy();
+ expect(Zone.current.name).toEqual('bluebird');
+ done();
+ });
+ });
+ });
+
+ it('bluebird promise filter method should be in zone', (done) => {
+ zone.run(() => {
+ BluebirdPromise
+ .filter(
+ [1, 2, 3],
+ (value: number) => {
+ return value % 2 === 0 ? BluebirdPromise.resolve(true) :
+ BluebirdPromise.resolve(false);
+ })
+ .then((r: number[]) => {
+ expect(r[0]).toBe(2);
+ expect(log.filter(item => item === 'schedule bluebird task Promise.then').length)
+ .toBe(1);
+ expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1);
+ expect(Zone.current.name).toEqual('bluebird');
+ done();
+ });
+ });
+ });
+
+ it('bluebird promise each method should be in zone', (done) => {
+ zone.run(() => {
+ const arr = [1, 2, 3];
+ BluebirdPromise
+ .each(
+ BluebirdPromise.map(arr, (item: number) => BluebirdPromise.resolve(item)),
+ (r: number[], idx: number) => {
+ expect(r[idx] === arr[idx]);
+ expect(Zone.current.name).toEqual('bluebird');
+ })
+ .then((r: any) => {
+ expect(log.filter(item => item === 'schedule bluebird task Promise.then').length)
+ .toBeTruthy();
+ expect(log.filter(item => item === 'invoke bluebird task Promise.then').length)
+ .toBeTruthy();
+ expect(Zone.current.name).toEqual('bluebird');
+ done();
+ });
+ });
+ });
+
+ it('bluebird promise mapSeries method should be in zone', (done) => {
+ zone.run(() => {
+ const arr = [1, 2, 3];
+ BluebirdPromise
+ .mapSeries(
+ BluebirdPromise.map(arr, (item: number) => BluebirdPromise.resolve(item)),
+ (r: number[], idx: number) => {
+ expect(r[idx] === arr[idx]);
+ expect(Zone.current.name).toEqual('bluebird');
+ })
+ .then((r: any) => {
+ expect(log.filter(item => item === 'schedule bluebird task Promise.then').length)
+ .toBeTruthy();
+ expect(log.filter(item => item === 'invoke bluebird task Promise.then').length)
+ .toBeTruthy();
+ expect(Zone.current.name).toEqual('bluebird');
+ done();
+ });
+ ;
+ });
+ });
+
+ it('bluebird promise race method should be in zone', (done) => {
+ zone.run(() => {
+ BluebirdPromise.race([BluebirdPromise.resolve('test1'), BluebirdPromise.resolve('test2')])
+ .then((r: string) => {
+ expect(r).toEqual('test1');
+ expect(log.filter(item => item === 'schedule bluebird task Promise.then').length)
+ .toBe(1);
+ expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1);
+ expect(Zone.current.name).toEqual('bluebird');
+ done();
+ });
+ });
+ });
+
+ it('bluebird promise using/disposer method should be in zone', (done) => {
+ zone.run(() => {
+ const p = new BluebirdPromise(
+ (resolve: Function, reject: any) => { setTimeout(() => { resolve('test'); }, 0); });
+ p.leakObj = [];
+ const disposer = p.disposer(() => { p.leakObj = null; });
+ BluebirdPromise.using(disposer, (v: string) => { p.leakObj.push(v); }).then(() => {
+ expect(Zone.current.name).toEqual('bluebird');
+ expect(p.leakObj).toBe(null);
+ // using will generate several promise inside bluebird
+ expect(log.filter(item => item === 'schedule bluebird task Promise.then').length)
+ .toBeTruthy();
+ expect(log.filter(item => item === 'invoke bluebird task Promise.then').length)
+ .toBeTruthy();
+ done();
+ });
+ });
+ });
+
+ it('bluebird promise promisify method should be in zone and treated as microTask', (done) => {
+ const func = (cb: Function) => { setTimeout(() => { cb(null, 'test'); }, 10); };
+
+ const promiseFunc = BluebirdPromise.promisify(func);
+ zone.run(() => {
+ promiseFunc().then((r: string) => {
+ expect(Zone.current.name).toEqual('bluebird');
+ expect(r).toBe('test');
+ expect(log.filter(item => item === 'schedule bluebird task Promise.then').length).toBe(1);
+ expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1);
+ done();
+ });
+ });
+ });
+
+ it('bluebird promise promisifyAll method should be in zone', (done) => {
+ const obj = {
+ func1: (cb: Function) => { setTimeout(() => { cb(null, 'test1'); }, 10); },
+ func2: (cb: Function) => { setTimeout(() => { cb(null, 'test2'); }, 10); },
+ };
+
+ const promiseObj = BluebirdPromise.promisifyAll(obj);
+ zone.run(() => {
+ BluebirdPromise.all([promiseObj.func1Async(), promiseObj.func2Async()])
+ .then((r: string[]) => {
+ expect(Zone.current.name).toEqual('bluebird');
+ expect(r[0]).toBe('test1');
+ expect(r[1]).toBe('test2');
+ // using will generate several promise inside
+ expect(log.filter(item => item === 'schedule bluebird task Promise.then').length)
+ .toBe(1);
+ expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1);
+ done();
+ });
+ });
+ });
+
+ it('bluebird promise fromCallback method should be in zone', (done) => {
+ const resolver = (cb: Function) => { setTimeout(() => { cb(null, 'test'); }, 10); };
+
+ zone.run(() => {
+ BluebirdPromise.fromCallback(resolver).then((r: string) => {
+ expect(Zone.current.name).toEqual('bluebird');
+ expect(r).toBe('test');
+ expect(log.filter(item => item === 'schedule bluebird task Promise.then').length).toBe(1);
+ expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1);
+ done();
+ });
+ });
+ });
+
+ it('bluebird promise asCallback method should be in zone', (done) => {
+ zone.run(() => {
+ BluebirdPromise.resolve('test').asCallback((err: Error, r: string) => {
+ expect(Zone.current.name).toEqual('bluebird');
+ expect(r).toBe('test');
+ done();
+ });
+ });
+ });
+
+ it('bluebird promise delay method should be in zone', (done) => {
+ zone.run(() => {
+ BluebirdPromise.resolve('test').delay(10).then((r: string) => {
+ expect(Zone.current.name).toEqual('bluebird');
+ expect(r).toBe('test');
+ expect(log.filter(item => item === 'schedule bluebird task Promise.then').length).toBe(1);
+ expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1);
+ done();
+ });
+ });
+ });
+
+ it('bluebird promise timeout method should be in zone', (done) => {
+ zone.run(() => {
+ new BluebirdPromise(
+ (resolve: any, reject: any) => { setTimeout(() => { resolve('test'); }, 10); })
+ .timeout(100)
+ .then((r: string) => {
+ expect(Zone.current.name).toEqual('bluebird');
+ expect(r).toBe('test');
+ expect(log.filter(item => item === 'schedule bluebird task Promise.then').length)
+ .toBe(1);
+ expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1);
+ done();
+ });
+ });
+ });
+
+ it('bluebird promise tap method should be in zone', (done) => {
+ zone.run(() => {
+ const p = new BluebirdPromise(
+ (resolve: any, reject: any) => { setTimeout(() => { resolve('test'); }, 0); });
+ p.tap(() => { expect(Zone.current.name).toEqual('bluebird'); }).then(() => {
+ expect(log.filter(item => item === 'schedule bluebird task Promise.then').length).toBe(1);
+ expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1);
+ expect(Zone.current.name).toEqual('bluebird');
+ done();
+ });
+ });
+ });
+
+ it('bluebird promise call method should be in zone', (done) => {
+ zone.run(() => {
+ BluebirdPromise
+ .map(['test1', 'test2'], (value: any) => { return BluebirdPromise.resolve(value); })
+ .call('shift', (value: any) => { return value; })
+ .then((r: string) => {
+ expect(r).toEqual('test1');
+ expect(log.filter(item => item === 'schedule bluebird task Promise.then').length)
+ .toBe(1);
+ expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1);
+ expect(Zone.current.name).toEqual('bluebird');
+ done();
+ });
+ });
+ });
+
+ it('bluebird promise get method should be in zone', (done) => {
+ zone.run(() => {
+ BluebirdPromise.resolve(['test1', 'test2']).get(-1).then((r: string) => {
+ expect(r).toEqual('test2');
+ expect(log.filter(item => item === 'schedule bluebird task Promise.then').length).toBe(1);
+ expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1);
+ expect(Zone.current.name).toEqual('bluebird');
+ done();
+ });
+ });
+ });
+
+ it('bluebird promise return method should be in zone', (done) => {
+ zone.run(() => {
+ BluebirdPromise.resolve().return ('test1').then((r: string) => {
+ expect(r).toEqual('test1');
+ expect(log.filter(item => item === 'schedule bluebird task Promise.then').length).toBe(1);
+ expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1);
+ expect(Zone.current.name).toEqual('bluebird');
+ done();
+ });
+ });
+ });
+
+ it('bluebird promise throw method should be in zone', (done) => {
+ zone.run(() => {
+ BluebirdPromise.resolve().throw('test1').catch((r: string) => {
+ expect(r).toEqual('test1');
+ expect(log.filter(item => item === 'schedule bluebird task Promise.then').length).toBe(1);
+ expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1);
+ expect(Zone.current.name).toEqual('bluebird');
+ done();
+ });
+ });
+ });
+
+ it('bluebird promise catchReturn method should be in zone', (done) => {
+ zone.run(() => {
+ BluebirdPromise.reject().catchReturn('test1').then((r: string) => {
+ expect(r).toEqual('test1');
+ expect(log.filter(item => item === 'schedule bluebird task Promise.then').length).toBe(1);
+ expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1);
+ expect(Zone.current.name).toEqual('bluebird');
+ done();
+ });
+ });
+ });
+
+ it('bluebird promise catchThrow method should be in zone', (done) => {
+ zone.run(() => {
+ BluebirdPromise.reject().catchThrow('test1').catch((r: string) => {
+ expect(r).toEqual('test1');
+ expect(log.filter(item => item === 'schedule bluebird task Promise.then').length).toBe(1);
+ expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1);
+ expect(Zone.current.name).toEqual('bluebird');
+ done();
+ });
+ });
+ });
+
+ it('bluebird promise reflect method should be in zone', (done) => {
+ zone.run(() => {
+ const promises = [BluebirdPromise.resolve('test1'), BluebirdPromise.reject('test2')];
+ BluebirdPromise.all(promises.map(promise => { return promise.reflect(); })).each((r: any) => {
+ if (r.isFulfilled()) {
+ expect(r.value()).toEqual('test1');
+ } else {
+ expect(r.reason()).toEqual('test2');
+ done();
+ }
+ expect(Zone.current.name).toEqual('bluebird');
+ });
+ });
+ });
+
+ it('bluebird should be able to run into different zone', (done: Function) => {
+ Zone.current.fork({name: 'zone_A'}).run(() => {
+ new BluebirdPromise((resolve: any, reject: any) => {
+ expect(Zone.current.name).toEqual('zone_A');
+ resolve(1);
+ }).then((r: any) => { expect(Zone.current.name).toEqual('zone_A'); });
+ });
+
+ Zone.current.fork({name: 'zone_B'}).run(() => {
+ new BluebirdPromise((resolve: any, reject: any) => {
+ expect(Zone.current.name).toEqual('zone_B');
+ resolve(2);
+ }).then((r: any) => {
+ expect(Zone.current.name).toEqual('zone_B');
+ done();
+ });
+ });
+ });
+
+ it('should be able to chain promise', (done: DoneFn) => {
+ Zone.current.fork({name: 'zone_A'}).run(() => {
+ new BluebirdPromise((resolve: any, reject: any) => {
+ expect(Zone.current.name).toEqual('zone_A');
+ resolve(1);
+ })
+ .then((r: any) => {
+ expect(r).toBe(1);
+ expect(Zone.current.name).toEqual('zone_A');
+ return Promise.resolve(2);
+ })
+ .then((r: any) => {
+ expect(r).toBe(2);
+ expect(Zone.current.name).toEqual('zone_A');
+ });
+ });
+ Zone.current.fork({name: 'zone_B'}).run(() => {
+ new BluebirdPromise((resolve: any, reject: any) => {
+ expect(Zone.current.name).toEqual('zone_B');
+ reject(1);
+ })
+ .then(
+ () => { fail('should not be here.'); },
+ (r: any) => {
+ expect(r).toBe(1);
+ expect(Zone.current.name).toEqual('zone_B');
+ return Promise.resolve(2);
+ })
+ .then((r: any) => {
+ expect(r).toBe(2);
+ expect(Zone.current.name).toEqual('zone_B');
+ done();
+ });
+ });
+ });
+
+ it('should catch rejected chained bluebird promise', (done: DoneFn) => {
+ const logs: string[] = [];
+ const zone = Zone.current.fork({
+ name: 'testErrorHandling',
+ onHandleError: function() {
+ // should not get here
+ logs.push('onHandleError');
+ return true;
+ }
+ });
+
+ zone.runGuarded(() => {
+ return BluebirdPromise.resolve().then(() => { throw new Error('test error'); }).catch(() => {
+ expect(logs).toEqual([]);
+ done();
+ });
+ });
+ });
+
+ it('should catch rejected chained global promise', (done: DoneFn) => {
+ const logs: string[] = [];
+ const zone = Zone.current.fork({
+ name: 'testErrorHandling',
+ onHandleError: function() {
+ // should not get here
+ logs.push('onHandleError');
+ return true;
+ }
+ });
+
+ zone.runGuarded(() => {
+ return Promise.resolve().then(() => { throw new Error('test error'); }).catch(() => {
+ expect(logs).toEqual([]);
+ done();
+ });
+ });
+ });
+
+ it('should catch rejected bluebird promise', (done: DoneFn) => {
+ const logs: string[] = [];
+ const zone = Zone.current.fork({
+ name: 'testErrorHandling',
+ onHandleError: function() {
+ // should not get here
+ logs.push('onHandleError');
+ return true;
+ }
+ });
+
+ zone.runGuarded(() => {
+ return BluebirdPromise.reject().catch(() => {
+ expect(logs).toEqual([]);
+ done();
+ });
+ });
+ });
+
+ it('should catch rejected global promise', (done: DoneFn) => {
+ const logs: string[] = [];
+ const zone = Zone.current.fork({
+ name: 'testErrorHandling',
+ onHandleError: function() {
+ // should not get here
+ logs.push('onHandleError');
+ return true;
+ }
+ });
+
+ zone.runGuarded(() => {
+ return Promise.reject(new Error('reject')).catch(() => {
+ expect(logs).toEqual([]);
+ done();
+ });
+ });
+ });
+
+ it('should trigger onHandleError when unhandledRejection', (done: DoneFn) => {
+ const zone = Zone.current.fork({
+ name: 'testErrorHandling',
+ onHandleError: function() {
+ setTimeout(done, 100);
+ return true;
+ }
+ });
+
+ zone.runGuarded(() => { return Promise.reject(new Error('reject')); });
+ });
+
+ it('should trigger onHandleError when unhandledRejection in chained Promise', (done: DoneFn) => {
+ const zone = Zone.current.fork({
+ name: 'testErrorHandling',
+ onHandleError: function() {
+ setTimeout(done, 100);
+ return true;
+ }
+ });
+
+ zone.runGuarded(() => { return Promise.resolve().then(() => { throw new Error('test'); }); });
+ });
+});
diff --git a/packages/zone.js/test/extra/cordova.spec.ts b/packages/zone.js/test/extra/cordova.spec.ts
new file mode 100644
index 0000000000..3339ac4849
--- /dev/null
+++ b/packages/zone.js/test/extra/cordova.spec.ts
@@ -0,0 +1,35 @@
+/**
+ * @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
+ */
+describe('cordova test', () => {
+ it('cordova.exec() should be patched as macroTask', (done) => {
+ const cordova = (window as any).cordova;
+ if (!cordova) {
+ done();
+ return;
+ }
+
+ const zone = Zone.current.fork({name: 'cordova'});
+
+ zone.run(() => {
+ cordova.exec(
+ () => {
+ expect(Zone.current.name).toEqual('cordova');
+ done();
+ },
+ () => { fail('should not fail'); }, 'service', 'successAction', ['arg0', 'arg1']);
+
+ cordova.exec(
+ () => { fail('should not success'); },
+ () => {
+ expect(Zone.current.name).toEqual('cordova');
+ done();
+ },
+ 'service', 'failAction', ['arg0', 'arg1']);
+ });
+ });
+});
\ No newline at end of file
diff --git a/packages/zone.js/test/fake_entry.js b/packages/zone.js/test/fake_entry.js
new file mode 100644
index 0000000000..ec965799df
--- /dev/null
+++ b/packages/zone.js/test/fake_entry.js
@@ -0,0 +1 @@
+var TEST = 'TEST';
diff --git a/packages/zone.js/test/jasmine-patch.spec.ts b/packages/zone.js/test/jasmine-patch.spec.ts
new file mode 100644
index 0000000000..2765aceace
--- /dev/null
+++ b/packages/zone.js/test/jasmine-patch.spec.ts
@@ -0,0 +1,76 @@
+/**
+ * @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
+ */
+
+import {ifEnvSupports} from './test-util';
+
+function supportJasmineSpec() {
+ return jasmine && (jasmine as any)['Spec'];
+}
+
+(supportJasmineSpec as any).message = 'jasmine spec';
+
+ifEnvSupports(supportJasmineSpec, () => {
+ beforeEach(() => {
+ // assert that each jasmine run has a task, so that drainMicrotask works properly.
+ expect(Zone.currentTask).toBeTruthy();
+ });
+
+ describe('jasmine', () => {
+ let throwOnAsync = false;
+ let beforeEachZone: Zone|null = null;
+ let beforeAllZone: Zone|null = null;
+ let itZone: Zone|null = null;
+ const syncZone = Zone.current;
+ try {
+ Zone.current.scheduleMicroTask('dontallow', (): any => null);
+ } catch (e) {
+ throwOnAsync = true;
+ }
+
+ beforeAll(() => beforeAllZone = Zone.current);
+
+ beforeEach(() => beforeEachZone = Zone.current);
+
+ it('should throw on async in describe', () => {
+ expect(throwOnAsync).toBe(true);
+ expect(syncZone.name).toEqual('syncTestZone for jasmine.describe');
+ itZone = Zone.current;
+ });
+
+ it('should cope with pending tests, which have no test body');
+
+ afterEach(() => {
+ let zone = Zone.current;
+ expect(zone.name).toEqual('ProxyZone');
+ expect(beforeEachZone !.name).toEqual(zone.name);
+ expect(itZone).toBe(zone);
+ });
+
+ afterAll(() => {
+ let zone = Zone.current;
+ expect(zone.name).toEqual('ProxyZone');
+ expect(beforeAllZone !.name).toEqual(zone.name);
+ });
+ });
+
+ describe('return promise', () => {
+ let log: string[];
+ beforeEach(() => { log = []; });
+
+ it('should wait for promise to resolve', () => {
+ return new Promise((res, _) => {
+ setTimeout(() => {
+ log.push('resolved');
+ res();
+ }, 100);
+ });
+ });
+
+ afterEach(() => { expect(log).toEqual(['resolved']); });
+ });
+})();
diff --git a/packages/zone.js/test/main.ts b/packages/zone.js/test/main.ts
new file mode 100644
index 0000000000..84e1a0fa85
--- /dev/null
+++ b/packages/zone.js/test/main.ts
@@ -0,0 +1,74 @@
+/**
+ * @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
+ */
+
+///
+
+declare const __karma__: {
+ loaded: Function,
+ start: Function,
+ error: Function,
+};
+
+__karma__.loaded = function() {};
+
+let entryPoint = 'browser_entry_point';
+
+if (typeof __karma__ !== 'undefined') {
+ (window as any)['__Zone_Error_BlacklistedStackFrames_policy'] =
+ (__karma__ as any).config.errorpolicy;
+ if ((__karma__ as any).config.entrypoint) {
+ entryPoint = (__karma__ as any).config.entrypoint;
+ }
+} else if (typeof process !== 'undefined') {
+ (window as any)['__Zone_Error_BlacklistedStackFrames_policy'] = process.env.errorpolicy;
+ if (process.env.entrypoint) {
+ entryPoint = process.env.entrypoint;
+ }
+}
+
+(window as any).global = window;
+System.config({
+ defaultJSExtensions: true,
+ map: {
+ 'rxjs': 'base/npm/node_modules/rxjs/index',
+ 'rxjs/operators': 'base/npm/node_modules/rxjs/operators/index',
+ 'core-js/features/set': 'base/npm/node_modules/core-js/es6/set',
+ 'core-js/features/map': 'base/npm/node_modules/core-js/es6/map',
+ 'es6-promise': 'base/npm/node_modules/es6-promise/dist/es6-promise'
+ },
+});
+
+let browserPatchedPromise: any = null;
+if ((window as any)[(Zone as any).__symbol__('setTimeout')]) {
+ browserPatchedPromise = Promise.resolve('browserPatched');
+} else {
+ // this means that Zone has not patched the browser yet, which means we must be running in
+ // build mode and need to load the browser patch.
+ browserPatchedPromise =
+ System.import('/base/angular/packages/zone.js/test/browser-zone-setup').then(() => {
+ let testFrameworkPatch = typeof(window as any).Mocha !== 'undefined' ?
+ '/base/angular/packages/zone.js/lib/mocha/mocha' :
+ '/base/angular/packages/zone.js/lib/jasmine/jasmine';
+ return System.import(testFrameworkPatch);
+ });
+}
+
+browserPatchedPromise.then(() => {
+ let testFrameworkPatch = typeof(window as any).Mocha !== 'undefined' ?
+ '/base/angular/packages/zone.js/test/test-env-setup-mocha' :
+ '/base/angular/packages/zone.js/test/test-env-setup-jasmine';
+ // Setup test environment
+ System.import(testFrameworkPatch).then(() => {
+ System.import('/base/angular/packages/zone.js/lib/common/error-rewrite').then(() => {
+ System.import(`/base/angular/packages/zone.js/test/${entryPoint}`)
+ .then(
+ () => { __karma__.start(); },
+ (error: any) => { console.error(error.stack || error); });
+ });
+ });
+});
diff --git a/packages/zone.js/test/mocha-patch.spec.ts b/packages/zone.js/test/mocha-patch.spec.ts
new file mode 100644
index 0000000000..ef19273cb5
--- /dev/null
+++ b/packages/zone.js/test/mocha-patch.spec.ts
@@ -0,0 +1,104 @@
+/**
+ * @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
+ */
+
+// Extra Mocha-specific typings to make sure typescript compiler is happy
+// Didn't want to add @types/mocha because of duplication in typings-file with @types/jasmine
+declare function suite(description: string, suiteFn: () => void): void;
+ declare function test(description: string, testFn: () => void): void;
+ declare function specify(description: string, testFn: () => void): void;
+ declare function setup(fn: () => void): void; declare function teardown(fn: () => void): void;
+ declare function suiteSetup(fn: () => void): void;
+ declare function suiteTeardown(fn: () => void): void;
+ declare function before(fn: () => void): void; declare function after(fn: () => void): void;
+ //
+
+ import {
+ ifEnvSupports
+ } from './test-util';
+
+ifEnvSupports('Mocha', function() {
+ describe('Mocha BDD-style', () => {
+ let throwOnAsync = false;
+ let beforeEachZone: Zone|null = null;
+ let itZone: Zone|null = null;
+ const syncZone = Zone.current;
+ let beforeZone: Zone|null = null;
+
+ before(() => { beforeZone = Zone.current; });
+
+ try {
+ Zone.current.scheduleMicroTask('dontallow', (): any => null);
+ } catch (e) {
+ throwOnAsync = true;
+ }
+
+ beforeEach(() => beforeEachZone = Zone.current);
+
+ it('should throw on async in describe', () => {
+ expect(Zone.currentTask).toBeTruthy();
+ expect(throwOnAsync).toBe(true);
+ expect(syncZone.name).toEqual('syncTestZone for Mocha.describe');
+ itZone = Zone.current;
+ });
+
+ afterEach(() => {
+ let zone = Zone.current;
+ expect(zone.name).toEqual('ProxyZone');
+ expect(beforeEachZone).toBe(zone);
+ expect(itZone).toBe(zone);
+ });
+
+ after(() => { expect(beforeZone).toBe(Zone.current); });
+ });
+
+ suite('Mocha TDD-style', () => {
+ let testZone: Zone|null = null;
+ let beforeEachZone: Zone|null = null;
+ let suiteSetupZone: Zone|null = null;
+
+ suiteSetup(() => { suiteSetupZone = Zone.current; });
+
+ setup(() => { beforeEachZone = Zone.current; });
+
+ test('should run in Zone with "test"-syntax in TDD-mode', () => {
+ testZone = Zone.current;
+ expect(Zone.currentTask).toBeTruthy();
+ expect(testZone.name).toEqual('ProxyZone');
+ });
+
+ specify('test should run in Zone with "specify"-syntax in TDD-mode', () => {
+ testZone = Zone.current;
+ expect(Zone.currentTask).toBeTruthy();
+ expect(testZone.name).toEqual('ProxyZone');
+ });
+
+ teardown(() => {
+ expect(Zone.current.name).toEqual('ProxyZone');
+ expect(beforeEachZone).toBe(Zone.current);
+ expect(testZone).toBe(Zone.current);
+ });
+
+ suiteTeardown(() => { expect(suiteSetupZone).toBe(Zone.current); });
+ });
+
+ describe('return promise', () => {
+ let log: string[];
+ beforeEach(() => { log = []; });
+
+ it('should wait for promise to resolve', () => {
+ return new Promise((res, _) => {
+ setTimeout(() => {
+ log.push('resolved');
+ res();
+ }, 100);
+ });
+ });
+
+ afterEach(() => { expect(log).toEqual(['resolved']); });
+ });
+})();
\ No newline at end of file
diff --git a/packages/zone.js/test/node-env-setup.ts b/packages/zone.js/test/node-env-setup.ts
new file mode 100644
index 0000000000..77f6606835
--- /dev/null
+++ b/packages/zone.js/test/node-env-setup.ts
@@ -0,0 +1,2 @@
+// Change default symbol prefix for testing to ensure no hard-coded references.
+(global as any)['__Zone_symbol_prefix'] = '__zone_symbol_test__';
diff --git a/packages/zone.js/test/node/Error.spec.ts b/packages/zone.js/test/node/Error.spec.ts
new file mode 100644
index 0000000000..114b54fb83
--- /dev/null
+++ b/packages/zone.js/test/node/Error.spec.ts
@@ -0,0 +1,40 @@
+/**
+ * @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
+ */
+
+describe('ZoneAwareError', () => {
+ // If the environment does not supports stack rewrites, then these tests will fail
+ // and there is no point in running them.
+ if (!(Error as any)['stackRewrite']) return;
+
+ it('should have all properties from NativeError', () => {
+ let obj: any = new Object();
+ Error.captureStackTrace(obj);
+ expect(obj.stack).not.toBeUndefined();
+ });
+
+ it('should support prepareStackTrace', () => {
+ const originalPrepareStackTrace = (Error).prepareStackTrace;
+ (Error).prepareStackTrace = function(error: Error, stack: string) { return stack; };
+ let obj: any = new Object();
+ Error.captureStackTrace(obj);
+ expect(obj.stack[0].getFileName()).not.toBeUndefined();
+ (Error).prepareStackTrace = originalPrepareStackTrace;
+ });
+
+ it('should not add additional stacktrace from Zone when use prepareStackTrace', () => {
+ const originalPrepareStackTrace = (Error).prepareStackTrace;
+ (Error).prepareStackTrace = function(error: Error, stack: string) { return stack; };
+ let obj: any = new Object();
+ Error.captureStackTrace(obj);
+ expect(obj.stack.length).not.toBe(0);
+ obj.stack.forEach(function(st: any) {
+ expect(st.getFunctionName()).not.toEqual('zoneCaptureStackTrace');
+ });
+ (Error).prepareStackTrace = originalPrepareStackTrace;
+ });
+});
diff --git a/packages/zone.js/test/node/console.spec.ts b/packages/zone.js/test/node/console.spec.ts
new file mode 100644
index 0000000000..c41bdb32a4
--- /dev/null
+++ b/packages/zone.js/test/node/console.spec.ts
@@ -0,0 +1,39 @@
+/**
+ * @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
+ */
+describe('node console', () => {
+ const log: string[] = [];
+ const zone = Zone.current.fork({
+ name: 'console',
+ onScheduleTask: function(
+ delegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task) {
+ log.push(task.source);
+ return delegate.scheduleTask(targetZone, task);
+ }
+ });
+
+ beforeEach(() => { log.length = 0; });
+
+ it('console methods should run in root zone', () => {
+ zone.run(() => {
+ console.log('test');
+ console.warn('test');
+ console.error('test');
+ console.info('test');
+ console.trace('test');
+ try {
+ console.assert(false, 'test');
+ } catch (error) {
+ }
+ console.dir('.');
+ console.time('start');
+ console.timeEnd('start');
+ console.debug && console.debug('test');
+ });
+ expect(log).toEqual([]);
+ });
+});
\ No newline at end of file
diff --git a/packages/zone.js/test/node/crypto.spec.ts b/packages/zone.js/test/node/crypto.spec.ts
new file mode 100644
index 0000000000..086e1a4ea5
--- /dev/null
+++ b/packages/zone.js/test/node/crypto.spec.ts
@@ -0,0 +1,63 @@
+/**
+ * @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
+ */
+describe('crypto test', () => {
+ let crypto: any = null;
+
+ try {
+ crypto = require('crypto');
+ } catch (err) {
+ }
+
+ it('crypto randomBytes method should be patched as tasks', (done) => {
+ if (!crypto) {
+ done();
+ return;
+ }
+ const zoneASpec = {
+ name: 'A',
+ onScheduleTask: (delegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task):
+ Task => { return delegate.scheduleTask(targetZone, task); }
+ };
+ const zoneA = Zone.current.fork(zoneASpec);
+ spyOn(zoneASpec, 'onScheduleTask').and.callThrough();
+ zoneA.run(() => {
+ crypto.randomBytes(256, (err: Error, buf: any) => {
+ expect(err).toBeFalsy();
+ expect(zoneASpec.onScheduleTask).toHaveBeenCalled();
+ expect(buf.length).toBe(256);
+ expect(Zone.current.name).toEqual('A');
+ done();
+ });
+ });
+ });
+
+ it('crypto pbkdf2 method should be patched as tasks', (done) => {
+ if (!crypto) {
+ done();
+ return;
+ }
+ const zoneASpec: ZoneSpec = {
+ name: 'A',
+ onScheduleTask: (delegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task):
+ Task => { return delegate.scheduleTask(targetZone, task); }
+ };
+ const zoneA = Zone.current.fork(zoneASpec);
+ spyOn(zoneASpec, 'onScheduleTask').and.callThrough();
+ zoneA.run(() => {
+ crypto.pbkdf2('secret', 'salt', 100000, 512, 'sha512', (err: Error, key: any) => {
+ expect(err).toBeFalsy();
+ expect(zoneASpec.onScheduleTask).toHaveBeenCalled();
+ expect(key.toString('hex'))
+ .toEqual(
+ '3745e482c6e0ade35da10139e797157f4a5da669dad7d5da88ef87e47471cc47ed941c7ad618e827304f083f8707f12b7cfdd5f489b782f10cc269e3c08d59ae04919ee902c99dba309cde75569fbe8e6d5c341d6f2576f6618c589e77911a261ee964e242797e64aeca9a134de5ced37fe2521d35d87303edb55a844c8cf11e3b42b18dbd7add0739ea9b172dc3810f911396fa3956f499415db35b79488d74926cdc0c15c3910bf2e4918f5a8efd7de3d4c314bace50c7a95150339eccd32dda2e15d961ea2c91eddd8b03110135a72b3562f189c2d15568854f9a1844cfa62fb77214f2810a2277fd21be95a794cde78e0fe5267a2c1b0894c7729fc4be378156aeb1cff8a215bb4df12312ba676fe2f270dfc3e2b54d8f9c74dfb531530042a09b226fafbcef45368a1ec75f9224a80f2280f75258ff74a2b9a864d857ede49af6a23af837a1f502a6c32e3537402280bef200d847d8fee42649e6d9a00df952ab2fbefc84ba8927f73137fdfbea81f86088edd4cf329edf3f6982429797143cbd43128777c2da269fadd55d18c7921308c7ad7a5bb85ef8d614e2e8461ea3b7fc2edcf72b85da6828a4198c46000953afb1f3a19ecac0df0d660848a0f89ed3d0e0a82115347c9918bdf16fad479c1de16a6b9798437622acff245e6cf80c9ee9d56cada8523ebb6ff348c73c836e5828761f8dda1dd5ab1633caa39b34');
+ expect(Zone.current.name).toEqual('A');
+ done();
+ });
+ });
+ });
+});
diff --git a/packages/zone.js/test/node/events.spec.ts b/packages/zone.js/test/node/events.spec.ts
new file mode 100644
index 0000000000..0fdc77fbf5
--- /dev/null
+++ b/packages/zone.js/test/node/events.spec.ts
@@ -0,0 +1,188 @@
+/**
+ * @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
+ */
+
+import {EventEmitter} from 'events';
+
+describe('nodejs EventEmitter', () => {
+ let zone: Zone, zoneA: Zone, zoneB: Zone, emitter: EventEmitter, expectZoneACount: number,
+ zoneResults: string[];
+ beforeEach(() => {
+ zone = Zone.current;
+ zoneA = zone.fork({name: 'A'});
+ zoneB = zone.fork({name: 'B'});
+
+ emitter = new EventEmitter();
+ expectZoneACount = 0;
+
+ zoneResults = [];
+ });
+
+ function expectZoneA(value: string) {
+ expectZoneACount++;
+ expect(Zone.current.name).toBe('A');
+ expect(value).toBe('test value');
+ }
+
+ function listenerA() { zoneResults.push('A'); }
+
+ function listenerB() { zoneResults.push('B'); }
+
+ function shouldNotRun() { fail('this listener should not run'); }
+
+ it('should register listeners in the current zone', () => {
+ zoneA.run(() => {
+ emitter.on('test', expectZoneA);
+ emitter.addListener('test', expectZoneA);
+ });
+ zoneB.run(() => emitter.emit('test', 'test value'));
+ expect(expectZoneACount).toBe(2);
+ });
+ it('allows chaining methods', () => {
+ zoneA.run(() => {
+ expect(emitter.on('test', expectZoneA)).toBe(emitter);
+ expect(emitter.addListener('test', expectZoneA)).toBe(emitter);
+ });
+ });
+ it('should remove listeners properly', () => {
+ zoneA.run(() => {
+ emitter.on('test', shouldNotRun);
+ emitter.on('test2', shouldNotRun);
+ emitter.removeListener('test', shouldNotRun);
+ });
+ zoneB.run(() => {
+ emitter.removeListener('test2', shouldNotRun);
+ emitter.emit('test', 'test value');
+ emitter.emit('test2', 'test value');
+ });
+ });
+ it('remove listener should return event emitter', () => {
+ zoneA.run(() => {
+ emitter.on('test', shouldNotRun);
+ expect(emitter.removeListener('test', shouldNotRun)).toEqual(emitter);
+ emitter.emit('test', 'test value');
+ });
+ });
+ it('should return all listeners for an event', () => {
+ zoneA.run(() => { emitter.on('test', expectZoneA); });
+ zoneB.run(() => { emitter.on('test', shouldNotRun); });
+ expect(emitter.listeners('test')).toEqual([expectZoneA, shouldNotRun]);
+ });
+ it('should return empty array when an event has no listeners',
+ () => { zoneA.run(() => { expect(emitter.listeners('test')).toEqual([]); }); });
+ it('should prepend listener by order', () => {
+ zoneA.run(() => {
+ emitter.on('test', listenerA);
+ emitter.on('test', listenerB);
+ expect(emitter.listeners('test')).toEqual([listenerA, listenerB]);
+ emitter.emit('test');
+ expect(zoneResults).toEqual(['A', 'B']);
+ zoneResults = [];
+
+ emitter.removeAllListeners('test');
+
+ emitter.on('test', listenerA);
+ emitter.prependListener('test', listenerB);
+ expect(emitter.listeners('test')).toEqual([listenerB, listenerA]);
+ emitter.emit('test');
+ expect(zoneResults).toEqual(['B', 'A']);
+ });
+ });
+ it('should remove All listeners properly', () => {
+ zoneA.run(() => {
+ emitter.on('test', expectZoneA);
+ emitter.on('test', expectZoneA);
+ emitter.removeAllListeners('test');
+ expect(emitter.listeners('test').length).toEqual(0);
+ });
+ });
+ it('remove All listeners should return event emitter', () => {
+ zoneA.run(() => {
+ emitter.on('test', expectZoneA);
+ emitter.on('test', expectZoneA);
+ expect(emitter.removeAllListeners('test')).toEqual(emitter);
+ expect(emitter.listeners('test').length).toEqual(0);
+ });
+ });
+ it('should remove All listeners properly even without a type parameter', () => {
+ zoneA.run(() => {
+ emitter.on('test', shouldNotRun);
+ emitter.on('test1', shouldNotRun);
+ emitter.removeAllListeners();
+ expect(emitter.listeners('test').length).toEqual(0);
+ expect(emitter.listeners('test1').length).toEqual(0);
+ });
+ });
+ it('should remove once listener after emit', () => {
+ zoneA.run(() => {
+ emitter.once('test', expectZoneA);
+ emitter.emit('test', 'test value');
+ expect(emitter.listeners('test').length).toEqual(0);
+ });
+ });
+ it('should remove once listener properly before listener triggered', () => {
+ zoneA.run(() => {
+ emitter.once('test', shouldNotRun);
+ emitter.removeListener('test', shouldNotRun);
+ emitter.emit('test');
+ });
+ });
+ it('should trigger removeListener when remove listener', () => {
+ zoneA.run(() => {
+ emitter.on('removeListener', function(type: string, handler: any) {
+ zoneResults.push('remove' + type);
+ });
+ emitter.on(
+ 'newListener', function(type: string, handler: any) { zoneResults.push('new' + type); });
+ emitter.on('test', shouldNotRun);
+ emitter.removeListener('test', shouldNotRun);
+ expect(zoneResults).toEqual(['newtest', 'removetest']);
+ });
+ });
+ it('should trigger removeListener when remove all listeners with eventname ', () => {
+ zoneA.run(() => {
+ emitter.on('removeListener', function(type: string, handler: any) {
+ zoneResults.push('remove' + type);
+ });
+ emitter.on('test', shouldNotRun);
+ emitter.on('test1', expectZoneA);
+ emitter.removeAllListeners('test');
+ expect(zoneResults).toEqual(['removetest']);
+ expect(emitter.listeners('removeListener').length).toBe(1);
+ });
+ });
+ it('should trigger removeListener when remove all listeners without eventname', () => {
+ zoneA.run(() => {
+ emitter.on('removeListener', function(type: string, handler: any) {
+ zoneResults.push('remove' + type);
+ });
+ emitter.on('test', shouldNotRun);
+ emitter.on('test1', shouldNotRun);
+ emitter.removeAllListeners();
+ expect(zoneResults).toEqual(['removetest', 'removetest1']);
+ expect(emitter.listeners('test').length).toBe(0);
+ expect(emitter.listeners('test1').length).toBe(0);
+ expect(emitter.listeners('removeListener').length).toBe(0);
+ });
+ });
+ it('should not enter endless loop when register uncaughtException to process', () => {
+ require('domain');
+ zoneA.run(() => { process.on('uncaughtException', function() {}); });
+ });
+ it('should be able to addEventListener with symbol eventName', () => {
+ zoneA.run(() => {
+ const testSymbol = Symbol('test');
+ const test1Symbol = Symbol('test1');
+ emitter.on(testSymbol, expectZoneA);
+ emitter.on(test1Symbol, shouldNotRun);
+ emitter.removeListener(test1Symbol, shouldNotRun);
+ expect(emitter.listeners(testSymbol).length).toBe(1);
+ expect(emitter.listeners(test1Symbol).length).toBe(0);
+ emitter.emit(testSymbol, 'test value');
+ });
+ });
+});
diff --git a/packages/zone.js/test/node/fs.spec.ts b/packages/zone.js/test/node/fs.spec.ts
new file mode 100644
index 0000000000..e6f265c627
--- /dev/null
+++ b/packages/zone.js/test/node/fs.spec.ts
@@ -0,0 +1,145 @@
+/**
+ * @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
+ */
+
+import {closeSync, exists, fstatSync, openSync, read, unlink, unlinkSync, unwatchFile, watch, watchFile, write, writeFile} from 'fs';
+import * as util from 'util';
+
+describe('nodejs file system', () => {
+ describe('async method patch test', () => {
+ it('has patched exists()', (done) => {
+ const zoneA = Zone.current.fork({name: 'A'});
+ zoneA.run(() => {
+ exists('testfile', (_) => {
+ expect(Zone.current.name).toBe(zoneA.name);
+ done();
+ });
+ });
+ });
+
+ it('has patched exists as macroTask', (done) => {
+ const zoneASpec = {
+ name: 'A',
+ onScheduleTask: (delegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task):
+ Task => { return delegate.scheduleTask(targetZone, task); }
+ };
+ const zoneA = Zone.current.fork(zoneASpec);
+ spyOn(zoneASpec, 'onScheduleTask').and.callThrough();
+ zoneA.run(() => {
+ exists('testfile', (_) => {
+ expect(zoneASpec.onScheduleTask).toHaveBeenCalled();
+ done();
+ });
+ });
+ });
+ });
+
+ describe('watcher related methods test', () => {
+ const zoneASpec = {
+ name: 'A',
+ onScheduleTask: (delegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task):
+ Task => { return delegate.scheduleTask(targetZone, task); }
+ };
+
+ it('fs.watch has been patched as eventTask', (done) => {
+ spyOn(zoneASpec, 'onScheduleTask').and.callThrough();
+ const zoneA = Zone.current.fork(zoneASpec);
+ zoneA.run(() => {
+ writeFile('testfile', 'test content', () => {
+ const watcher = watch('testfile', (eventType, filename) => {
+ expect(filename).toEqual('testfile');
+ expect(eventType).toEqual('change');
+ expect(zoneASpec.onScheduleTask).toHaveBeenCalled();
+ expect(Zone.current.name).toBe('A');
+ watcher.close();
+ unlink('testfile', () => { done(); });
+ });
+ writeFile('testfile', 'test new content', () => {});
+ });
+ });
+ });
+
+ it('fs.watchFile has been patched as eventTask', (done) => {
+ spyOn(zoneASpec, 'onScheduleTask').and.callThrough();
+ const zoneA = Zone.current.fork(zoneASpec);
+ zoneA.run(() => {
+ writeFile('testfile', 'test content', () => {
+ watchFile('testfile', {persistent: false, interval: 1000}, (curr, prev) => {
+ expect(curr.size).toBe(16);
+ expect(prev.size).toBe(12);
+ expect(zoneASpec.onScheduleTask).toHaveBeenCalled();
+ expect(Zone.current.name).toBe('A');
+ unwatchFile('testfile');
+ unlink('testfile', () => { done(); });
+ });
+ writeFile('testfile', 'test new content', () => {});
+ });
+ });
+ });
+ });
+});
+
+describe('util.promisify', () => {
+ it('fs.exists should work with util.promisify', (done: DoneFn) => {
+ const promisifyExists = util.promisify(exists);
+ promisifyExists(__filename)
+ .then(
+ r => {
+ expect(r).toBe(true);
+ done();
+ },
+ err => { fail(`should not be here with error: ${err}`); });
+ });
+
+ it('fs.read should work with util.promisify', (done: DoneFn) => {
+ const promisifyRead = util.promisify(read);
+ const fd = openSync(__filename, 'r');
+ const stats = fstatSync(fd);
+ const bufferSize = stats.size;
+ const chunkSize = 512;
+ const buffer = new Buffer(bufferSize);
+ let bytesRead = 0;
+ // fd, buffer, offset, length, position, callback
+ promisifyRead(fd, buffer, bytesRead, chunkSize, bytesRead)
+ .then(
+ (value) => {
+ expect(value.bytesRead).toBe(chunkSize);
+ closeSync(fd);
+ done();
+ },
+ err => {
+ closeSync(fd);
+ fail(`should not be here with error: ${error}.`);
+ });
+ });
+
+ it('fs.write should work with util.promisify', (done: DoneFn) => {
+ const promisifyWrite = util.promisify(write);
+ const dest = __filename + 'write';
+ const fd = openSync(dest, 'a');
+ const stats = fstatSync(fd);
+ const chunkSize = 512;
+ const buffer = new Buffer(chunkSize);
+ for (let i = 0; i < chunkSize; i++) {
+ buffer[i] = 0;
+ }
+ // fd, buffer, offset, length, position, callback
+ promisifyWrite(fd, buffer, 0, chunkSize, 0)
+ .then(
+ (value) => {
+ expect(value.bytesWritten).toBe(chunkSize);
+ closeSync(fd);
+ unlinkSync(dest);
+ done();
+ },
+ err => {
+ closeSync(fd);
+ unlinkSync(dest);
+ fail(`should not be here with error: ${error}.`);
+ });
+ });
+});
\ No newline at end of file
diff --git a/packages/zone.js/test/node/http.spec.ts b/packages/zone.js/test/node/http.spec.ts
new file mode 100644
index 0000000000..22022aa1ba
--- /dev/null
+++ b/packages/zone.js/test/node/http.spec.ts
@@ -0,0 +1,31 @@
+/**
+ * @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 http = require('http');
+describe('http test', () => {
+ it('http.request should be patched as eventTask', (done) => {
+ const server = http.createServer((req: any, res: any) => { res.end(); });
+ server.listen(9999, () => {
+ const zoneASpec = {
+ name: 'A',
+ onScheduleTask: (delegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task):
+ Task => { return delegate.scheduleTask(targetZone, task); }
+ };
+ const zoneA = Zone.current.fork(zoneASpec);
+ spyOn(zoneASpec, 'onScheduleTask').and.callThrough();
+ zoneA.run(() => {
+ const req =
+ http.request({hostname: 'localhost', port: '9999', method: 'GET'}, (res: any) => {
+ expect(Zone.current.name).toEqual('A');
+ expect(zoneASpec.onScheduleTask).toHaveBeenCalled();
+ server.close(() => { done(); });
+ });
+ req.end();
+ });
+ });
+ });
+});
diff --git a/packages/zone.js/test/node/process.spec.ts b/packages/zone.js/test/node/process.spec.ts
new file mode 100644
index 0000000000..cbc91a1f97
--- /dev/null
+++ b/packages/zone.js/test/node/process.spec.ts
@@ -0,0 +1,116 @@
+/**
+ * @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
+ */
+
+import {zoneSymbol} from '../../lib/common/utils';
+
+describe('process related test', () => {
+ let zoneA: Zone, result: any[];
+ beforeEach(() => {
+ zoneA = Zone.current.fork({name: 'zoneA'});
+ result = [];
+ });
+ it('process.nextTick callback should in zone', (done) => {
+ zoneA.run(function() {
+ process.nextTick(() => {
+ expect(Zone.current.name).toEqual('zoneA');
+ done();
+ });
+ });
+ });
+ it('process.nextTick should be executed before macroTask and promise', (done) => {
+ zoneA.run(function() {
+ setTimeout(() => { result.push('timeout'); }, 0);
+ process.nextTick(() => { result.push('tick'); });
+ setTimeout(() => {
+ expect(result).toEqual(['tick', 'timeout']);
+ done();
+ });
+ });
+ });
+ it('process.nextTick should be treated as microTask', (done) => {
+ let zoneTick = Zone.current.fork({
+ name: 'zoneTick',
+ onScheduleTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
+ task: Task): Task => {
+ result.push({callback: 'scheduleTask', targetZone: targetZone.name, task: task.source});
+ return parentZoneDelegate.scheduleTask(targetZone, task);
+ },
+ onInvokeTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
+ task: Task, applyThis?: any, applyArgs?: any): any => {
+ result.push({callback: 'invokeTask', targetZone: targetZone.name, task: task.source});
+ return parentZoneDelegate.invokeTask(targetZone, task, applyThis, applyArgs);
+ }
+ });
+ zoneTick.run(() => { process.nextTick(() => { result.push('tick'); }); });
+ setTimeout(() => {
+ expect(result.length).toBe(3);
+ expect(result[0]).toEqual(
+ {callback: 'scheduleTask', targetZone: 'zoneTick', task: 'process.nextTick'});
+ expect(result[1]).toEqual(
+ {callback: 'invokeTask', targetZone: 'zoneTick', task: 'process.nextTick'});
+ done();
+ });
+ });
+
+ it('should support process.on(unhandledRejection)', function(done) {
+ const hookSpy = jasmine.createSpy('hook');
+ (Zone as any)[zoneSymbol('ignoreConsoleErrorUncaughtError')] = true;
+ Zone.current.fork({name: 'promise'}).run(function() {
+ const listener = function(reason: any, promise: any) {
+ hookSpy(promise, reason.message);
+ process.removeListener('unhandledRejection', listener);
+ };
+ process.on('unhandledRejection', listener);
+ const p = new Promise((resolve, reject) => { throw new Error('promise error'); });
+
+ setTimeout(function() {
+ expect(hookSpy).toHaveBeenCalledWith(p, 'promise error');
+ done();
+ }, 10);
+ });
+ });
+
+ it('should support process.on(rejectionHandled)', function(done) {
+ (Zone as any)[zoneSymbol('ignoreConsoleErrorUncaughtError')] = true;
+ Zone.current.fork({name: 'promise'}).run(function() {
+ const listener = function(promise: any) {
+ expect(promise).toEqual(p);
+ process.removeListener('rejectionHandled', listener);
+ done();
+ };
+ process.on('rejectionHandled', listener);
+ const p = new Promise((resolve, reject) => { throw new Error('promise error'); });
+
+ setTimeout(function() { p.catch(reason => {}); }, 10);
+ });
+ });
+
+ it('should support multiple process.on(unhandledRejection)', function(done) {
+ const hookSpy = jasmine.createSpy('hook');
+ (Zone as any)[zoneSymbol('ignoreConsoleErrorUncaughtError')] = true;
+ Zone.current.fork({name: 'promise'}).run(function() {
+ const listener1 = function(reason: any, promise: any) {
+ hookSpy(promise, reason.message);
+ process.removeListener('unhandledRejection', listener1);
+ };
+ const listener2 = function(reason: any, promise: any) {
+ hookSpy(promise, reason.message);
+ process.removeListener('unhandledRejection', listener2);
+ };
+ process.on('unhandledRejection', listener1);
+ process.on('unhandledRejection', listener2);
+ const p = new Promise((resolve, reject) => { throw new Error('promise error'); });
+
+ setTimeout(function() {
+ expect(hookSpy.calls.count()).toBe(2);
+ expect(hookSpy.calls.allArgs()).toEqual([[p, 'promise error'], [p, 'promise error']]);
+ done();
+ }, 10);
+ });
+ });
+});
\ No newline at end of file
diff --git a/packages/zone.js/test/node/timer.spec.ts b/packages/zone.js/test/node/timer.spec.ts
new file mode 100644
index 0000000000..59a9d7e0ad
--- /dev/null
+++ b/packages/zone.js/test/node/timer.spec.ts
@@ -0,0 +1,31 @@
+/**
+ * @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
+ */
+import {promisify} from 'util';
+
+describe('node timer', () => {
+ it('util.promisify should work with setTimeout', (done: DoneFn) => {
+ const setTimeoutPromise = promisify(setTimeout);
+ setTimeoutPromise(50, 'value')
+ .then(
+ value => {
+ expect(value).toEqual('value');
+ done();
+ },
+ error => { fail(`should not be here with error: ${error}.`); });
+ });
+
+ it('util.promisify should work with setImmediate', (done: DoneFn) => {
+ const setImmediatePromise = promisify(setImmediate);
+ setImmediatePromise('value').then(
+ value => {
+ expect(value).toEqual('value');
+ done();
+ },
+ error => { fail(`should not be here with error: ${error}.`); });
+ });
+});
\ No newline at end of file
diff --git a/packages/zone.js/test/node_bluebird_entry_point.ts b/packages/zone.js/test/node_bluebird_entry_point.ts
new file mode 100644
index 0000000000..ae0da24027
--- /dev/null
+++ b/packages/zone.js/test/node_bluebird_entry_point.ts
@@ -0,0 +1,54 @@
+/**
+ * @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
+ */
+
+// Must be loaded before zone loads, so that zone can detect WTF.
+import './test_fake_polyfill';
+
+// Setup tests for Zone without microtask support
+import '../lib/zone';
+import '../lib/common/promise';
+import '../lib/common/to-string';
+import '../lib/node/node';
+// Setup test environment
+require('@bazel/jasmine').boot();
+import './test-env-setup-jasmine';
+import './wtf_mock';
+
+import '../lib/zone-spec/async-test';
+import '../lib/zone-spec/fake-async-test';
+import '../lib/zone-spec/long-stack-trace';
+import '../lib/zone-spec/proxy';
+import '../lib/zone-spec/sync-test';
+import '../lib/zone-spec/task-tracking';
+import '../lib/zone-spec/wtf';
+import '../lib/rxjs/rxjs';
+
+import '../lib/testing/promise-testing';
+
+const globalErrors = (jasmine as any).GlobalErrors;
+const symbol = Zone.__symbol__;
+if (globalErrors && !(jasmine as any)[symbol('GlobalErrors')]) {
+ (jasmine as any)[symbol('GlobalErrors')] = globalErrors;
+ (jasmine as any).GlobalErrors = function() {
+ const instance = new globalErrors();
+ const originalInstall = instance.install;
+ if (originalInstall && !instance[symbol('install')]) {
+ instance[symbol('install')] = originalInstall;
+ instance.install = function() {
+ const originalHandlers = process.listeners('unhandledRejection');
+ const r = originalInstall.apply(this, arguments);
+ process.removeAllListeners('unhandledRejection');
+ if (originalHandlers) {
+ originalHandlers.forEach(h => process.on('unhandledRejection', h));
+ }
+ return r;
+ };
+ }
+ return instance;
+ };
+}
diff --git a/packages/zone.js/test/node_entry_point.ts b/packages/zone.js/test/node_entry_point.ts
new file mode 100644
index 0000000000..e63e7595d6
--- /dev/null
+++ b/packages/zone.js/test/node_entry_point.ts
@@ -0,0 +1,37 @@
+/**
+ * @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
+ */
+/**
+ * @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
+ */
+
+// Must be loaded before zone loads, so that zone can detect WTF.
+import './node-env-setup';
+import './test_fake_polyfill';
+
+// Setup tests for Zone without microtask support
+import '../lib/node/rollup-main';
+
+require('@bazel/jasmine').boot();
+// Zone symbol prefix is set to '__zone_symbol2__' in node-env-setup.ts.
+import './test-env-setup-jasmine';
+if (typeof global !== 'undefined' &&
+ (global as any)['__zone_symbol_test__fakeAsyncAutoFakeAsyncWhenClockPatched'] !== false) {
+ (global as any)['__zone_symbol_test__fakeAsyncAutoFakeAsyncWhenClockPatched'] = true;
+}
+
+import './wtf_mock';
+import '../lib/testing/zone-testing';
+import '../lib/zone-spec/task-tracking';
+import '../lib/zone-spec/wtf';
+import '../lib/rxjs/rxjs';
+import '../lib/rxjs/rxjs-fake-async';
+import '../lib/jasmine/jasmine';
diff --git a/packages/zone.js/test/node_entry_point_no_patch_clock.ts b/packages/zone.js/test/node_entry_point_no_patch_clock.ts
new file mode 100644
index 0000000000..a91766407f
--- /dev/null
+++ b/packages/zone.js/test/node_entry_point_no_patch_clock.ts
@@ -0,0 +1,36 @@
+/**
+ * @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
+ */
+/**
+ * @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
+ */
+
+// Must be loaded before zone loads, so that zone can detect WTF.
+import './node-env-setup';
+import './test_fake_polyfill';
+
+// Setup tests for Zone without microtask support
+import '../lib/node/rollup-main';
+require('@bazel/jasmine').boot();
+import './test-env-setup-jasmine-no-patch-clock';
+// Zone symbol prefix is set to '__zone_symbol2__' in node-env-setup.ts.
+if (typeof global !== 'undefined' &&
+ (global as any)['__zone_symbol_test__fakeAsyncAutoFakeAsyncWhenClockPatched'] !== false) {
+ (global as any)['__zone_symbol_test__fakeAsyncAutoFakeAsyncWhenClockPatched'] = true;
+}
+
+import './wtf_mock';
+import '../lib/testing/zone-testing';
+import '../lib/zone-spec/task-tracking';
+import '../lib/zone-spec/wtf';
+import '../lib/rxjs/rxjs';
+import '../lib/rxjs/rxjs-fake-async';
+import '../lib/jasmine/jasmine';
diff --git a/packages/zone.js/test/node_error_disable_policy_entry_point.ts b/packages/zone.js/test/node_error_disable_policy_entry_point.ts
new file mode 100644
index 0000000000..841039900d
--- /dev/null
+++ b/packages/zone.js/test/node_error_disable_policy_entry_point.ts
@@ -0,0 +1,11 @@
+/**
+ * @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
+ */
+
+process.env['errorpolicy'] = (global as any)['__Zone_Error_BlacklistedStackFrames_policy'] =
+ 'disable';
+import './node_error_entry_point';
diff --git a/packages/zone.js/test/node_error_entry_point.ts b/packages/zone.js/test/node_error_entry_point.ts
new file mode 100644
index 0000000000..5acf7f2f16
--- /dev/null
+++ b/packages/zone.js/test/node_error_entry_point.ts
@@ -0,0 +1,35 @@
+/**
+ * @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
+ */
+
+// Must be loaded before zone loads, so that zone can detect WTF.
+import './test_fake_polyfill';
+
+// Setup tests for Zone without microtask support
+import '../lib/zone';
+import '../lib/common/promise';
+import '../lib/common/to-string';
+
+process.env['errorpolicy'] = (global as any)['__Zone_Error_BlacklistedStackFrames_policy'] =
+ 'disable';
+// Setup test environment
+require('@bazel/jasmine').boot();
+import './test-env-setup-jasmine';
+
+import './wtf_mock';
+import '../lib/common/error-rewrite';
+import '../lib/node/node';
+import '../lib/zone-spec/async-test';
+import '../lib/zone-spec/fake-async-test';
+import '../lib/zone-spec/long-stack-trace';
+import '../lib/zone-spec/proxy';
+import '../lib/zone-spec/sync-test';
+import '../lib/zone-spec/task-tracking';
+import '../lib/zone-spec/wtf';
+import '../lib/rxjs/rxjs';
+
+import '../lib/testing/promise-testing';
diff --git a/packages/zone.js/test/node_error_lazy_policy_entry_point.ts b/packages/zone.js/test/node_error_lazy_policy_entry_point.ts
new file mode 100644
index 0000000000..61b5e56093
--- /dev/null
+++ b/packages/zone.js/test/node_error_lazy_policy_entry_point.ts
@@ -0,0 +1,10 @@
+/**
+ * @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
+ */
+
+process.env['errorpolicy'] = (global as any)['__Zone_Error_BlacklistedStackFrames_policy'] = 'lazy';
+import './node_error_entry_point';
diff --git a/packages/zone.js/test/node_tests.ts b/packages/zone.js/test/node_tests.ts
new file mode 100644
index 0000000000..1366ac573d
--- /dev/null
+++ b/packages/zone.js/test/node_tests.ts
@@ -0,0 +1,16 @@
+/**
+ * @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
+ */
+
+import './node/events.spec';
+import './node/fs.spec';
+import './node/process.spec';
+import './node/Error.spec';
+import './node/crypto.spec';
+import './node/http.spec';
+import './node/console.spec';
+import './node/timer.spec';
diff --git a/packages/zone.js/test/npm_package/npm_package.spec.ts b/packages/zone.js/test/npm_package/npm_package.spec.ts
new file mode 100644
index 0000000000..1c9de06332
--- /dev/null
+++ b/packages/zone.js/test/npm_package/npm_package.spec.ts
@@ -0,0 +1,143 @@
+/**
+ * @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
+ */
+import * as path from 'path';
+import * as shx from 'shelljs';
+
+describe('Zone.js npm_package', () => {
+ beforeEach(
+ () => {shx.cd(
+ path.dirname(require.resolve('angular/packages/zone.js/npm_package/package.json')))});
+ describe('misc root files', () => {
+ describe('README.md', () => {
+ it('should have a README.md file with basic info',
+ () => { expect(shx.cat('README.md')).toContain(`Zone`); });
+ });
+ });
+
+ describe('primary entry-point', () => {
+ const packageJson = 'package.json';
+
+ it('should have a package.json file',
+ () => { expect(shx.grep('"name":', packageJson)).toContain(`zone.js`); });
+
+ it('should contain correct version number with the PLACEHOLDER string replaced', () => {
+ expect(shx.grep('"version":', packageJson)).toMatch(/\d+\.\d+\.\d+(?!-PLACEHOLDER)/);
+ });
+
+ it('should contain module resolution mappings',
+ () => { expect(shx.grep('"main":', packageJson)).toContain(`dist/zone-node.js`); });
+ });
+
+ describe('check dist folder', () => {
+ beforeEach(() => { shx.cd('./dist'); });
+ afterEach(() => { shx.cd('../'); });
+ describe('typescript support', () => {
+ it('should have an zone.js.d.ts file',
+ () => { expect(shx.cat('zone.js.d.ts')).toContain('declare const'); });
+ });
+
+ describe('closure', () => {
+ it('should contain externs',
+ () => { expect(shx.cat('zone_externs.js')).toContain('Externs for zone.js'); });
+ });
+
+ describe('es5', () => {
+ it('zone.js(es5) should not contain es6 spread code',
+ () => { expect(shx.cat('zone.js')).not.toContain('let value of values'); });
+ });
+
+ describe('es2015', () => {
+ it('zone-evergreen.js(es2015) should contain es6 code',
+ () => { expect(shx.cat('zone-evergreen.js')).toContain('let value of values'); });
+ });
+
+ describe('dist file list', () => {
+ it('should contain all files', () => {
+ const list = shx.ls('./').stdout.split('\n').sort().slice(1);
+ const expected = [
+ 'async-test.js',
+ 'async-test.min.js',
+ 'fake-async-test.js',
+ 'fake-async-test.min.js',
+ 'jasmine-patch.js',
+ 'jasmine-patch.min.js',
+ 'long-stack-trace-zone.js',
+ 'long-stack-trace-zone.min.js',
+ 'mocha-patch.js',
+ 'mocha-patch.min.js',
+ 'proxy.js',
+ 'proxy.min.js',
+ 'sync-test.js',
+ 'sync-test.min.js',
+ 'task-tracking.js',
+ 'task-tracking.min.js',
+ 'webapis-media-query.js',
+ 'webapis-media-query.min.js',
+ 'webapis-notification.js',
+ 'webapis-notification.min.js',
+ 'webapis-rtc-peer-connection.js',
+ 'webapis-rtc-peer-connection.min.js',
+ 'webapis-shadydom.js',
+ 'webapis-shadydom.min.js',
+ 'wtf.js',
+ 'wtf.min.js',
+ 'zone_externs.js',
+ 'zone-bluebird.js',
+ 'zone-bluebird.min.js',
+ 'zone-error.js',
+ 'zone-error.min.js',
+ 'zone-evergreen.js',
+ 'zone-evergreen.min.js',
+ 'zone-evergreen-testing-bundle.js',
+ 'zone-evergreen-testing-bundle.min.js',
+ 'zone-legacy.js',
+ 'zone-legacy.min.js',
+ 'zone-mix.js',
+ 'zone-mix.min.js',
+ 'zone-node.js',
+ 'zone-node.min.js',
+ 'zone-patch-canvas.js',
+ 'zone-patch-canvas.min.js',
+ 'zone-patch-cordova.js',
+ 'zone-patch-cordova.min.js',
+ 'zone-patch-electron.js',
+ 'zone-patch-electron.min.js',
+ 'zone-patch-fetch.js',
+ 'zone-patch-fetch.min.js',
+ 'zone-patch-jsonp.js',
+ 'zone-patch-jsonp.min.js',
+ 'zone-patch-promise-test.js',
+ 'zone-patch-promise-test.min.js',
+ 'zone-patch-resize-observer.js',
+ 'zone-patch-resize-observer.min.js',
+ 'zone-patch-rxjs-fake-async.js',
+ 'zone-patch-rxjs-fake-async.min.js',
+ 'zone-patch-rxjs.js',
+ 'zone-patch-rxjs.min.js',
+ 'zone-patch-socket-io.js',
+ 'zone-patch-socket-io.min.js',
+ 'zone-patch-user-media.js',
+ 'zone-patch-user-media.min.js',
+ 'zone-testing-bundle.js',
+ 'zone-testing-bundle.min.js',
+ 'zone-testing-node-bundle.js',
+ 'zone-testing-node-bundle.min.js',
+ 'zone-testing.js',
+ 'zone-testing.min.js',
+ 'zone.js',
+ 'zone.js.d.ts',
+ 'zone.min.js',
+ ].sort();
+ expect(list.length).toBe(expected.length);
+ for (let i = 0; i < list.length; i++) {
+ expect(list[i]).toEqual(expected[i]);
+ }
+ });
+ });
+ });
+});
diff --git a/packages/zone.js/test/patch/IndexedDB.spec.js b/packages/zone.js/test/patch/IndexedDB.spec.js
new file mode 100644
index 0000000000..f563de0997
--- /dev/null
+++ b/packages/zone.js/test/patch/IndexedDB.spec.js
@@ -0,0 +1,133 @@
+/**
+ * @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
+ */
+
+'use strict';
+
+describe(
+ 'IndexedDB', ifEnvSupports('IDBDatabase', function() {
+ var testZone = zone.fork();
+ var db;
+
+ beforeEach(function(done) {
+ var openRequest = indexedDB.open('_zone_testdb');
+ openRequest.onupgradeneeded = function(event) {
+ db = event.target.result;
+ var objectStore = db.createObjectStore('test-object-store', {keyPath: 'key'});
+ objectStore.createIndex('key', 'key', {unique: true});
+ objectStore.createIndex('data', 'data', {unique: false});
+
+ objectStore.transaction.oncomplete = function() {
+ var testStore =
+ db.transaction('test-object-store', 'readwrite').objectStore('test-object-store');
+ testStore.add({key: 1, data: 'Test data'});
+ testStore.transaction.oncomplete = function() { done(); }
+ };
+ };
+ });
+
+ afterEach(function(done) {
+ db.close();
+
+ var openRequest = indexedDB.deleteDatabase('_zone_testdb');
+ openRequest.onsuccess = function(event) { done(); };
+ });
+
+ describe('IDBRequest', function() {
+ it('should bind EventTarget.addEventListener', function(done) {
+ testZone.run(function() {
+ db.transaction('test-object-store')
+ .objectStore('test-object-store')
+ .get(1)
+ .addEventListener('success', function(event) {
+ expect(zone).toBeDirectChildOf(testZone);
+ expect(event.target.result.data).toBe('Test data');
+ done();
+ });
+ });
+ });
+
+ it('should bind onEventType listeners', function(done) {
+ testZone.run(function() {
+ db.transaction('test-object-store').objectStore('test-object-store').get(1).onsuccess =
+ function(event) {
+ expect(zone).toBeDirectChildOf(testZone);
+ expect(event.target.result.data).toBe('Test data');
+ done();
+ };
+ });
+ });
+ });
+
+ describe('IDBCursor', function() {
+ it('should bind EventTarget.addEventListener', function(done) {
+ testZone.run(function() {
+ db.transaction('test-object-store')
+ .objectStore('test-object-store')
+ .openCursor()
+ .addEventListener('success', function(event) {
+ var cursor = event.target.result;
+ if (cursor) {
+ expect(zone).toBeDirectChildOf(testZone);
+ expect(cursor.value.data).toBe('Test data');
+ done();
+ } else {
+ throw 'Error while reading cursor!';
+ }
+ });
+ });
+ });
+
+ it('should bind onEventType listeners', function(done) {
+ testZone.run(function() {
+ db.transaction('test-object-store')
+ .objectStore('test-object-store')
+ .openCursor()
+ .onsuccess = function(event) {
+ var cursor = event.target.result;
+ if (cursor) {
+ expect(zone).toBeDirectChildOf(testZone);
+ expect(cursor.value.data).toBe('Test data');
+ done();
+ } else {
+ throw 'Error while reading cursor!';
+ }
+ };
+ });
+ });
+ });
+
+ describe('IDBIndex', function() {
+ it('should bind EventTarget.addEventListener', function(done) {
+ testZone.run(function() {
+ db.transaction('test-object-store')
+ .objectStore('test-object-store')
+ .index('data')
+ .get('Test data')
+ .addEventListener('success', function(event) {
+ expect(zone).toBeDirectChildOf(testZone);
+ expect(event.target.result.key).toBe(1);
+ done();
+ });
+ });
+ });
+
+ it('should bind onEventType listeners', function(done) {
+ testZone.run(function() {
+ db.transaction('test-object-store')
+ .objectStore('test-object-store')
+ .index('data')
+ .get('Test data')
+ .onsuccess = function(event) {
+ expect(zone).toBeDirectChildOf(testZone);
+ expect(event.target.result.key).toBe(1);
+ done();
+ };
+ });
+ });
+ });
+ }));
\ No newline at end of file
diff --git a/packages/zone.js/test/performance/eventTarget.js b/packages/zone.js/test/performance/eventTarget.js
new file mode 100644
index 0000000000..b8e56dd122
--- /dev/null
+++ b/packages/zone.js/test/performance/eventTarget.js
@@ -0,0 +1,80 @@
+/**
+ * @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
+ */
+(function(_global) {
+ var testRunner = _global['__zone_symbol__testRunner'];
+ var mark = _global['__zone_symbol__mark'];
+ var measure = _global['__zone_symbol__measure'];
+ var zone = _global['__zone_symbol__callbackZone'];
+ var button;
+ var testTarget = {
+ title: 'addEventListener',
+ times: 10,
+ before: function() {
+ button = document.createElement('button');
+ document.body.appendChild(button);
+ _global['__zone_symbol__callbackContext'].measureName = 'addEventListener_callback';
+ _global['__zone_symbol__callbackContext'].type = 'eventTask';
+ _global['__zone_symbol__callbackContext'].source = 'addEventListener';
+ },
+ after: function() {
+ document.body.removeChild(button);
+ button = null;
+ },
+ apis: [
+ {
+ supportClear: true,
+ method: 'addEventListener',
+ nativeMethod: '__zone_symbol__addEventListener',
+ clearMethod: 'removeEventListener',
+ nativeClearMethod: '__zone_symbol__removeEventListener',
+ run: function() {
+ var listener = function() {};
+ button.addEventListener('click', listener);
+ return listener;
+ },
+ runClear: function(timerId) { return button.removeEventListener('click', timerId); },
+ nativeRun: function() {
+ var listener = function() {};
+ button['__zone_symbol__addEventListener']('click', listener);
+ return listener;
+ },
+ nativeRunClear: function(timerId) {
+ return button['__zone_symbol__removeEventListener']('click', timerId);
+ }
+ },
+ {
+ isCallback: true,
+ supportClear: false,
+ method: 'addEventListener_callback',
+ nativeMethod: 'native_addEventListener_callback',
+ run: function() {
+ var listener = function() {};
+ zone.run(function() { button.addEventListener('click', listener); });
+ var event = document.createEvent('Event');
+ event.initEvent('click', true, true);
+ button.dispatchEvent(event);
+ button.removeEventListener('click', listener);
+ },
+ nativeRun: function() {
+ var func = function() {};
+ var listener = function() {
+ mark('native_addEventListener_callback');
+ func.apply(this, arguments);
+ measure('native_addEventListener_callback', 'native_addEventListener_callback');
+ };
+ button['__zone_symbol__addEventListener']('click', listener);
+ var event = document.createEvent('Event');
+ event.initEvent('click', true, true);
+ button.dispatchEvent(event);
+ button['__zone_symbol__removeEventListener']('click', listener);
+ }
+ }
+ ],
+ };
+ return testRunner(testTarget);
+}(typeof window === 'undefined' ? global : window));
diff --git a/packages/zone.js/test/performance/performance.html b/packages/zone.js/test/performance/performance.html
new file mode 100644
index 0000000000..37d5b59431
--- /dev/null
+++ b/packages/zone.js/test/performance/performance.html
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Performance Bencnhmark of Zone.js vs Native Delegate!
+
+
+
+
+
+
diff --git a/packages/zone.js/test/performance/performance_setup.js b/packages/zone.js/test/performance/performance_setup.js
new file mode 100644
index 0000000000..b9f6d3db47
--- /dev/null
+++ b/packages/zone.js/test/performance/performance_setup.js
@@ -0,0 +1,284 @@
+/**
+ * @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
+ */
+(function(_global) {
+ var allTasks = _global['__zone_symbol__performance_tasks'];
+ if (!allTasks) {
+ allTasks = _global['__zone_symbol__performance_tasks'] = [];
+ }
+
+ var mark = _global['__zone_symbol__mark'] = function(name) {
+ performance && performance['mark'] && performance['mark'](name);
+ };
+
+ var measure = _global['__zone_symbol__measure'] = function(name, label) {
+ performance && performance['measure'] && performance['measure'](name, label);
+ };
+
+ var getEntries = _global['__zone_symbol__getEntries'] = function() {
+ performance && performance['getEntries'] && performance['getEntries']();
+ };
+
+ var getEntriesByName = _global['__zone_symbol__getEntriesByName'] = function(name) {
+ return performance && performance['getEntriesByName'] && performance['getEntriesByName'](name);
+ };
+
+ var clearMarks = _global['__zone_symbol__clearMarks'] = function(name) {
+ return performance && performance['clearMarks'] && performance['clearMarks'](name);
+ };
+
+ var clearMeasures = _global['__zone_symbol__clearMeasures'] = function(name) {
+ return performance && performance['clearMeasures'] && performance['clearMeasures'](name);
+ };
+
+ var averageMeasures = _global['__zone_symbol__averageMeasures'] = function(name, times) {
+ var sum = _global['__zone_symbol__getEntriesByName'](name)
+ .filter(function(m) { return m.entryType === 'measure'; })
+ .map(function(m) { return m.duration })
+ .reduce(function(sum, d) { return sum + d; });
+ return sum / times;
+ };
+
+ var serialPromise = _global['__zone_symbol__serialPromise'] =
+ function(promiseFactories) {
+ let lastPromise;
+ for (var i = 0; i < promiseFactories.length; i++) {
+ var promiseFactory = promiseFactories[i];
+ if (!lastPromise) {
+ lastPromise = promiseFactory.factory(promiseFactory.context).then(function(value) {
+ return {value, idx: 0};
+ });
+ } else {
+ lastPromise = lastPromise.then(function(ctx) {
+ var idx = ctx.idx + 1;
+ var promiseFactory = promiseFactories[idx];
+ return promiseFactory.factory(promiseFactory.context).then(function(value) {
+ return {value, idx};
+ });
+ });
+ }
+ }
+ return lastPromise;
+ }
+
+ var callbackContext = _global['__zone_symbol__callbackContext'] = {};
+ var zone = _global['__zone_symbol__callbackZone'] = Zone.current.fork({
+ name: 'callback',
+ onScheduleTask: function(delegate, curr, target, task) {
+ delegate.scheduleTask(target, task);
+ if (task.type === callbackContext.type &&
+ task.source.indexOf(callbackContext.source) !== -1) {
+ if (task.type === 'macroTask' || task.type === 'eventTask') {
+ var invoke = task.invoke;
+ task.invoke = function() {
+ mark(callbackContext.measureName);
+ var result = invoke.apply(this, arguments);
+ measure(callbackContext.measureName, callbackContext.measureName);
+ return result;
+ };
+ } else if (task.type === 'microTask') {
+ var callback = task.callback;
+ task.callback = function() {
+ mark(callbackContext.measureName);
+ var result = callback.apply(this, arguments);
+ measure(callbackContext.measureName, callbackContext.measureName);
+ return result;
+ };
+ }
+ }
+ return task;
+ }
+ });
+
+ var runAsync = _global['__zone_symbol__runAsync'] = function(testFn, times, _delay) {
+ var delay = _delay | 100;
+ const fnPromise = function() {
+ return new Promise(function(res, rej) {
+ // run test with a setTimeout
+ // several times to decrease measurement error
+ setTimeout(function() { testFn().then(function() { res(); }); }, delay);
+ });
+ };
+ var promiseFactories = [];
+ for (var i = 0; i < times; i++) {
+ promiseFactories.push({factory: fnPromise, context: {}});
+ }
+
+ return serialPromise(promiseFactories);
+ };
+
+ var getNativeMethodName = function(nativeWithSymbol) {
+ return nativeWithSymbol.replace('__zone_symbol__', 'native_');
+ };
+
+ function testAddRemove(api, count) {
+ var timerId = [];
+
+ var name = api.method;
+ mark(name);
+ for (var i = 0; i < count; i++) {
+ timerId.push(api.run());
+ }
+ measure(name, name);
+
+ if (api.supportClear) {
+ var clearName = api.clearMethod;
+ mark(clearName);
+ for (var i = 0; i < count; i++) {
+ api.runClear(timerId[i]);
+ }
+ measure(clearName, clearName);
+ }
+
+ timerId = [];
+
+ var nativeName = getNativeMethodName(api.nativeMethod);
+ mark(nativeName);
+ for (var i = 0; i < count; i++) {
+ timerId.push(api.nativeRun());
+ }
+ measure(nativeName, nativeName);
+
+ if (api.supportClear) {
+ var nativeClearName = getNativeMethodName(api.nativeClearMethod);
+ mark(nativeClearName);
+ for (var i = 0; i < count; i++) {
+ api.nativeRunClear(timerId[i]);
+ }
+ measure(nativeClearName, nativeClearName);
+ }
+
+ return Promise.resolve(1);
+ }
+
+ function testCallback(api, count) {
+ var promises = [Promise.resolve(1)];
+ for (var i = 0; i < count; i++) {
+ var r = api.run();
+ if (api.isAsync) {
+ promises.push(r);
+ }
+ }
+
+ for (var i = 0; i < count; i++) {
+ var r = api.nativeRun();
+ if (api.isAsync) {
+ promises.push(r);
+ }
+ }
+ return Promise.all(promises);
+ }
+
+ function measureCallback(api, ops) {
+ var times = ops.times;
+ var displayText = ops.displayText;
+ var rawData = ops.rawData;
+ var summary = ops.summary;
+
+ var name = api.method;
+ var nativeName = getNativeMethodName(api.nativeMethod);
+ var measure = averageMeasures(name, times);
+ var nativeMeasure = averageMeasures(nativeName, times);
+ displayText += `- ${name} costs ${measure} ms\n`;
+ displayText += `- ${nativeName} costs ${nativeMeasure} ms\n`;
+ var absolute = Math.floor(1000 * (measure - nativeMeasure)) / 1000;
+ displayText += `# ${name} is ${absolute}ms slower than ${nativeName}\n`;
+ rawData[name + '_measure'] = measure;
+ rawData[nativeName + '_measure'] = nativeMeasure;
+ summary[name] = absolute + 'ms';
+ }
+
+ function measureAddRemove(api, ops) {
+ var times = ops.times;
+ var displayText = ops.displayText;
+ var rawData = ops.rawData;
+ var summary = ops.summary;
+
+ var name = api.method;
+ var nativeName = getNativeMethodName(api.nativeMethod);
+
+ var measure = averageMeasures(name, times);
+ var nativeMeasure = averageMeasures(nativeName, times);
+ displayText += `- ${name} costs ${measure} ms\n`;
+ displayText += `- ${nativeName} costs ${nativeMeasure} ms\n`;
+ var percent = Math.floor(100 * (measure - nativeMeasure) / nativeMeasure);
+ displayText += `# ${name} is ${percent}% slower than ${nativeName}\n`;
+ rawData[name + '_measure'] = measure;
+ rawData[nativeName + '_measure'] = nativeMeasure;
+ summary[name] = percent + '%';
+ if (api.supportClear) {
+ var clearName = api.clearMethod;
+ var nativeClearName = getNativeMethodName(api.nativeClearMethod);
+ var clearMeasure = averageMeasures(clearName, times);
+ var nativeClearMeasure = averageMeasures(nativeClearName, times);
+ var clearPercent = Math.floor(100 * (clearMeasure - nativeClearMeasure) / nativeClearMeasure);
+ displayText += `- ${clearName} costs ${clearMeasure} ms\n`;
+ displayText += `- ${nativeClearName} costs ${nativeClearMeasure} ms\n`;
+ displayText += `# ${clearName} is ${clearPercent}% slower than ${nativeClearName}\n`;
+ rawData[clearName + '_measure'] = clearMeasure;
+ rawData[nativeClearName + '_measure'] = nativeClearMeasure;
+ summary[clearName] = clearPercent + '%';
+ }
+ }
+
+ var testRunner = _global['__zone_symbol__testRunner'] = function(testTarget) {
+ var title = testTarget.title;
+ var apis = testTarget.apis;
+ var methods = apis.reduce(function(acc, api) {
+ return acc.concat([
+ api.method, api.nativeMethod
+ ].concat(api.supportClear ? [api.clearMethod, api.nativeClearMethod] : [])
+ .concat[api.method + '_callback', api.nativeMethod + '_callback']);
+
+ }, []);
+ var times = testTarget.times;
+
+ allTasks.push({
+ title: title,
+ cleanFn: function() {
+ methods.forEach(function(m) {
+ clearMarks(m);
+ clearMeasures(m);
+ });
+ },
+ before: function() { testTarget.before && testTarget.before(); },
+ after: function() { testTarget.after && testTarget.after(); },
+ testFn: function() {
+ var count = typeof testTarget.count === 'number' ? testTarget.count : 10000;
+ var times = typeof testTarget.times === 'number' ? testTarget.times : 5;
+
+ var testFunction = function() {
+ var promises = [];
+ apis.forEach(function(api) {
+ if (api.isCallback) {
+ var r = testCallback(api, count / 100);
+ promises.push(api.isAsync ? r : Promise.resolve(1));
+ } else {
+ var r = testAddRemove(api, count);
+ promises.push[api.isAsync ? r : Promise.resolve(1)];
+ }
+ });
+ return Promise.all(promises);
+ };
+
+ return runAsync(testFunction, times).then(function() {
+ var displayText = `running ${count} times\n`;
+ var rawData = {};
+ var summary = {};
+ apis.forEach(function(api) {
+ if (api.isCallback) {
+ measureCallback(api, {times, displayText, rawData, summary});
+ } else {
+ measureAddRemove(api, {times, displayText, rawData, summary});
+ }
+ });
+ return Promise.resolve({displayText: displayText, rawData: rawData, summary: summary});
+ });
+ }
+ });
+ };
+}(typeof window === 'undefined' ? global : window));
diff --git a/packages/zone.js/test/performance/performance_ui.js b/packages/zone.js/test/performance/performance_ui.js
new file mode 100644
index 0000000000..d54469cb42
--- /dev/null
+++ b/packages/zone.js/test/performance/performance_ui.js
@@ -0,0 +1,156 @@
+/**
+ * @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
+ */
+(function(_global) {
+ var options;
+
+ function setAttributes(elem, attrs) {
+ if (!attrs) {
+ return;
+ }
+ Object.keys(attrs).forEach(function(key) { elem.setAttribute(key, attrs[key]); });
+ }
+
+ function createLi(attrs) {
+ var li = document.createElement('li');
+ setAttributes(li, attrs);
+ return li;
+ }
+
+ function createLabel(attrs) {
+ var label = document.createElement('label');
+ setAttributes(label, attrs);
+ return label;
+ }
+
+ function createButton(attrs, innerHtml) {
+ var button = document.createElement('button');
+ button.innerHTML = innerHtml;
+ setAttributes(button, attrs);
+ return button;
+ }
+
+ function createTextNode(text) { return document.createTextNode(text); }
+
+ function createCheckbox(attrs, checked) {
+ var checkbox = document.createElement('input');
+ checkbox.type = 'checkbox';
+ checkbox.checked = !!checked;
+ setAttributes(checkbox, attrs);
+ return checkbox;
+ }
+
+ function createUl(attrs) {
+ var ul = document.createElement('ul');
+ setAttributes(ul, attrs);
+ return ul;
+ }
+
+ var serailPromise = _global['__zone_symbol__serialPromise'];
+
+ _global['__zone_symbol__testTargetsUIBuild'] = function(_options) {
+ options = _options;
+ var allButton = createButton({}, 'test selected');
+ allButton.addEventListener('click', function() {
+ var promiseFactories = [];
+ for (var i = 0; i < options.tests.length; i++) {
+ var checkbox = document.getElementById('testcheck' + i);
+ if (checkbox.checked) {
+ var test = options.tests[i];
+ promiseFactories.push({
+ factory: function(context) { return doTest(context.test, context.idx); },
+ context: {test: test, idx: i}
+ });
+ }
+ }
+ serailPromise(promiseFactories);
+ });
+ options.targetContainer.appendChild(allButton);
+
+ var ul = createUl();
+ options.targetContainer.appendChild(ul);
+
+ for (var i = 0; i < options.tests.length; i++) {
+ buildTestItemUI(ul, options.tests[i], i);
+ }
+ };
+
+ function buildTestItemUI(ul, testItem, idx) {
+ var li = createLi({'id': 'test' + idx});
+
+ var button = createButton({'id': 'buttontest' + idx}, 'begin test');
+ buildButtonClickHandler(button);
+
+ var title = createTextNode(options.tests[idx].title);
+ var checkbox = createCheckbox({'id': 'testcheck' + idx}, true);
+ var label = createLabel({'id': 'label' + idx});
+
+ li.appendChild(checkbox);
+ li.appendChild(title);
+ li.appendChild(button);
+ li.appendChild(label);
+
+ ul.appendChild(li);
+ }
+
+ function processTestResult(test, result, id) {
+ var split = result.displayText.split('\n');
+ options.jsonResult[test.title] = result.rawData;
+ options.jsonContainer.innerHTML =
+ '' + JSON.stringify(options.jsonResult) + '
';
+
+ var summary = result.summary;
+ var row = options.resultsContainer.insertRow();
+ var cell = row.insertCell();
+ cell.innerHTML = test.title;
+ cell.rowSpan = Object.keys(summary).length;
+ var idx = 0;
+ Object.keys(summary).forEach(function(key) {
+ var tableRow = row;
+ if (idx !== 0) {
+ tableRow = options.resultsContainer.insertRow();
+ }
+ var keyCell = tableRow.insertCell();
+ keyCell.innerHTML = key;
+ var valueCell = tableRow.insertCell();
+ valueCell.innerHTML = summary[key];
+ idx++;
+ });
+
+ var testLi = document.getElementById('test' + id);
+ for (var j = 0; j < split.length; j++) {
+ var br = document.createElement('br');
+ var s = document.createTextNode(split[j]);
+ testLi.appendChild(br);
+ testLi.appendChild(s);
+ }
+ }
+
+ function doTest(test, id) {
+ test.cleanFn();
+ test.before();
+ var button = document.getElementById('buttontest' + id);
+ button.setAttribute('enabled', 'false');
+ var label = document.getElementById('label' + id);
+ label.innerHTML = 'Testing';
+ return test.testFn().then(function(result) {
+ processTestResult(test, result, id);
+ test.after();
+ label.innerHTML = 'Finished';
+ button.setAttribute('enabled', 'true');
+ });
+ }
+
+ function buildButtonClickHandler(button) {
+ button.onclick = function(event) {
+ var target = event.target;
+ var id = target.getAttribute('id').substring(10);
+ var test = options.tests[id];
+ doTest(test, id);
+ };
+ }
+}(typeof window === 'undefined' ? global : window));
diff --git a/packages/zone.js/test/performance/promise.js b/packages/zone.js/test/performance/promise.js
new file mode 100644
index 0000000000..6e40799e72
--- /dev/null
+++ b/packages/zone.js/test/performance/promise.js
@@ -0,0 +1,57 @@
+/**
+ * @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
+ */
+(function(_global) {
+ var mark = _global['__zone_symbol__mark'];
+ var measure = _global['__zone_symbol__measure'];
+ var testRunner = _global['__zone_symbol__testRunner'];
+ var zone = _global['__zone_symbol__callbackZone'];
+ var nativePromise = _global['__zone_symbol__Promise'];
+ var resolved = Promise.resolve(1);
+ var nativeResolved = nativePromise.resolve(1);
+ var testTarget = {
+ title: 'Promise',
+ times: 10,
+ before: function() {
+ _global['__zone_symbol__callbackContext'].measureName = 'Promise_callback';
+ _global['__zone_symbol__callbackContext'].type = 'microTask';
+ _global['__zone_symbol__callbackContext'].source = 'Promise.then';
+ },
+ apis: [
+ {
+ supportClear: false,
+ isAsync: true,
+ method: 'Promise',
+ nativeMethod: 'native_Promise',
+ run: function() { return resolved.then(function() {}); },
+ nativeRun: function() { return nativeResolved['__zone_symbol__then'](function() {}); },
+ },
+ {
+ isCallback: true,
+ isAsync: true,
+ supportClear: false,
+ method: 'Promise_callback',
+ nativeMethod: 'native_Promise_callback',
+ run: function() {
+ return zone.run(function() {
+ return Promise.resolve(1).then(function(v) { return v; });
+ });
+ },
+ nativeRun: function() {
+ var func = function() {};
+ return _global['__zone_symbol__Promise'].resolve(1)['__zone_symbol__then'](function() {
+ mark('native_Promise_callback');
+ var result = func.apply(this, arguments);
+ measure('native_Promise_callback', 'native_Promise_callback');
+ return result;
+ });
+ }
+ }
+ ],
+ };
+ return testRunner(testTarget);
+}(typeof window === 'undefined' ? global : window));
diff --git a/packages/zone.js/test/performance/requestAnimationFrame.js b/packages/zone.js/test/performance/requestAnimationFrame.js
new file mode 100644
index 0000000000..06583809b5
--- /dev/null
+++ b/packages/zone.js/test/performance/requestAnimationFrame.js
@@ -0,0 +1,56 @@
+/**
+ * @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
+ */
+(function(_global) {
+ var mark = _global['__zone_symbol__mark'];
+ var measure = _global['__zone_symbol__measure'];
+ var zone = _global['__zone_symbol__callbackZone'];
+ var testRunner = _global['__zone_symbol__testRunner'];
+ var raf = _global['requestAnimationFrame'];
+ var cancel = _global['cancelAnimationFrame'];
+ var nativeRaf = _global['__zone_symbol__requestAnimationFrame'];
+ var nativeCancel = _global['__zone_symbol__cancelAnimationFrame'];
+ var testTarget = {
+ title: 'requestAnimationFrame',
+ times: 10,
+ before: function() {
+ _global['__zone_symbol__callbackContext'].measureName = 'requestAnimationFrame_callback';
+ _global['__zone_symbol__callbackContext'].type = 'macroTask';
+ _global['__zone_symbol__callbackContext'].source = 'requestAnimationFrame';
+ },
+ apis: [
+ {
+ supportClear: true,
+ method: 'requestAnimationFrame',
+ nativeMethod: '__zone_symbol__requestAnimationFrame',
+ clearMethod: 'cancelAnimationFrame',
+ nativeClearMethod: '__zone_symbol__cancelAnimationFrame',
+ run: function() { return raf(function() {}); },
+ runClear: function(timerId) { return cancel(timerId); },
+ nativeRun: function() { return nativeRaf(function() {}); },
+ nativeRunClear: function(timerId) { return nativeCancel(timerId); }
+ },
+ {
+ isCallback: true,
+ supportClear: false,
+ method: 'requestAnimationFrame_callback',
+ nativeMethod: 'native_requestAnimationFrame_callback',
+ run: function() { zone.run(function() { raf(function() {}); }); },
+ nativeRun: function() {
+ var func = function() {};
+ nativeRaf(function() {
+ mark('native_requestAnimationFrame_callback');
+ func.apply(this, arguments);
+ measure(
+ 'native_requestAnimationFrame_callback', 'native_requestAnimationFrame_callback');
+ });
+ }
+ }
+ ],
+ };
+ return testRunner(testTarget);
+}(typeof window === 'undefined' ? global : window));
diff --git a/packages/zone.js/test/performance/timeout.js b/packages/zone.js/test/performance/timeout.js
new file mode 100644
index 0000000000..7dcdea63bc
--- /dev/null
+++ b/packages/zone.js/test/performance/timeout.js
@@ -0,0 +1,55 @@
+/**
+ * @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
+ */
+(function(_global) {
+ var mark = _global['__zone_symbol__mark'];
+ var measure = _global['__zone_symbol__measure'];
+ var testRunner = _global['__zone_symbol__testRunner'];
+ var setTimeout = _global['setTimeout'];
+ var clearTimeout = _global['clearTimeout'];
+ var nativeSetTimeout = _global['__zone_symbol__setTimeout'];
+ var nativeClearTimeout = _global['__zone_symbol__clearTimeout'];
+ var zone = _global['__zone_symbol__callbackZone'];
+ var testTarget = {
+ title: 'timer',
+ times: 10,
+ before: function() {
+ _global['__zone_symbol__callbackContext'].measureName = 'setTimeout_callback';
+ _global['__zone_symbol__callbackContext'].type = 'macroTask';
+ _global['__zone_symbol__callbackContext'].source = 'setTimeout';
+ },
+ apis: [
+ {
+ supportClear: true,
+ method: 'setTimeout',
+ nativeMethod: '__zone_symbol__setTimeout',
+ clearMethod: 'clearTimeout',
+ nativeClearMethod: '__zone_symbol__clearTimeout',
+ run: function() { return setTimeout(function() {}); },
+ runClear: function(timerId) { return clearTimeout(timerId); },
+ nativeRun: function() { return nativeSetTimeout(function() {}); },
+ nativeRunClear: function(timerId) { return nativeClearTimeout(timerId); }
+ },
+ {
+ isCallback: true,
+ supportClear: false,
+ method: 'setTimeout_callback',
+ nativeMethod: 'native_setTimeout_callback',
+ run: function() { zone.run(function() { setTimeout(function() {}); }); },
+ nativeRun: function() {
+ var func = function() {};
+ nativeSetTimeout(function() {
+ mark('native_setTimeout_callback');
+ func.apply(this, arguments);
+ measure('native_setTimeout_callback', 'native_setTimeout_callback');
+ });
+ }
+ }
+ ],
+ };
+ return testRunner(testTarget);
+}(typeof window === 'undefined' ? global : window));
diff --git a/packages/zone.js/test/performance/xhr.js b/packages/zone.js/test/performance/xhr.js
new file mode 100644
index 0000000000..22d6068515
--- /dev/null
+++ b/packages/zone.js/test/performance/xhr.js
@@ -0,0 +1,47 @@
+/**
+ * @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
+ */
+(function(_global) {
+ var mark = _global['__zone_symbol__mark'];
+ var measure = _global['__zone_symbol__measure'];
+ var testRunner = _global['__zone_symbol__testRunner'];
+ var zone = _global['__zone_symbol__callbackZone'];
+ var testTarget = {
+ title: 'xhr',
+ times: 3,
+ count: 1000,
+ before: function() {
+ _global['__zone_symbol__callbackContext'].measureName = 'xhr_callback';
+ _global['__zone_symbol__callbackContext'].type = 'macroTask';
+ _global['__zone_symbol__callbackContext'].source = 'send';
+ },
+ apis: [
+ {
+ supportClear: true,
+ method: 'XHR.send',
+ nativeMethod: 'native.XHR.send',
+ clearMethod: 'XHR.abort',
+ nativeClearMethod: 'native.XHR.abort',
+ run: function() {
+ var xhr = new XMLHttpRequest();
+ xhr.open('get', 'http://localhost:8080', true);
+ xhr.send();
+ return xhr;
+ },
+ runClear: function(xhr) { xhr.abort(); },
+ nativeRun: function() {
+ var xhr = new XMLHttpRequest();
+ xhr['__zone_symbol__open']('get', 'http://localhost:8080', true);
+ xhr['__zone_symbol__send']();
+ return xhr;
+ },
+ nativeRunClear: function(xhr) { xhr['__zone_symbol__abort'](); }
+ },
+ ],
+ };
+ return testRunner(testTarget);
+}(typeof window === 'undefined' ? global : window));
diff --git a/packages/zone.js/test/rxjs/rxjs.Observable.audit.spec.ts b/packages/zone.js/test/rxjs/rxjs.Observable.audit.spec.ts
new file mode 100644
index 0000000000..92d1ae10e0
--- /dev/null
+++ b/packages/zone.js/test/rxjs/rxjs.Observable.audit.spec.ts
@@ -0,0 +1,79 @@
+/**
+ * @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
+ */
+import {Observable, interval} from 'rxjs';
+import {audit, auditTime} from 'rxjs/operators';
+
+import {asyncTest} from '../test-util';
+
+xdescribe('Observable.audit', () => {
+ let log: any[];
+ let observable1: Observable;
+
+ beforeEach(() => { log = []; });
+
+ it('audit func callback should run in the correct zone', asyncTest((done: any) => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 = constructorZone1.run(() => {
+ const source = interval(100);
+ return source.pipe(audit(ev => {
+ expect(Zone.current.name).toEqual(constructorZone1.name);
+ return interval(150);
+ }));
+ });
+
+ subscriptionZone.run(() => {
+ const subscriber = observable1.subscribe(
+ (result: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push(result);
+ if (result >= 3) {
+ subscriber.unsubscribe();
+ }
+ },
+ () => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([1, 3, 'completed']);
+ done();
+ });
+ });
+
+ expect(log).toEqual([]);
+ }, Zone.root));
+
+ xit('auditTime func callback should run in the correct zone', asyncTest((done: any) => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 = constructorZone1.run(() => {
+ const source = interval(100);
+ return source.pipe(auditTime(360));
+ });
+
+ subscriptionZone.run(() => {
+ const subscriber = observable1.subscribe(
+ (result: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push(result);
+ if (result >= 7) {
+ subscriber.unsubscribe();
+ }
+ },
+ () => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([3, 7, 'completed']);
+ done();
+ });
+ });
+
+ expect(log).toEqual([]);
+ }, Zone.root));
+});
diff --git a/packages/zone.js/test/rxjs/rxjs.Observable.buffer.spec.ts b/packages/zone.js/test/rxjs/rxjs.Observable.buffer.spec.ts
new file mode 100644
index 0000000000..8ddc8eb325
--- /dev/null
+++ b/packages/zone.js/test/rxjs/rxjs.Observable.buffer.spec.ts
@@ -0,0 +1,173 @@
+/**
+ * @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
+ */
+
+import {Observable, empty, interval, of } from 'rxjs';
+import {buffer, bufferCount, bufferTime, bufferToggle, bufferWhen} from 'rxjs/operators';
+
+import {asyncTest} from '../test-util';
+
+xdescribe('Observable.buffer', () => {
+ let log: any[];
+ let observable1: Observable;
+
+ beforeEach(() => { log = []; });
+
+ it('buffer func callback should run in the correct zone', asyncTest((done: any) => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 = constructorZone1.run(() => {
+ const source = interval(350);
+ const iv = interval(100);
+ return iv.pipe(buffer(source));
+ });
+
+ subscriptionZone.run(() => {
+ const subscriber = observable1.subscribe(
+ (result: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push(result);
+ if (result[0] >= 3) {
+ subscriber.unsubscribe();
+ }
+ },
+ () => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([[0, 1, 2], [3, 4, 5], 'completed']);
+ done();
+ });
+ });
+
+ expect(log).toEqual([]);
+ }, Zone.root));
+
+ it('bufferCount func callback should run in the correct zone', asyncTest((done: any) => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 = constructorZone1.run(() => {
+ const iv = interval(100);
+ return iv.pipe(bufferCount(3));
+ });
+
+ subscriptionZone.run(() => {
+ const subscriber = observable1.subscribe(
+ (result: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push(result);
+ if (result[0] >= 3) {
+ subscriber.unsubscribe();
+ }
+ },
+ () => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([[0, 1, 2], [3, 4, 5], 'completed']);
+ done();
+ });
+ });
+
+ expect(log).toEqual([]);
+ }, Zone.root));
+
+ it('bufferTime func callback should run in the correct zone', asyncTest((done: any) => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 = constructorZone1.run(() => {
+ const iv = interval(100);
+ return iv.pipe(bufferTime(350));
+ });
+
+ subscriptionZone.run(() => {
+ const subscriber = observable1.subscribe(
+ (result: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push(result);
+ if (result[0] >= 3) {
+ subscriber.unsubscribe();
+ }
+ },
+ () => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([[0, 1, 2], [3, 4, 5], 'completed']);
+ done();
+ });
+ });
+
+ expect(log).toEqual([]);
+ }, Zone.root));
+
+ it('bufferToggle func callback should run in the correct zone', asyncTest((done: any) => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 = constructorZone1.run(() => {
+ const source = interval(10);
+ const opening = interval(25);
+ const closingSelector = (v: any) => {
+ expect(Zone.current.name).toEqual(constructorZone1.name);
+ return v % 2 === 0 ? of (v) : empty();
+ };
+ return source.pipe(bufferToggle(opening, closingSelector));
+ });
+
+ let i = 0;
+ subscriptionZone.run(() => {
+ const subscriber = observable1.subscribe(
+ (result: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push(result);
+ subscriber.unsubscribe();
+ },
+ () => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([[], 'completed']);
+ done();
+ });
+ });
+
+ expect(log).toEqual([]);
+ }, Zone.root));
+
+ it('bufferWhen func callback should run in the correct zone', asyncTest((done: any) => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 = constructorZone1.run(() => {
+ const source = interval(100);
+ return source.pipe(bufferWhen(() => {
+ expect(Zone.current.name).toEqual(constructorZone1.name);
+ return interval(220);
+ }));
+ });
+
+ let i = 0;
+ subscriptionZone.run(() => {
+ const subscriber = observable1.subscribe(
+ (result: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push(result);
+ if (i++ >= 3) {
+ subscriber.unsubscribe();
+ }
+ },
+ () => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([[0, 1], [2, 3], [4, 5], [6, 7], 'completed']);
+ done();
+ });
+ });
+
+ expect(log).toEqual([]);
+ }, Zone.root));
+});
diff --git a/packages/zone.js/test/rxjs/rxjs.Observable.catch.spec.ts b/packages/zone.js/test/rxjs/rxjs.Observable.catch.spec.ts
new file mode 100644
index 0000000000..8ee04a4e7d
--- /dev/null
+++ b/packages/zone.js/test/rxjs/rxjs.Observable.catch.spec.ts
@@ -0,0 +1,83 @@
+/**
+ * @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
+ */
+import {Observable, of } from 'rxjs';
+import {catchError, map, retry} from 'rxjs/operators';
+
+describe('Observable.catch', () => {
+ let log: any[];
+ let observable1: Observable;
+
+ beforeEach(() => { log = []; });
+
+ it('catch func callback should run in the correct zone', () => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 = constructorZone1.run(() => {
+ const error = new Error('test');
+ const source = of (1, 2, 3).pipe(map((n: number) => {
+ expect(Zone.current.name).toEqual(constructorZone1.name);
+ if (n === 2) {
+ throw error;
+ }
+ return n;
+ }));
+ return source.pipe(catchError((err: any) => {
+ expect(Zone.current.name).toEqual(constructorZone1.name);
+ return of ('error1', 'error2');
+ }));
+ });
+
+ subscriptionZone.run(() => {
+ const subscriber = observable1.subscribe(
+ (result: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push(result);
+ },
+ () => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ });
+ });
+ expect(log).toEqual([1, 'error1', 'error2', 'completed']);
+ });
+
+ it('retry func callback should run in the correct zone', () => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ const error = new Error('test');
+ observable1 = constructorZone1.run(() => {
+ return of (1, 2, 3).pipe(
+ map((n: number) => {
+ expect(Zone.current.name).toEqual(constructorZone1.name);
+ if (n === 2) {
+ throw error;
+ }
+ return n;
+ }),
+ retry(1));
+ });
+
+ subscriptionZone.run(() => {
+ const subscriber = observable1.subscribe(
+ (result: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push(result);
+ },
+ (error: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push(error);
+ },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ });
+ });
+ expect(log).toEqual([1, 1, error]);
+ });
+});
diff --git a/packages/zone.js/test/rxjs/rxjs.Observable.collection.spec.ts b/packages/zone.js/test/rxjs/rxjs.Observable.collection.spec.ts
new file mode 100644
index 0000000000..34bd6354e4
--- /dev/null
+++ b/packages/zone.js/test/rxjs/rxjs.Observable.collection.spec.ts
@@ -0,0 +1,643 @@
+/**
+ * @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
+ */
+import {Observable, from, interval, of } from 'rxjs';
+import {elementAt, every, filter, find, findIndex, first, flatMap, groupBy, ignoreElements, isEmpty, last, map, mapTo, max, min, reduce, repeat, scan, single, skip, skipUntil, skipWhile, startWith} from 'rxjs/operators';
+
+import {asyncTest, isPhantomJS} from '../test-util';
+
+describe('Observable.collection', () => {
+ let log: any[];
+ let observable1: Observable;
+ let defaultTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
+
+ beforeEach(() => {
+ log = [];
+ jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
+ });
+
+ afterEach(function() { jasmine.DEFAULT_TIMEOUT_INTERVAL = defaultTimeout; });
+
+ it('elementAt func callback should run in the correct zone', () => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ const error = new Error('test');
+ observable1 = constructorZone1.run(() => { return of (1, 2, 3).pipe(elementAt(1)); });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([2, 'completed']);
+ });
+ });
+ });
+
+ it('every func callback should run in the correct zone', () => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const everyZone1: Zone = Zone.current.fork({name: 'Every Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ const error = new Error('test');
+ observable1 = constructorZone1.run(() => { return of (1, 2, 3); });
+
+ observable1 = everyZone1.run(() => {
+ return observable1.pipe(every((v: any) => {
+ expect(Zone.current.name).toEqual(everyZone1.name);
+ return v % 2 === 0;
+ }));
+ });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([false, 'completed']);
+ });
+ });
+ });
+
+ it('filter func callback should run in the correct zone', () => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const filterZone1: Zone = Zone.current.fork({name: 'Filter Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ const error = new Error('test');
+ observable1 = constructorZone1.run(() => { return of (1, 2, 3); });
+
+ observable1 = filterZone1.run(() => {
+ return observable1.pipe(filter((v: any) => {
+ expect(Zone.current.name).toEqual(filterZone1.name);
+ return v % 2 === 0;
+ }));
+ });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([2, 'completed']);
+ });
+ });
+ });
+
+ it('find func callback should run in the correct zone', () => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const findZone1: Zone = Zone.current.fork({name: 'Find Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ const error = new Error('test');
+ observable1 = constructorZone1.run(() => { return of (1, 2, 3); });
+
+ observable1 = findZone1.run(() => {
+ return observable1.pipe(find((v: any) => {
+ expect(Zone.current.name).toEqual(findZone1.name);
+ return v === 2;
+ }));
+ });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([2, 'completed']);
+ });
+ });
+ });
+
+ it('findIndex func callback should run in the correct zone', () => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const findZone1: Zone = Zone.current.fork({name: 'Find Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ const error = new Error('test');
+ observable1 = constructorZone1.run(() => { return of (1, 2, 3); });
+
+ observable1 = findZone1.run(() => {
+ return observable1.pipe(findIndex((v: any) => {
+ expect(Zone.current.name).toEqual(findZone1.name);
+ return v === 2;
+ }));
+ });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([1, 'completed']);
+ });
+ });
+ });
+
+ it('first func callback should run in the correct zone', () => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const firstZone1: Zone = Zone.current.fork({name: 'First Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ const error = new Error('test');
+ observable1 = constructorZone1.run(() => { return of (1, 2, 3); });
+
+ observable1 = firstZone1.run(() => {
+ return observable1.pipe(first((v: any) => {
+ expect(Zone.current.name).toEqual(firstZone1.name);
+ return v === 2;
+ }));
+ });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([2, 'completed']);
+ });
+ });
+ });
+
+ it('groupBy func callback should run in the correct zone', () => {
+ if (isPhantomJS()) {
+ return;
+ }
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const groupByZone1: Zone = Zone.current.fork({name: 'groupBy Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 = constructorZone1.run(() => {
+ const people = [
+ {name: 'Sue', age: 25}, {name: 'Joe', age: 30}, {name: 'Frank', age: 25},
+ {name: 'Sarah', age: 35}
+ ];
+ return from(people);
+ });
+
+ observable1 = groupByZone1.run(() => {
+ return observable1.pipe(
+ groupBy((person: any) => {
+ expect(Zone.current.name).toEqual(groupByZone1.name);
+ return person.age;
+ }),
+ // return as array of each group
+ flatMap((group: any) => {
+ return group.pipe(reduce((acc: any, curr: any) => [...acc, curr], []));
+ }));
+ });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ (err: any) => { fail('should not call error' + err); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([
+ [{age: 25, name: 'Sue'}, {age: 25, name: 'Frank'}], [{age: 30, name: 'Joe'}],
+ [{age: 35, name: 'Sarah'}], 'completed'
+ ]);
+ });
+ });
+ });
+
+ it('ignoreElements func callback should run in the correct zone', () => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const ignoreZone1: Zone = Zone.current.fork({name: 'Ignore Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ const error = new Error('test');
+ observable1 = constructorZone1.run(() => { return of (1, 2, 3).pipe(ignoreElements()); });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => { fail('should not call next'); },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual(['completed']);
+ });
+ });
+ });
+
+ it('isEmpty func callback should run in the correct zone', () => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const isEmptyZone1: Zone = Zone.current.fork({name: 'IsEmpty Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ const error = new Error('test');
+ observable1 = constructorZone1.run(() => { return of (1, 2, 3).pipe(isEmpty()); });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([false, 'completed']);
+ });
+ });
+ });
+
+ it('last func callback should run in the correct zone', () => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const lastZone1: Zone = Zone.current.fork({name: 'Last Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ const error = new Error('test');
+ observable1 = constructorZone1.run(() => { return of (1, 2, 3).pipe(last()); });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([3, 'completed']);
+ });
+ });
+ });
+
+ it('map func callback should run in the correct zone', () => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const mapZone1: Zone = Zone.current.fork({name: 'Map Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ const error = new Error('test');
+ observable1 = constructorZone1.run(() => { return of (1, 2, 3); });
+
+ observable1 = mapZone1.run(() => {
+ return observable1.pipe(map((v: any) => {
+ expect(Zone.current.name).toEqual(mapZone1.name);
+ return v + 1;
+ }));
+ });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([2, 3, 4, 'completed']);
+ });
+ });
+ });
+
+ it('mapTo func callback should run in the correct zone', () => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const mapToZone1: Zone = Zone.current.fork({name: 'MapTo Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ const error = new Error('test');
+ observable1 = constructorZone1.run(() => { return of (1, 2, 3); });
+
+ observable1 = mapToZone1.run(() => { return observable1.pipe(mapTo('a')); });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual(['a', 'a', 'a', 'completed']);
+ });
+ });
+ });
+
+ it('max func callback should run in the correct zone', () => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ const error = new Error('test');
+ observable1 = constructorZone1.run(() => { return of (4, 2, 3).pipe(max()); });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([4, 'completed']);
+ });
+ });
+ });
+
+ it('max with comparer func callback should run in the correct zone', () => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const maxZone1: Zone = Zone.current.fork({name: 'Max Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ const error = new Error('test');
+ observable1 = constructorZone1.run(() => { return of (4, 2, 3); });
+
+ observable1 = maxZone1.run(() => {
+ return observable1.pipe(max((x: number, y: number) => {
+ expect(Zone.current.name).toEqual(maxZone1.name);
+ return x < y ? -1 : 1;
+ }));
+ });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([4, 'completed']);
+ });
+ });
+ });
+
+ it('min func callback should run in the correct zone', () => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ const error = new Error('test');
+ observable1 = constructorZone1.run(() => { return of (4, 2, 3).pipe(min()); });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([2, 'completed']);
+ });
+ });
+ });
+
+ it('min with comparer func callback should run in the correct zone', () => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const minZone1: Zone = Zone.current.fork({name: 'Min Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ const error = new Error('test');
+ observable1 = constructorZone1.run(() => { return of (4, 2, 3); });
+
+ observable1 = minZone1.run(() => {
+ return observable1.pipe(max((x: number, y: number) => {
+ expect(Zone.current.name).toEqual(minZone1.name);
+ return x < y ? 1 : -1;
+ }));
+ });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([2, 'completed']);
+ });
+ });
+ });
+
+ it('reduce func callback should run in the correct zone', () => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const reduceZone1: Zone = Zone.current.fork({name: 'Min Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 = constructorZone1.run(() => { return of (4, 2, 3); });
+
+ observable1 = reduceZone1.run(() => {
+ return observable1.pipe(reduce((acc: number, one: number) => {
+ expect(Zone.current.name).toEqual(reduceZone1.name);
+ return acc + one;
+ }));
+ });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([9, 'completed']);
+ });
+ });
+ });
+
+ it('scan func callback should run in the correct zone', () => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const scanZone1: Zone = Zone.current.fork({name: 'Min Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 = constructorZone1.run(() => { return of (4, 2, 3); });
+
+ observable1 = scanZone1.run(() => {
+ return observable1.pipe(scan((acc: number, one: number) => {
+ expect(Zone.current.name).toEqual(scanZone1.name);
+ return acc + one;
+ }));
+ });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([4, 6, 9, 'completed']);
+ });
+ });
+ });
+
+ it('repeat func callback should run in the correct zone', () => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 = constructorZone1.run(() => { return of (1).pipe(repeat(2)); });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([1, 1, 'completed']);
+ });
+ });
+ });
+
+ it('single func callback should run in the correct zone', () => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const singleZone1: Zone = Zone.current.fork({name: 'Single Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 = constructorZone1.run(() => { return of (1, 2, 3, 4, 5); });
+
+ observable1 = singleZone1.run(() => {
+ return observable1.pipe(single((val: any) => {
+ expect(Zone.current.name).toEqual(singleZone1.name);
+ return val === 4;
+ }));
+ });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([4, 'completed']);
+ });
+ });
+ });
+
+ it('skip func callback should run in the correct zone', () => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 = constructorZone1.run(() => { return of (1, 2, 3, 4, 5).pipe(skip(3)); });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([4, 5, 'completed']);
+ });
+ });
+ });
+
+ xit('skipUntil func callback should run in the correct zone', asyncTest((done: any) => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 =
+ constructorZone1.run(() => { return interval(10).pipe(skipUntil(interval(25))); });
+
+ subscriptionZone.run(() => {
+ const subscriber = observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ subscriber.unsubscribe();
+ },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([2, 'completed']);
+ done();
+ });
+ });
+ }, Zone.root));
+
+ it('skipWhile func callback should run in the correct zone', asyncTest((done: any) => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const skipZone1: Zone = Zone.current.fork({name: 'Skip Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 = constructorZone1.run(() => { return interval(10); });
+
+ observable1 = skipZone1.run(() => {
+ return observable1.pipe(skipWhile((val: any) => {
+ expect(Zone.current.name).toEqual(skipZone1.name);
+ return val < 2;
+ }));
+ });
+
+ subscriptionZone.run(() => {
+ const subscriber = observable1.subscribe(
+ (result: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ subscriber.unsubscribe();
+ expect(result).toEqual(2);
+ done();
+ },
+ (err: any) => { fail('should not call error'); });
+ });
+ }, Zone.root));
+
+ it('startWith func callback should run in the correct zone', () => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 = constructorZone1.run(() => { return of (1, 2).pipe(startWith(3)); });
+
+ subscriptionZone.run(() => {
+ const subscriber = observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([3, 1, 2, 'completed']);
+ });
+ });
+ });
+});
diff --git a/packages/zone.js/test/rxjs/rxjs.Observable.combine.spec.ts b/packages/zone.js/test/rxjs/rxjs.Observable.combine.spec.ts
new file mode 100644
index 0000000000..07dc8319bc
--- /dev/null
+++ b/packages/zone.js/test/rxjs/rxjs.Observable.combine.spec.ts
@@ -0,0 +1,128 @@
+/**
+ * @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
+ */
+import {Observable, combineLatest, of } from 'rxjs';
+import {combineAll, map} from 'rxjs/operators';
+
+import {asyncTest} from '../test-util';
+
+describe('Observable.combine', () => {
+ let log: any[];
+ let observable1: Observable;
+
+ beforeEach(() => { log = []; });
+
+ it('combineAll func callback should run in the correct zone', asyncTest((done: any) => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 = constructorZone1.run(() => {
+ const source = of (1, 2);
+ const highOrder = source.pipe(map((src: any) => {
+ expect(Zone.current.name).toEqual(constructorZone1.name);
+ return of (src);
+ }));
+ return highOrder.pipe(combineAll());
+ });
+
+ subscriptionZone.run(() => {
+ const subscriber = observable1.subscribe(
+ (result: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push(result);
+ },
+ () => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([[1, 2], 'completed']);
+ done();
+ });
+ });
+ }, Zone.root));
+
+ it('combineAll func callback should run in the correct zone with project function',
+ asyncTest((done: any) => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 = constructorZone1.run(() => {
+ const source = of (1, 2, 3);
+ const highOrder = source.pipe(map((src: any) => {
+ expect(Zone.current.name).toEqual(constructorZone1.name);
+ return of (src);
+ }));
+ return highOrder.pipe(combineAll((x: any, y: any) => {
+ expect(Zone.current.name).toEqual(constructorZone1.name);
+ return {x: x, y: y};
+ }));
+ });
+
+ subscriptionZone.run(() => {
+ const subscriber = observable1.subscribe(
+ (result: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push(result);
+ },
+ () => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([{x: 1, y: 2}, 'completed']);
+ done();
+ });
+ });
+ }, Zone.root));
+
+ it('combineLatest func callback should run in the correct zone', () => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 = constructorZone1.run(() => {
+ const source = of (1, 2, 3);
+ const input = of (4, 5, 6);
+ return combineLatest(source, input);
+ });
+
+ subscriptionZone.run(() => {
+ const subscriber = observable1.subscribe(
+ (result: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push(result);
+ },
+ () => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ });
+ });
+
+ expect(log).toEqual([[3, 4], [3, 5], [3, 6], 'completed']);
+ });
+
+ it('combineLatest func callback should run in the correct zone with project function', () => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 = constructorZone1.run(() => {
+ const source = of (1, 2, 3);
+ const input = of (4, 5, 6);
+ return combineLatest(source, input, (x: number, y: number) => { return x + y; });
+ });
+
+ subscriptionZone.run(() => {
+ const subscriber = observable1.subscribe(
+ (result: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push(result);
+ },
+ () => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ });
+ });
+
+ expect(log).toEqual([7, 8, 9, 'completed']);
+ });
+});
diff --git a/packages/zone.js/test/rxjs/rxjs.Observable.concat.spec.ts b/packages/zone.js/test/rxjs/rxjs.Observable.concat.spec.ts
new file mode 100644
index 0000000000..7084cf022b
--- /dev/null
+++ b/packages/zone.js/test/rxjs/rxjs.Observable.concat.spec.ts
@@ -0,0 +1,178 @@
+/**
+ * @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
+ */
+
+import {Observable, asapScheduler, concat, of , range} from 'rxjs';
+import {concatAll, concatMap, concatMapTo, map} from 'rxjs/operators';
+
+import {asyncTest} from '../test-util';
+
+describe('Observable instance method concat', () => {
+ let log: any[];
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const constructorZone2: Zone = Zone.current.fork({name: 'Constructor Zone2'});
+ const constructorZone3: Zone = Zone.current.fork({name: 'Constructor Zone3'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ let observable1: Observable;
+ let observable2: any;
+
+ let concatObservable: any;
+
+ beforeEach(() => { log = []; });
+
+ it('concat func callback should run in the correct zone', () => {
+ observable1 = constructorZone1.run(() => {
+ return new Observable(subscriber => {
+ expect(Zone.current.name).toEqual(constructorZone1.name);
+ subscriber.next(1);
+ subscriber.next(2);
+ subscriber.complete();
+ });
+ });
+
+ observable2 = constructorZone2.run(() => { return range(3, 4); });
+
+ constructorZone3.run(() => { concatObservable = concat(observable1, observable2); });
+
+ subscriptionZone.run(() => {
+ concatObservable.subscribe((concat: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push(concat);
+ });
+ });
+
+ expect(log).toEqual([1, 2, 3, 4, 5, 6]);
+ });
+
+ xit('concat func callback should run in the correct zone with scheduler',
+ asyncTest((done: any) => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const constructorZone2: Zone = Zone.current.fork({name: 'Constructor Zone2'});
+ const constructorZone3: Zone = Zone.current.fork({name: 'Constructor Zone3'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 = constructorZone1.run(() => { return of (1, 2); });
+
+ observable2 = constructorZone2.run(() => { return range(3, 4); });
+
+ constructorZone3.run(
+ () => { concatObservable = concat(observable1, observable2, asapScheduler); });
+
+ subscriptionZone.run(() => {
+ concatObservable.subscribe(
+ (concat: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push(concat);
+ },
+ (error: any) => { fail('subscribe failed' + error); },
+ () => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([1, 2, 3, 4, 5, 6]);
+ done();
+ });
+ });
+
+ expect(log).toEqual([]);
+ }, Zone.root));
+
+ it('concatAll func callback should run in the correct zone', asyncTest((done: any) => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const constructorZone2: Zone = Zone.current.fork({name: 'Constructor Zone2'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 = constructorZone1.run(() => { return of (0, 1, 2); });
+
+ constructorZone2.run(() => {
+ const highOrder = observable1.pipe(map((v: any) => {
+ expect(Zone.current.name).toEqual(constructorZone2.name);
+ return of (v + 1);
+ }));
+ concatObservable = highOrder.pipe(concatAll());
+ });
+
+ subscriptionZone.run(() => {
+ concatObservable.subscribe(
+ (concat: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push(concat);
+ },
+ (error: any) => { fail('subscribe failed' + error); },
+ () => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([1, 2, 3]);
+ done();
+ });
+ });
+ }, Zone.root));
+
+ it('concatMap func callback should run in the correct zone', asyncTest((done: any) => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const constructorZone2: Zone = Zone.current.fork({name: 'Constructor Zone2'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 = constructorZone1.run(() => {
+ return new Observable(subscriber => {
+ expect(Zone.current.name).toEqual(constructorZone1.name);
+ subscriber.next(1);
+ subscriber.next(2);
+ subscriber.next(3);
+ subscriber.next(4);
+ subscriber.complete();
+ });
+ });
+
+ constructorZone2.run(() => {
+ concatObservable = observable1.pipe(concatMap((v: any) => {
+ expect(Zone.current.name).toEqual(constructorZone2.name);
+ return of (0, 1);
+ }));
+ });
+
+ subscriptionZone.run(() => {
+ concatObservable.subscribe(
+ (concat: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push(concat);
+ },
+ (error: any) => { fail('subscribe failed' + error); },
+ () => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([0, 1, 0, 1, 0, 1, 0, 1]);
+ done();
+ });
+ });
+ }, Zone.root));
+
+ it('concatMapTo func callback should run in the correct zone', asyncTest((done: any) => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const constructorZone2: Zone = Zone.current.fork({name: 'Constructor Zone2'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 = constructorZone1.run(() => {
+ return new Observable(subscriber => {
+ expect(Zone.current.name).toEqual(constructorZone1.name);
+ subscriber.next(1);
+ subscriber.next(2);
+ subscriber.next(3);
+ subscriber.next(4);
+ subscriber.complete();
+ });
+ });
+
+ constructorZone2.run(() => { concatObservable = observable1.pipe(concatMapTo(of (0, 1))); });
+
+ subscriptionZone.run(() => {
+ concatObservable.subscribe(
+ (concat: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push(concat);
+ },
+ (error: any) => { fail('subscribe failed' + error); },
+ () => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([0, 1, 0, 1, 0, 1, 0, 1]);
+ done();
+ });
+ });
+ }, Zone.root));
+});
diff --git a/packages/zone.js/test/rxjs/rxjs.Observable.count.spec.ts b/packages/zone.js/test/rxjs/rxjs.Observable.count.spec.ts
new file mode 100644
index 0000000000..c3aa504a86
--- /dev/null
+++ b/packages/zone.js/test/rxjs/rxjs.Observable.count.spec.ts
@@ -0,0 +1,41 @@
+/**
+ * @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
+ */
+import {Observable, range} from 'rxjs';
+import {count} from 'rxjs/operators';
+
+describe('Observable.count', () => {
+ let log: any[];
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ let observable1: Observable;
+
+ beforeEach(() => { log = []; });
+
+ it('count func callback should run in the correct zone', () => {
+ observable1 = constructorZone1.run(() => {
+ return range(1, 3).pipe(count((i: number) => {
+ expect(Zone.current.name).toEqual(constructorZone1.name);
+ return i % 2 === 0;
+ }));
+ });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ () => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ });
+ });
+ expect(log).toEqual([1, 'completed']);
+ });
+});
diff --git a/packages/zone.js/test/rxjs/rxjs.Observable.debounce.spec.ts b/packages/zone.js/test/rxjs/rxjs.Observable.debounce.spec.ts
new file mode 100644
index 0000000000..4112ae1224
--- /dev/null
+++ b/packages/zone.js/test/rxjs/rxjs.Observable.debounce.spec.ts
@@ -0,0 +1,65 @@
+/**
+ * @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
+ */
+import {Observable, of , timer} from 'rxjs';
+import {debounce, debounceTime} from 'rxjs/operators';
+
+import {asyncTest} from '../test-util';
+
+describe('Observable.debounce', () => {
+ let log: any[];
+ let observable1: Observable;
+
+ beforeEach(() => { log = []; });
+
+ it('debounce func callback should run in the correct zone', asyncTest((done: any) => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 = constructorZone1.run(() => {
+ return of (1, 2, 3).pipe(debounce(() => {
+ expect(Zone.current.name).toEqual(constructorZone1.name);
+ return timer(100);
+ }));
+ });
+
+ subscriptionZone.run(() => {
+ const subscriber = observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ () => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ done();
+ });
+ });
+ expect(log).toEqual([3, 'completed']);
+ }, Zone.root));
+
+ it('debounceTime func callback should run in the correct zone', asyncTest((done: any) => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 = constructorZone1.run(() => { return of (1, 2, 3).pipe(debounceTime(100)); });
+
+ subscriptionZone.run(() => {
+ const subscriber = observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ () => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ done();
+ });
+ });
+ expect(log).toEqual([3, 'completed']);
+ }, Zone.root));
+});
diff --git a/packages/zone.js/test/rxjs/rxjs.Observable.default.spec.ts b/packages/zone.js/test/rxjs/rxjs.Observable.default.spec.ts
new file mode 100644
index 0000000000..0a305f3be2
--- /dev/null
+++ b/packages/zone.js/test/rxjs/rxjs.Observable.default.spec.ts
@@ -0,0 +1,40 @@
+/**
+ * @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
+ */
+import {Observable, of } from 'rxjs';
+import {defaultIfEmpty} from 'rxjs/operators';
+
+import {asyncTest} from '../test-util';
+
+describe('Observable.defaultIfEmpty', () => {
+ let log: any[];
+ let observable1: Observable;
+
+ beforeEach(() => { log = []; });
+
+ it('defaultIfEmpty func callback should run in the correct zone', asyncTest((done: any) => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 =
+ constructorZone1.run(() => { return of ().pipe(defaultIfEmpty('empty' as any)); });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ () => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual(['empty', 'completed']);
+ done();
+ });
+ });
+ }, Zone.root));
+});
diff --git a/packages/zone.js/test/rxjs/rxjs.Observable.delay.spec.ts b/packages/zone.js/test/rxjs/rxjs.Observable.delay.spec.ts
new file mode 100644
index 0000000000..ce8fb1575a
--- /dev/null
+++ b/packages/zone.js/test/rxjs/rxjs.Observable.delay.spec.ts
@@ -0,0 +1,62 @@
+/**
+ * @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
+ */
+
+import {Observable, of , timer} from 'rxjs';
+import {delay, delayWhen} from 'rxjs/operators';
+
+import {asyncTest} from '../test-util';
+
+describe('Observable.delay', () => {
+ let log: any[];
+ let observable1: Observable;
+
+ beforeEach(() => { log = []; });
+
+ it('delay func callback should run in the correct zone', asyncTest((done: any) => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 = constructorZone1.run(() => { return of (1, 2, 3).pipe(delay(100)); });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ () => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([1, 2, 3, 'completed']);
+ done();
+ });
+ });
+ }, Zone.root));
+
+ it('delayWhen func callback should run in the correct zone', asyncTest((done: any) => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 = constructorZone1.run(
+ () => { return of (1, 2, 3).pipe(delayWhen((v: any) => { return timer(v * 10); })); });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ () => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([1, 2, 3, 'completed']);
+ done();
+ });
+ });
+ }, Zone.root));
+});
diff --git a/packages/zone.js/test/rxjs/rxjs.Observable.distinct.spec.ts b/packages/zone.js/test/rxjs/rxjs.Observable.distinct.spec.ts
new file mode 100644
index 0000000000..45ddf751e5
--- /dev/null
+++ b/packages/zone.js/test/rxjs/rxjs.Observable.distinct.spec.ts
@@ -0,0 +1,87 @@
+/**
+ * @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
+ */
+
+import {Observable, of } from 'rxjs';
+import {distinct, distinctUntilChanged, distinctUntilKeyChanged} from 'rxjs/operators';
+
+describe('Observable.distinct', () => {
+ let log: any[];
+ let observable1: Observable;
+
+ beforeEach(() => { log = []; });
+
+ it('distinct func callback should run in the correct zone', () => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ const error = new Error('test');
+ observable1 = constructorZone1.run(
+ () => { return of (1, 1, 2, 2, 2, 1, 2, 3, 4, 3, 2, 1).pipe(distinct()); });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ });
+ });
+ expect(log).toEqual([1, 2, 3, 4, 'completed']);
+ });
+
+ it('distinctUntilChanged func callback should run in the correct zone', () => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ const error = new Error('test');
+ observable1 = constructorZone1.run(
+ () => { return of (1, 1, 2, 2, 2, 1, 1, 2, 3, 3, 4).pipe(distinctUntilChanged()); });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ });
+ });
+ expect(log).toEqual([1, 2, 1, 2, 3, 4, 'completed']);
+ });
+
+ it('distinctUntilKeyChanged func callback should run in the correct zone', () => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ const error = new Error('test');
+ observable1 = constructorZone1.run(() => {
+ return of ({age: 4, name: 'Foo'}, {age: 7, name: 'Bar'}, {age: 5, name: 'Foo'},
+ {age: 6, name: 'Foo'})
+ .pipe(distinctUntilKeyChanged('name'));
+ });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ });
+ });
+ expect(log).toEqual(
+ [{age: 4, name: 'Foo'}, {age: 7, name: 'Bar'}, {age: 5, name: 'Foo'}, 'completed']);
+ });
+});
diff --git a/packages/zone.js/test/rxjs/rxjs.Observable.do.spec.ts b/packages/zone.js/test/rxjs/rxjs.Observable.do.spec.ts
new file mode 100644
index 0000000000..a876b6d84c
--- /dev/null
+++ b/packages/zone.js/test/rxjs/rxjs.Observable.do.spec.ts
@@ -0,0 +1,45 @@
+/**
+ * @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
+ */
+import {Observable, of } from 'rxjs';
+import {tap} from 'rxjs/operators';
+
+describe('Observable.tap', () => {
+ let log: any[];
+ let observable1: Observable;
+
+ beforeEach(() => { log = []; });
+
+ it('do func callback should run in the correct zone', () => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const doZone1: Zone = Zone.current.fork({name: 'Do Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ const error = new Error('test');
+ observable1 = constructorZone1.run(() => { return of (1); });
+
+ observable1 = doZone1.run(() => {
+ return observable1.pipe(tap((v: any) => {
+ log.push(v);
+ expect(Zone.current.name).toEqual(doZone1.name);
+ }));
+ });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push('result' + result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([1, 'result1', 'completed']);
+ });
+ });
+ });
+});
diff --git a/packages/zone.js/test/rxjs/rxjs.Observable.map.spec.ts b/packages/zone.js/test/rxjs/rxjs.Observable.map.spec.ts
new file mode 100644
index 0000000000..7a0e606650
--- /dev/null
+++ b/packages/zone.js/test/rxjs/rxjs.Observable.map.spec.ts
@@ -0,0 +1,99 @@
+/**
+ * @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
+ */
+import {Observable, observable, of } from 'rxjs';
+import {pairwise, partition, pluck} from 'rxjs/operators';
+
+import {ifEnvSupports} from '../test-util';
+
+import {supportFeature} from './rxjs.util';
+
+describe('Observable.map', () => {
+ let log: any[];
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ let observable1: Observable;
+
+ beforeEach(() => { log = []; });
+
+ it('pairwise func callback should run in the correct zone', () => {
+ observable1 = constructorZone1.run(() => { return of (1, 2, 3).pipe(pairwise()); });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push(result);
+ },
+ () => { fail('should not call error'); },
+ () => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push('completed');
+ });
+ });
+
+ expect(log).toEqual([[1, 2], [2, 3], 'completed']);
+ });
+
+ it('partition func callback should run in the correct zone', () => {
+ const partitionZone = Zone.current.fork({name: 'Partition Zone1'});
+ const observable1: any = constructorZone1.run(() => { return of (1, 2, 3); });
+
+ const part: any = partitionZone.run(() => {
+ return observable1.pipe(partition((val: any) => {
+ expect(Zone.current.name).toEqual(partitionZone.name);
+ return val % 2 === 0;
+ }));
+ });
+
+ subscriptionZone.run(() => {
+ part[0].subscribe(
+ (result: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push('first' + result);
+ },
+ () => { fail('should not call error'); },
+ () => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push('completed');
+ });
+
+ part[1].subscribe(
+ (result: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push('second' + result);
+ },
+ () => { fail('should not call error'); },
+ () => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push('completed');
+ });
+ });
+
+ expect(log).toEqual(['first2', 'completed', 'second1', 'second3', 'completed']);
+ });
+
+ it('pluck func callback should run in the correct zone', () => {
+ observable1 =
+ constructorZone1.run(() => { return of ({a: 1, b: 2}, {a: 3, b: 4}).pipe(pluck('a')); });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push(result);
+ },
+ () => { fail('should not call error'); },
+ () => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push('completed');
+ });
+ });
+
+ expect(log).toEqual([1, 3, 'completed']);
+ });
+});
diff --git a/packages/zone.js/test/rxjs/rxjs.Observable.merge.spec.ts b/packages/zone.js/test/rxjs/rxjs.Observable.merge.spec.ts
new file mode 100644
index 0000000000..ccbcd8600f
--- /dev/null
+++ b/packages/zone.js/test/rxjs/rxjs.Observable.merge.spec.ts
@@ -0,0 +1,209 @@
+/**
+ * @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
+ */
+import {Observable, interval, merge, of , range} from 'rxjs';
+import {expand, map, mergeAll, mergeMap, mergeMapTo, switchAll, switchMap, switchMapTo, take} from 'rxjs/operators';
+
+import {asyncTest, ifEnvSupports} from '../test-util';
+
+import {supportFeature} from './rxjs.util';
+
+describe('Observable.merge', () => {
+ let log: any[];
+ let observable1: Observable;
+ let defaultTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
+
+ beforeEach(() => {
+ log = [];
+ jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
+ });
+
+ afterEach(() => { jasmine.DEFAULT_TIMEOUT_INTERVAL = defaultTimeout; });
+
+ it('expand func callback should run in the correct zone', () => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const expandZone1: Zone = Zone.current.fork({name: 'Expand Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ const error = new Error('test');
+ observable1 = constructorZone1.run(() => { return of (2); });
+
+ observable1 = expandZone1.run(() => {
+ return observable1.pipe(
+ expand((val: any) => {
+ expect(Zone.current.name).toEqual(expandZone1.name);
+ return of (1 + val);
+ }),
+ take(2));
+ });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ });
+ });
+ expect(log).toEqual([2, 3, 'completed']);
+ });
+
+ it('merge func callback should run in the correct zone', asyncTest((done: any) => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ const error = new Error('test');
+ observable1 = constructorZone1.run(
+ () => { return merge(interval(10).pipe(take(2)), interval(15).pipe(take(1))); });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([0, 0, 1, 'completed']);
+ done();
+ });
+ });
+ }, Zone.root));
+
+ it('mergeAll func callback should run in the correct zone', asyncTest((done: any) => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ const error = new Error('test');
+ observable1 = constructorZone1.run(
+ () => { return of (1, 2).pipe(map((v: any) => { return of (v + 1); }), mergeAll()); });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([2, 3, 'completed']);
+ done();
+ });
+ });
+ }, Zone.root));
+
+ it('mergeMap func callback should run in the correct zone', () => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ const error = new Error('test');
+ observable1 = constructorZone1.run(
+ () => { return of (1, 2).pipe(mergeMap((v: any) => { return of (v + 1); })); });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([2, 3, 'completed']);
+ });
+ });
+ });
+
+ it('mergeMapTo func callback should run in the correct zone', () => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ const error = new Error('test');
+ observable1 = constructorZone1.run(() => { return of (1, 2).pipe(mergeMapTo(of (10))); });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([10, 10, 'completed']);
+ });
+ });
+ });
+
+ it('switch func callback should run in the correct zone', () => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 = constructorZone1.run(() => {
+ return range(0, 3).pipe(map(function(x: any) { return range(x, 3); }), switchAll());
+ });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([0, 1, 2, 1, 2, 3, 2, 3, 4, 'completed']);
+ });
+ });
+ });
+
+ it('switchMap func callback should run in the correct zone', () => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 = constructorZone1.run(
+ () => { return range(0, 3).pipe(switchMap(function(x: any) { return range(x, 3); })); });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([0, 1, 2, 1, 2, 3, 2, 3, 4, 'completed']);
+ });
+ });
+ });
+
+ it('switchMapTo func callback should run in the correct zone', () => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 = constructorZone1.run(() => { return range(0, 3).pipe(switchMapTo('a')); });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual(['a', 'a', 'a', 'completed']);
+ });
+ });
+ });
+});
diff --git a/packages/zone.js/test/rxjs/rxjs.Observable.multicast.spec.ts b/packages/zone.js/test/rxjs/rxjs.Observable.multicast.spec.ts
new file mode 100644
index 0000000000..9e9bf0131a
--- /dev/null
+++ b/packages/zone.js/test/rxjs/rxjs.Observable.multicast.spec.ts
@@ -0,0 +1,68 @@
+/**
+ * @license
+ * Copyright Google Inc. All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+import {Observable, Subject, of } from 'rxjs';
+import {mapTo, multicast, tap} from 'rxjs/operators';
+
+
+// TODO: @JiaLiPassion, Observable.prototype.multicast return a readonly _subscribe
+// should find another way to patch subscribe
+describe('Observable.multicast', () => {
+ let log: any[];
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const doZone1: Zone = Zone.current.fork({name: 'Do Zone1'});
+ const mapZone1: Zone = Zone.current.fork({name: 'Map Zone1'});
+ const multicastZone1: Zone = Zone.current.fork({name: 'Multicast Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ let observable1: Observable;
+
+ beforeEach(() => { log = []; });
+
+ it('multicast func callback should run in the correct zone', () => {
+ observable1 = constructorZone1.run(() => { return of (1, 2, 3); });
+
+ observable1 = doZone1.run(() => {
+ return observable1.pipe(tap((v: any) => {
+ expect(Zone.current.name).toEqual(doZone1.name);
+ log.push('do' + v);
+ }));
+ });
+
+ observable1 = mapZone1.run(() => { return observable1.pipe(mapTo('test')); });
+
+ const multi: any = multicastZone1.run(() => {
+ return observable1.pipe(multicast(() => {
+ expect(Zone.current.name).toEqual(multicastZone1.name);
+ return new Subject();
+ }));
+ });
+
+ multi.subscribe((val: any) => { log.push('one' + val); });
+
+ multi.subscribe((val: any) => { log.push('two' + val); });
+
+ multi.connect();
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push(result);
+ },
+ () => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ });
+ });
+
+ expect(log).toEqual([
+ 'do1', 'onetest', 'twotest', 'do2', 'onetest', 'twotest', 'do3', 'onetest', 'twotest', 'do1',
+ 'test', 'do2', 'test', 'do3', 'test', 'completed'
+ ]);
+ });
+});
diff --git a/packages/zone.js/test/rxjs/rxjs.Observable.notification.spec.ts b/packages/zone.js/test/rxjs/rxjs.Observable.notification.spec.ts
new file mode 100644
index 0000000000..4fa381304c
--- /dev/null
+++ b/packages/zone.js/test/rxjs/rxjs.Observable.notification.spec.ts
@@ -0,0 +1,54 @@
+/**
+ * @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
+ */
+import {Notification, Observable, of } from 'rxjs';
+import {dematerialize} from 'rxjs/operators';
+
+import {asyncTest, ifEnvSupports} from '../test-util';
+
+const supportNotification = function() {
+ return typeof Notification !== 'undefined';
+};
+
+(supportNotification as any).message = 'RxNotification';
+
+describe('Observable.notification', ifEnvSupports(supportNotification, () => {
+ let log: any[];
+ let observable1: Observable;
+
+ beforeEach(() => { log = []; });
+
+ it('notification func callback should run in the correct zone', () => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ const error = new Error('test');
+ observable1 = constructorZone1.run(() => {
+ const notifA = new Notification('N' as any, 'A');
+ const notifB = new Notification('N' as any, 'B');
+ const notifE = new Notification('E' as any, void 0, error);
+ const materialized = of (notifA, notifB, notifE as any);
+ return materialized.pipe(dematerialize());
+ });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ (err: any) => {
+ log.push(err);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual(['A', 'B', error]);
+ });
+ });
+ });
+ }));
diff --git a/packages/zone.js/test/rxjs/rxjs.Observable.race.spec.ts b/packages/zone.js/test/rxjs/rxjs.Observable.race.spec.ts
new file mode 100644
index 0000000000..79ca6fdebe
--- /dev/null
+++ b/packages/zone.js/test/rxjs/rxjs.Observable.race.spec.ts
@@ -0,0 +1,41 @@
+/**
+ * @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
+ */
+import {Observable, interval, race} from 'rxjs';
+import {mapTo} from 'rxjs/operators';
+
+import {asyncTest} from '../test-util';
+
+describe('Observable.race', () => {
+ let log: any[];
+ let observable1: Observable;
+
+ beforeEach(() => { log = []; });
+
+ it('race func callback should run in the correct zone', asyncTest((done: any) => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 = constructorZone1.run(
+ () => { return race(interval(10).pipe(mapTo('a')), interval(15).pipe(mapTo('b'))); });
+
+ subscriptionZone.run(() => {
+ const subscriber: any = observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ subscriber.complete();
+ },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual(['a', 'completed']);
+ done();
+ });
+ });
+ }, Zone.root));
+});
diff --git a/packages/zone.js/test/rxjs/rxjs.Observable.sample.spec.ts b/packages/zone.js/test/rxjs/rxjs.Observable.sample.spec.ts
new file mode 100644
index 0000000000..f752faaf30
--- /dev/null
+++ b/packages/zone.js/test/rxjs/rxjs.Observable.sample.spec.ts
@@ -0,0 +1,67 @@
+/**
+ * @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
+ */
+import {Observable, interval} from 'rxjs';
+import {sample, take, throttle} from 'rxjs/operators';
+
+import {asyncTest} from '../test-util';
+
+describe('Observable.sample', () => {
+ let log: any[];
+ let observable1: Observable;
+
+ beforeEach(() => { log = []; });
+
+ it('sample func callback should run in the correct zone', asyncTest((done: any) => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 =
+ constructorZone1.run(() => { return interval(10).pipe(sample(interval(15))); });
+
+ subscriptionZone.run(() => {
+ const subscriber: any = observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ subscriber.complete();
+ },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([0, 'completed']);
+ done();
+ });
+ });
+ }, Zone.root));
+
+ xit('throttle func callback should run in the correct zone', asyncTest((done: any) => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 = constructorZone1.run(() => {
+ return interval(10).pipe(take(5), throttle((val: any) => {
+ expect(Zone.current.name).toEqual(constructorZone1.name);
+ return interval(20);
+ }));
+ });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([0, 2, 4, 'completed']);
+ done();
+ });
+ });
+ }, Zone.root));
+});
diff --git a/packages/zone.js/test/rxjs/rxjs.Observable.take.spec.ts b/packages/zone.js/test/rxjs/rxjs.Observable.take.spec.ts
new file mode 100644
index 0000000000..a84a8f9b27
--- /dev/null
+++ b/packages/zone.js/test/rxjs/rxjs.Observable.take.spec.ts
@@ -0,0 +1,115 @@
+/**
+ * @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
+ */
+import {Observable, interval, of } from 'rxjs';
+import {take, takeLast, takeUntil, takeWhile} from 'rxjs/operators';
+
+import {asyncTest} from '../test-util';
+
+describe('Observable.take', () => {
+ let log: any[];
+ let observable1: Observable;
+ let defaultTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
+
+ beforeEach(() => {
+ log = [];
+ jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
+ });
+
+ afterEach(() => { jasmine.DEFAULT_TIMEOUT_INTERVAL = defaultTimeout; });
+
+ it('take func callback should run in the correct zone', () => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 = constructorZone1.run(() => { return of (1, 2, 3).pipe(take(1)); });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([1, 'completed']);
+ });
+ });
+ });
+
+ it('takeLast func callback should run in the correct zone', () => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 = constructorZone1.run(() => { return of (1, 2, 3).pipe(takeLast(1)); });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([3, 'completed']);
+ });
+ });
+ });
+
+ xit('takeUntil func callback should run in the correct zone', asyncTest((done: any) => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 =
+ constructorZone1.run(() => { return interval(10).pipe(takeUntil(interval(25))); });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([0, 1, 'completed']);
+ done();
+ });
+ });
+ }, Zone.root));
+
+ it('takeWhile func callback should run in the correct zone', asyncTest((done: any) => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const takeZone1: Zone = Zone.current.fork({name: 'Take Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 = constructorZone1.run(() => { return interval(10); });
+
+ observable1 = takeZone1.run(() => {
+ return observable1.pipe(takeWhile((val: any) => {
+ expect(Zone.current.name).toEqual(takeZone1.name);
+ return val < 2;
+ }));
+ });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([0, 1, 'completed']);
+ done();
+ });
+ });
+ }, Zone.root));
+});
diff --git a/packages/zone.js/test/rxjs/rxjs.Observable.timeout.spec.ts b/packages/zone.js/test/rxjs/rxjs.Observable.timeout.spec.ts
new file mode 100644
index 0000000000..b110c30f9c
--- /dev/null
+++ b/packages/zone.js/test/rxjs/rxjs.Observable.timeout.spec.ts
@@ -0,0 +1,59 @@
+/**
+ * @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
+ */
+import {Observable, of } from 'rxjs';
+import {timeout} from 'rxjs/operators';
+
+import {asyncTest, isPhantomJS} from '../test-util';
+
+describe('Observable.timeout', () => {
+ let log: any[];
+ let observable1: Observable;
+
+ beforeEach(() => { log = []; });
+
+ it('timeout func callback should run in the correct zone', asyncTest((done: any) => {
+ if (isPhantomJS()) {
+ done();
+ return;
+ }
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 = constructorZone1.run(() => { return of (1).pipe(timeout(10)); });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([1, 'completed']);
+ done();
+ });
+ });
+ }, Zone.root));
+
+ it('promise should run in the correct zone', asyncTest((done: any) => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ const promise: any = constructorZone1.run(() => { return of (1).toPromise(); });
+
+ subscriptionZone.run(() => {
+ promise.then(
+ (result: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(result).toEqual(1);
+ done();
+ },
+ (err: any) => { fail('should not call error'); });
+ });
+ }, Zone.root));
+});
diff --git a/packages/zone.js/test/rxjs/rxjs.Observable.window.spec.ts b/packages/zone.js/test/rxjs/rxjs.Observable.window.spec.ts
new file mode 100644
index 0000000000..ddb09043cc
--- /dev/null
+++ b/packages/zone.js/test/rxjs/rxjs.Observable.window.spec.ts
@@ -0,0 +1,135 @@
+/**
+ * @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
+ */
+import {Observable, interval, timer} from 'rxjs';
+import {mergeAll, take, window, windowCount, windowToggle, windowWhen} from 'rxjs/operators';
+
+import {asyncTest} from '../test-util';
+
+
+// @JiaLiPassion, in Safari 9(iOS 9), the case is not
+// stable because of the timer, try to fix it later
+xdescribe('Observable.window', () => {
+ let log: any[];
+ let observable1: Observable;
+
+ beforeEach(() => { log = []; });
+
+ it('window func callback should run in the correct zone', asyncTest((done: any) => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ const error = new Error('test');
+ observable1 = constructorZone1.run(() => {
+ const source = timer(0, 10).pipe(take(6));
+ const w = source.pipe(window(interval(30)));
+ return w.pipe(mergeAll());
+ });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([0, 1, 2, 3, 4, 5, 'completed']);
+ done();
+ });
+ });
+ }, Zone.root));
+
+ it('windowCount func callback should run in the correct zone', asyncTest((done: any) => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ const error = new Error('test');
+ observable1 = constructorZone1.run(() => {
+ const source = timer(0, 10).pipe(take(10));
+ const window = source.pipe(windowCount(4));
+ return window.pipe(mergeAll());
+ });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 'completed']);
+ done();
+ });
+ });
+ }, Zone.root));
+
+ it('windowToggle func callback should run in the correct zone', asyncTest((done: any) => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const windowZone1: Zone = Zone.current.fork({name: 'Window Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ const error = new Error('test');
+ observable1 = constructorZone1.run(() => { return timer(0, 10).pipe(take(10)); });
+
+ windowZone1.run(() => {
+ return observable1.pipe(windowToggle(interval(30), (val: any) => {
+ expect(Zone.current.name).toEqual(windowZone1.name);
+ return interval(15);
+ }), mergeAll());
+ });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 'completed']);
+ done();
+ });
+ });
+ }, Zone.root));
+
+ it('windowWhen func callback should run in the correct zone', asyncTest((done: any) => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const windowZone1: Zone = Zone.current.fork({name: 'Window Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ const error = new Error('test');
+ observable1 = constructorZone1.run(() => { return timer(0, 10).pipe(take(10)); });
+
+ windowZone1.run(() => {
+ return observable1.pipe(
+ windowWhen(() => {
+ expect(Zone.current.name).toEqual(windowZone1.name);
+ return interval(15);
+ }),
+ mergeAll());
+ });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ (err: any) => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 'completed']);
+ done();
+ });
+ });
+ }, Zone.root));
+});
diff --git a/packages/zone.js/test/rxjs/rxjs.asap.spec.ts b/packages/zone.js/test/rxjs/rxjs.asap.spec.ts
new file mode 100644
index 0000000000..46822d7bdb
--- /dev/null
+++ b/packages/zone.js/test/rxjs/rxjs.asap.spec.ts
@@ -0,0 +1,59 @@
+/**
+ * @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
+ */
+
+import {asapScheduler, of } from 'rxjs';
+import {map, observeOn} from 'rxjs/operators';
+
+import {asyncTest} from '../test-util';
+
+describe('Scheduler.asap', () => {
+ let log: any[];
+ let errorCallback: Function;
+ const constructorZone: Zone = Zone.root.fork({name: 'Constructor Zone'});
+
+ beforeEach(() => { log = []; });
+
+ it('scheduler asap should run in correct zone', asyncTest((done: any) => {
+ let observable: any;
+ constructorZone.run(() => { observable = of (1, 2, 3).pipe(observeOn(asapScheduler)); });
+
+ const zone = Zone.current.fork({name: 'subscribeZone'});
+
+ zone.run(() => {
+ observable.pipe(map((value: number) => { return value; }))
+ .subscribe(
+ (value: number) => {
+ expect(Zone.current.name).toEqual(zone.name);
+ if (value === 3) {
+ setTimeout(done);
+ }
+ },
+ (err: any) => { fail('should not be here'); });
+ });
+ }, Zone.root));
+
+ it('scheduler asap error should run in correct zone', asyncTest((done: any) => {
+ let observable: any;
+ constructorZone.run(() => { observable = of (1, 2, 3).pipe(observeOn(asapScheduler)); });
+
+ Zone.root.run(() => {
+ observable
+ .pipe(map((value: number) => {
+ if (value === 3) {
+ throw new Error('oops');
+ }
+ return value;
+ }))
+ .subscribe((value: number) => {}, (err: any) => {
+ expect(err.message).toEqual('oops');
+ expect(Zone.current.name).toEqual('');
+ done();
+ });
+ });
+ }, Zone.root));
+});
diff --git a/packages/zone.js/test/rxjs/rxjs.bindCallback.spec.ts b/packages/zone.js/test/rxjs/rxjs.bindCallback.spec.ts
new file mode 100644
index 0000000000..b70b3896e2
--- /dev/null
+++ b/packages/zone.js/test/rxjs/rxjs.bindCallback.spec.ts
@@ -0,0 +1,86 @@
+/**
+ * @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
+ */
+
+import {asapScheduler, bindCallback} from 'rxjs';
+
+import {asyncTest} from '../test-util';
+
+describe('Observable.bindCallback', () => {
+ let log: any[];
+ const constructorZone: Zone = Zone.root.fork({name: 'Constructor Zone'});
+ const subscriptionZone: Zone = Zone.root.fork({name: 'Subscription Zone'});
+ let func: any;
+ let boundFunc: any;
+ let observable: any;
+
+ beforeEach(() => { log = []; });
+
+ it('bindCallback func callback should run in the correct zone', () => {
+ constructorZone.run(() => {
+ func = function(arg0: any, callback: Function) {
+ expect(Zone.current.name).toEqual(constructorZone.name);
+ callback(arg0);
+ };
+ boundFunc = bindCallback(func);
+ observable = boundFunc('test');
+ });
+
+ subscriptionZone.run(() => {
+ observable.subscribe((arg: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push('next' + arg);
+ });
+ });
+
+ expect(log).toEqual(['nexttest']);
+ });
+
+ it('bindCallback with selector should run in correct zone', () => {
+ constructorZone.run(() => {
+ func = function(arg0: any, callback: Function) {
+ expect(Zone.current.name).toEqual(constructorZone.name);
+ callback(arg0);
+ };
+ boundFunc = bindCallback(func, (arg: any) => {
+ expect(Zone.current.name).toEqual(constructorZone.name);
+ return 'selector' + arg;
+ });
+ observable = boundFunc('test');
+ });
+
+ subscriptionZone.run(() => {
+ observable.subscribe((arg: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push('next' + arg);
+ });
+ });
+
+ expect(log).toEqual(['nextselectortest']);
+ });
+
+ it('bindCallback with async scheduler should run in correct zone', asyncTest((done: any) => {
+ constructorZone.run(() => {
+ func = function(arg0: any, callback: Function) {
+ expect(Zone.current.name).toEqual(constructorZone.name);
+ callback(arg0);
+ };
+ boundFunc = bindCallback(func, () => true, asapScheduler);
+ observable = boundFunc('test');
+ });
+
+ subscriptionZone.run(() => {
+ observable.subscribe((arg: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push('next' + arg);
+ done();
+ });
+ });
+
+ expect(log).toEqual([]);
+ }, Zone.root));
+});
diff --git a/packages/zone.js/test/rxjs/rxjs.bindNodeCallback.spec.ts b/packages/zone.js/test/rxjs/rxjs.bindNodeCallback.spec.ts
new file mode 100644
index 0000000000..32ba049bd3
--- /dev/null
+++ b/packages/zone.js/test/rxjs/rxjs.bindNodeCallback.spec.ts
@@ -0,0 +1,108 @@
+/**
+ * @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
+ */
+
+import {Observable, asapScheduler, bindCallback, bindNodeCallback} from 'rxjs';
+
+import {asyncTest} from '../test-util';
+
+describe('Observable.bindNodeCallback', () => {
+ let log: any[];
+ const constructorZone: Zone = Zone.root.fork({name: 'Constructor Zone'});
+ const subscriptionZone: Zone = Zone.root.fork({name: 'Subscription Zone'});
+ let func: any;
+ let boundFunc: any;
+ let observable: any;
+
+ beforeEach(() => { log = []; });
+
+ it('bindNodeCallback func callback should run in the correct zone', () => {
+ constructorZone.run(() => {
+ func = function(arg: any, callback: (error: any, result: any) => any) {
+ expect(Zone.current.name).toEqual(constructorZone.name);
+ callback(null, arg);
+ };
+ boundFunc = bindNodeCallback(func);
+ observable = boundFunc('test');
+ });
+
+ subscriptionZone.run(() => {
+ observable.subscribe((arg: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push('next' + arg);
+ });
+ });
+
+ expect(log).toEqual(['nexttest']);
+ });
+
+ it('bindNodeCallback with selector should run in correct zone', () => {
+ constructorZone.run(() => {
+ func = function(arg: any, callback: (error: any, result: any) => any) {
+ expect(Zone.current.name).toEqual(constructorZone.name);
+ callback(null, arg);
+ };
+ boundFunc = bindNodeCallback(func, (arg: any) => {
+ expect(Zone.current.name).toEqual(constructorZone.name);
+ return 'selector' + arg;
+ });
+ observable = boundFunc('test');
+ });
+
+ subscriptionZone.run(() => {
+ observable.subscribe((arg: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push('next' + arg);
+ });
+ });
+
+ expect(log).toEqual(['nextselectortest']);
+ });
+
+ it('bindNodeCallback with async scheduler should run in correct zone', asyncTest((done: any) => {
+ constructorZone.run(() => {
+ func = function(arg: any, callback: (error: any, result: any) => any) {
+ expect(Zone.current.name).toEqual(constructorZone.name);
+ callback(null, arg);
+ };
+ boundFunc = bindCallback(func, () => true, asapScheduler);
+ observable = boundFunc('test');
+ });
+
+ subscriptionZone.run(() => {
+ observable.subscribe((arg: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push('next' + arg);
+ done();
+ });
+ });
+
+ expect(log).toEqual([]);
+ }));
+
+ it('bindNodeCallback call with error should run in correct zone', () => {
+ constructorZone.run(() => {
+ func = function(arg: any, callback: (error: any, result: any) => any) {
+ expect(Zone.current.name).toEqual(constructorZone.name);
+ callback(arg, null);
+ };
+ boundFunc = bindCallback(func);
+ observable = boundFunc('test');
+ });
+
+ subscriptionZone.run(() => {
+ observable.subscribe(
+ (arg: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push('next' + arg);
+ },
+ (error: any) => { log.push('error' + error); });
+ });
+
+ expect(log).toEqual(['nexttest,']);
+ });
+});
diff --git a/packages/zone.js/test/rxjs/rxjs.combineLatest.spec.ts b/packages/zone.js/test/rxjs/rxjs.combineLatest.spec.ts
new file mode 100644
index 0000000000..8c9fede437
--- /dev/null
+++ b/packages/zone.js/test/rxjs/rxjs.combineLatest.spec.ts
@@ -0,0 +1,86 @@
+/**
+ * @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
+ */
+
+import {Observable, combineLatest} from 'rxjs';
+
+describe('Observable.combineLatest', () => {
+ let log: any[];
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const constructorZone2: Zone = Zone.current.fork({name: 'Constructor Zone2'});
+ const constructorZone3: Zone = Zone.current.fork({name: 'Constructor Zone3'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ let observable1: Observable;
+ let observable2: any;
+ let subscriber1: any;
+ let subscriber2: any;
+
+ let combinedObservable: any;
+
+ beforeEach(() => { log = []; });
+
+ it('combineLatest func should run in the correct zone', () => {
+ observable1 = constructorZone1.run(() => new Observable((_subscriber) => {
+ subscriber1 = _subscriber;
+ expect(Zone.current.name).toEqual(constructorZone1.name);
+ log.push('setup1');
+ }));
+ observable2 = constructorZone2.run(() => new Observable((_subscriber) => {
+ subscriber2 = _subscriber;
+ expect(Zone.current.name).toEqual(constructorZone2.name);
+ log.push('setup2');
+ }));
+
+ constructorZone3.run(() => { combinedObservable = combineLatest(observable1, observable2); });
+
+ subscriptionZone.run(() => {
+ combinedObservable.subscribe((combined: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push(combined);
+ });
+ });
+
+ subscriber1.next(1);
+ subscriber2.next(2);
+ subscriber2.next(3);
+
+ expect(log).toEqual(['setup1', 'setup2', [1, 2], [1, 3]]);
+ });
+
+ it('combineLatest func with project function should run in the correct zone', () => {
+ observable1 = constructorZone1.run(() => new Observable((_subscriber) => {
+ subscriber1 = _subscriber;
+ expect(Zone.current.name).toEqual(constructorZone1.name);
+ log.push('setup1');
+ }));
+ observable2 = constructorZone2.run(() => new Observable((_subscriber) => {
+ subscriber2 = _subscriber;
+ expect(Zone.current.name).toEqual(constructorZone2.name);
+ log.push('setup2');
+ }));
+
+ constructorZone3.run(() => {
+ combinedObservable = combineLatest(observable1, observable2, (x: number, y: number) => {
+ expect(Zone.current.name).toEqual(constructorZone3.name);
+ return x + y;
+ });
+ });
+
+ subscriptionZone.run(() => {
+ combinedObservable.subscribe((combined: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push(combined);
+ });
+ });
+
+ subscriber1.next(1);
+ subscriber2.next(2);
+ subscriber2.next(3);
+
+ expect(log).toEqual(['setup1', 'setup2', 3, 4]);
+ });
+});
diff --git a/packages/zone.js/test/rxjs/rxjs.common.spec.ts b/packages/zone.js/test/rxjs/rxjs.common.spec.ts
new file mode 100644
index 0000000000..8e5f5db725
--- /dev/null
+++ b/packages/zone.js/test/rxjs/rxjs.common.spec.ts
@@ -0,0 +1,209 @@
+/**
+ * @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
+ */
+
+import {Observable, Subject} from 'rxjs';
+import {map} from 'rxjs/operators';
+
+/**
+ * The point of these tests, is to ensure that all callbacks execute in the Zone which was active
+ * when the callback was passed into the Rx.
+ *
+ * The implications are:
+ * - Observable callback passed into `Observable` executes in the same Zone as when the
+ * `new Observable` was invoked.
+ * - The subscription callbacks passed into `subscribe` execute in the same Zone as when the
+ * `subscribe` method was invoked.
+ * - The operator callbacks passe into `map`, etc..., execute in the same Zone as when the
+ * `operator` (`lift`) method was invoked.
+ */
+describe('Zone interaction', () => {
+ it('should run methods in the zone of declaration', () => {
+ const log: any[] = [];
+ const constructorZone: Zone = Zone.current.fork({name: 'Constructor Zone'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ let subscriber: any = null;
+ const observable: any =
+ constructorZone.run(() => new Observable((_subscriber: any) => {
+ subscriber = _subscriber;
+ log.push('setup');
+ expect(Zone.current.name).toEqual(constructorZone.name);
+ return () => {
+ expect(Zone.current.name).toEqual(constructorZone.name);
+ log.push('cleanup');
+ };
+ }));
+ subscriptionZone.run(
+ () => observable.subscribe(
+ () => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push('next');
+ },
+ (): any => null,
+ () => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push('complete');
+ }));
+ subscriber.next('MyValue');
+ subscriber.complete();
+
+ expect(log).toEqual(['setup', 'next', 'complete', 'cleanup']);
+ log.length = 0;
+
+ subscriptionZone.run(() => observable.subscribe((): any => null, () => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push('error');
+ }, (): any => null));
+ subscriber.next('MyValue');
+ subscriber.error('MyError');
+
+ expect(log).toEqual(['setup', 'error', 'cleanup']);
+ });
+
+ it('should run methods in the zone of declaration when nexting synchronously', () => {
+ const log: any[] = [];
+ const rootZone: Zone = Zone.current;
+ const constructorZone: Zone = Zone.current.fork({name: 'Constructor Zone'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ const observable: any =
+ constructorZone.run(() => new Observable((subscriber: any) => {
+ // Execute the `next`/`complete` in different zone, and assert that
+ // correct zone
+ // is restored.
+ rootZone.run(() => {
+ subscriber.next('MyValue');
+ subscriber.complete();
+ });
+ return () => {
+ expect(Zone.current.name).toEqual(constructorZone.name);
+ log.push('cleanup');
+ };
+ }));
+
+ subscriptionZone.run(
+ () => observable.subscribe(
+ () => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push('next');
+ },
+ (): any => null,
+ () => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push('complete');
+ }));
+
+ expect(log).toEqual(['next', 'complete', 'cleanup']);
+ });
+
+ it('should run operators in the zone of declaration', () => {
+ const log: any[] = [];
+ const rootZone: Zone = Zone.current;
+ const constructorZone: Zone = Zone.current.fork({name: 'Constructor Zone'});
+ const operatorZone: Zone = Zone.current.fork({name: 'Operator Zone'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ let observable: any =
+ constructorZone.run(() => new Observable((subscriber: any) => {
+ // Execute the `next`/`complete` in different zone, and assert that
+ // correct zone
+ // is restored.
+ rootZone.run(() => {
+ subscriber.next('MyValue');
+ subscriber.complete();
+ });
+ return () => {
+ expect(Zone.current.name).toEqual(constructorZone.name);
+ log.push('cleanup');
+ };
+ }));
+
+ observable = operatorZone.run(() => observable.pipe(map((value: any) => {
+ expect(Zone.current.name).toEqual(operatorZone.name);
+ log.push('map: ' + value);
+ return value;
+ })));
+
+ subscriptionZone.run(
+ () => observable.subscribe(
+ () => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push('next');
+ },
+ (e: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push('error: ' + e);
+ },
+ () => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push('complete');
+ }));
+
+ expect(log).toEqual(['map: MyValue', 'next', 'complete', 'cleanup']);
+ });
+
+ it('should run subscribe in zone of declaration with Observable.create', () => {
+ const log: any[] = [];
+ const constructorZone: Zone = Zone.current.fork({name: 'Constructor Zone'});
+ let observable: any = constructorZone.run(() => Observable.create((subscriber: any) => {
+ expect(Zone.current.name).toEqual(constructorZone.name);
+ subscriber.next(1);
+ subscriber.complete();
+ return () => {
+ expect(Zone.current.name).toEqual(constructorZone.name);
+ log.push('cleanup');
+ };
+ }));
+
+ observable.subscribe(() => { log.push('next'); });
+
+ expect(log).toEqual(['next', 'cleanup']);
+ });
+
+ it('should run in the zone when subscribe is called to the same Subject', () => {
+ const log: any[] = [];
+ const constructorZone: Zone = Zone.current.fork({name: 'Constructor Zone'});
+ const subscriptionZone1: Zone = Zone.current.fork({name: 'Subscription Zone 1'});
+ const subscriptionZone2: Zone = Zone.current.fork({name: 'Subscription Zone 2'});
+
+ let subject: any;
+
+ constructorZone.run(() => { subject = new Subject(); });
+
+ let subscription1: any;
+ let subscription2: any;
+
+ subscriptionZone1.run(() => {
+ subscription1 = subject.subscribe(
+ () => {
+ expect(Zone.current.name).toEqual(subscriptionZone1.name);
+ log.push('next1');
+ },
+ () => {},
+ () => {
+ expect(Zone.current.name).toEqual(subscriptionZone1.name);
+ log.push('complete1');
+ });
+ });
+
+ subscriptionZone2.run(() => {
+ subscription2 = subject.subscribe(
+ () => {
+ expect(Zone.current.name).toEqual(subscriptionZone2.name);
+ log.push('next2');
+ },
+ () => {},
+ () => {
+ expect(Zone.current.name).toEqual(subscriptionZone2.name);
+ log.push('complete2');
+ });
+ });
+
+ subject.next(1);
+ subject.complete();
+
+ expect(log).toEqual(['next1', 'next2', 'complete1', 'complete2']);
+ });
+});
diff --git a/packages/zone.js/test/rxjs/rxjs.concat.spec.ts b/packages/zone.js/test/rxjs/rxjs.concat.spec.ts
new file mode 100644
index 0000000000..6fef1a0662
--- /dev/null
+++ b/packages/zone.js/test/rxjs/rxjs.concat.spec.ts
@@ -0,0 +1,86 @@
+/**
+ * @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
+ */
+
+import {Observable, asapScheduler, concat, range} from 'rxjs';
+
+import {asyncTest} from '../test-util';
+
+describe('Observable.concat', () => {
+ let log: any[];
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const constructorZone2: Zone = Zone.current.fork({name: 'Constructor Zone2'});
+ const constructorZone3: Zone = Zone.current.fork({name: 'Constructor Zone3'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ let observable1: Observable;
+ let observable2: any;
+
+ let concatObservable: any;
+
+ beforeEach(() => { log = []; });
+
+ it('concat func callback should run in the correct zone', () => {
+ observable1 = constructorZone1.run(() => {
+ return new Observable(subscriber => {
+ expect(Zone.current.name).toEqual(constructorZone1.name);
+ subscriber.next(1);
+ subscriber.next(2);
+ subscriber.complete();
+ });
+ });
+
+ observable2 = constructorZone2.run(() => { return range(3, 4); });
+
+ constructorZone3.run(() => { concatObservable = concat(observable1, observable2); });
+
+ subscriptionZone.run(() => {
+ concatObservable.subscribe((concat: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push(concat);
+ });
+ });
+
+ expect(log).toEqual([1, 2, 3, 4, 5, 6]);
+ });
+
+ it('concat func callback should run in the correct zone with scheduler',
+ asyncTest((done: any) => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const constructorZone2: Zone = Zone.current.fork({name: 'Constructor Zone2'});
+ const constructorZone3: Zone = Zone.current.fork({name: 'Constructor Zone3'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 = constructorZone1.run(() => {
+ return new Observable(subscriber => {
+ expect(Zone.current.name).toEqual(constructorZone1.name);
+ subscriber.next(1);
+ subscriber.next(2);
+ subscriber.complete();
+ });
+ });
+
+ observable2 = constructorZone2.run(() => { return range(3, 4); });
+
+ constructorZone3.run(
+ () => { concatObservable = concat(observable1, observable2, asapScheduler); });
+
+ subscriptionZone.run(() => {
+ concatObservable.subscribe(
+ (concat: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push(concat);
+ },
+ (error: any) => { fail('subscribe failed' + error); },
+ () => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([1, 2, 3, 4, 5, 6]);
+ done();
+ });
+ });
+
+ expect(log).toEqual([]);
+ }, Zone.root));
+});
diff --git a/packages/zone.js/test/rxjs/rxjs.defer.spec.ts b/packages/zone.js/test/rxjs/rxjs.defer.spec.ts
new file mode 100644
index 0000000000..386667156c
--- /dev/null
+++ b/packages/zone.js/test/rxjs/rxjs.defer.spec.ts
@@ -0,0 +1,44 @@
+/**
+ * @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
+ */
+
+import {Observable, defer} from 'rxjs';
+
+describe('Observable.defer', () => {
+ let log: any[];
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ let observable1: Observable;
+
+ beforeEach(() => { log = []; });
+
+ it('defer func callback should run in the correct zone', () => {
+ observable1 = constructorZone1.run(() => {
+ return defer(() => {
+ return new Observable(subscribe => {
+ log.push('setup');
+ expect(Zone.current.name).toEqual(constructorZone1.name);
+ subscribe.next(1);
+ subscribe.complete();
+ return () => {
+ expect(Zone.current.name).toEqual(constructorZone1.name);
+ log.push('cleanup');
+ };
+ });
+ });
+ });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe((result: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push(result);
+ });
+ });
+
+ expect(log).toEqual(['setup', 1, 'cleanup']);
+ });
+});
diff --git a/packages/zone.js/test/rxjs/rxjs.empty.spec.ts b/packages/zone.js/test/rxjs/rxjs.empty.spec.ts
new file mode 100644
index 0000000000..bb9fbd8008
--- /dev/null
+++ b/packages/zone.js/test/rxjs/rxjs.empty.spec.ts
@@ -0,0 +1,28 @@
+/**
+ * @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
+ */
+import {Observable, empty} from 'rxjs';
+
+describe('Observable.empty', () => {
+ let log: any[];
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ let observable1: Observable;
+
+ beforeEach(() => { log = []; });
+
+ it('empty func callback should run in the correct zone', () => {
+ observable1 = constructorZone1.run(() => { return empty(); });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => { fail('should not call next'); },
+ () => { fail('should not call error'); },
+ () => { expect(Zone.current.name).toEqual(subscriptionZone.name); });
+ });
+ });
+});
diff --git a/packages/zone.js/test/rxjs/rxjs.forkjoin.spec.ts b/packages/zone.js/test/rxjs/rxjs.forkjoin.spec.ts
new file mode 100644
index 0000000000..8c05136e86
--- /dev/null
+++ b/packages/zone.js/test/rxjs/rxjs.forkjoin.spec.ts
@@ -0,0 +1,61 @@
+/**
+ * @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
+ */
+
+import {Observable, forkJoin, from, range} from 'rxjs';
+
+describe('Observable.forkjoin', () => {
+ let log: any[];
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ let observable1: Observable;
+
+ beforeEach(() => { log = []; });
+
+ it('forkjoin func callback should run in the correct zone', () => {
+ observable1 = constructorZone1.run(() => { return forkJoin(range(1, 2), from([4, 5])); });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push(result);
+ },
+ () => { fail('should not call error'); },
+ () => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push('completed');
+ });
+ });
+
+ expect(log).toEqual([[2, 5], 'completed']);
+ });
+
+ it('forkjoin func callback with selector should run in the correct zone', () => {
+ observable1 = constructorZone1.run(() => {
+ return forkJoin(range(1, 2), from([4, 5]), (x: number, y: number) => {
+ expect(Zone.current.name).toEqual(constructorZone1.name);
+ return x + y;
+ });
+ });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push(result);
+ },
+ () => { fail('should not call error'); },
+ () => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push('completed');
+ });
+ });
+
+ expect(log).toEqual([7, 'completed']);
+ });
+});
diff --git a/packages/zone.js/test/rxjs/rxjs.from.spec.ts b/packages/zone.js/test/rxjs/rxjs.from.spec.ts
new file mode 100644
index 0000000000..dc4b357a2e
--- /dev/null
+++ b/packages/zone.js/test/rxjs/rxjs.from.spec.ts
@@ -0,0 +1,81 @@
+/**
+ * @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
+ */
+import {Observable, from} from 'rxjs';
+
+import {asyncTest} from '../test-util';
+
+describe('Observable.from', () => {
+ let log: any[];
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ let observable1: Observable;
+
+ beforeEach(() => { log = []; });
+
+ it('from array should run in the correct zone', () => {
+ observable1 = constructorZone1.run(() => { return from([1, 2]); });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push(result);
+ },
+ () => { fail('should not call error'); },
+ () => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push('completed');
+ });
+ });
+
+ expect(log).toEqual([1, 2, 'completed']);
+ });
+
+ it('from array like object should run in the correct zone', () => {
+ observable1 = constructorZone1.run(() => { return from('foo'); });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push(result);
+ },
+ () => { fail('should not call error'); },
+ () => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push('completed');
+ });
+ });
+
+ expect(log).toEqual(['f', 'o', 'o', 'completed']);
+ });
+
+ it('from promise object should run in the correct zone', asyncTest((done: any) => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 = constructorZone1.run(
+ () => { return from(new Promise((resolve, reject) => { resolve(1); })); });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push(result);
+ },
+ (error: any) => { fail('should not call error' + error); },
+ () => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push('completed');
+ expect(log).toEqual([1, 'completed']);
+ done();
+ });
+ });
+
+ expect(log).toEqual([]);
+ }, Zone.root));
+});
diff --git a/packages/zone.js/test/rxjs/rxjs.fromEvent.spec.ts b/packages/zone.js/test/rxjs/rxjs.fromEvent.spec.ts
new file mode 100644
index 0000000000..b00c2d3102
--- /dev/null
+++ b/packages/zone.js/test/rxjs/rxjs.fromEvent.spec.ts
@@ -0,0 +1,95 @@
+/**
+ * @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
+ */
+
+import {Observable, fromEvent, fromEventPattern} from 'rxjs';
+
+import {isBrowser} from '../../lib/common/utils';
+import {ifEnvSupports} from '../test-util';
+
+function isEventTarget() {
+ return isBrowser;
+}
+
+(isEventTarget as any).message = 'EventTargetTest';
+
+describe('Observable.fromEvent', () => {
+ let log: any[];
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ const triggerZone: Zone = Zone.current.fork({name: 'Trigger Zone'});
+ let observable1: Observable;
+
+ beforeEach(() => { log = []; });
+
+ it('fromEvent EventTarget func callback should run in the correct zone',
+ ifEnvSupports(isEventTarget, () => {
+ observable1 = constructorZone1.run(() => { return fromEvent(document, 'click'); });
+
+ const clickEvent = document.createEvent('Event');
+ clickEvent.initEvent('click', true, true);
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push(result);
+ },
+ () => { fail('should not call error'); },
+ () => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push('completed');
+ });
+ });
+
+ triggerZone.run(() => { document.dispatchEvent(clickEvent); });
+
+ expect(log).toEqual([clickEvent]);
+ }));
+
+ it('fromEventPattern EventTarget func callback should run in the correct zone',
+ ifEnvSupports(isEventTarget, () => {
+ const button = document.createElement('button');
+ document.body.appendChild(button);
+ observable1 = constructorZone1.run(() => {
+ return fromEventPattern(
+ (handler: any) => {
+ expect(Zone.current.name).toEqual(constructorZone1.name);
+ button.addEventListener('click', handler);
+ log.push('addListener');
+ },
+ (handler: any) => {
+ expect(Zone.current.name).toEqual(constructorZone1.name);
+ button.removeEventListener('click', handler);
+ document.body.removeChild(button);
+ log.push('removeListener');
+ });
+ });
+
+ const clickEvent = document.createEvent('Event');
+ clickEvent.initEvent('click', false, false);
+
+ const subscriper: any = subscriptionZone.run(() => {
+ return observable1.subscribe(
+ (result: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push(result);
+ },
+ () => { fail('should not call error'); },
+ () => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push('completed');
+ });
+ });
+
+ triggerZone.run(() => {
+ button.dispatchEvent(clickEvent);
+ subscriper.complete();
+ });
+ expect(log).toEqual(['addListener', clickEvent, 'completed', 'removeListener']);
+ }));
+});
diff --git a/packages/zone.js/test/rxjs/rxjs.fromPromise.spec.ts b/packages/zone.js/test/rxjs/rxjs.fromPromise.spec.ts
new file mode 100644
index 0000000000..dfb2db4965
--- /dev/null
+++ b/packages/zone.js/test/rxjs/rxjs.fromPromise.spec.ts
@@ -0,0 +1,41 @@
+/**
+ * @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
+ */
+
+import {from} from 'rxjs';
+import {asyncTest} from '../test-util';
+
+describe('Observable.fromPromise', () => {
+ let log: any[];
+ let observable1: any;
+
+ beforeEach(() => { log = []; });
+
+ it('fromPromise func callback should run in the correct zone', asyncTest((done: any) => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const promiseZone1: Zone = Zone.current.fork({name: 'Promise Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ let res: any;
+ let promise: any =
+ promiseZone1.run(() => { return new Promise((resolve, reject) => { res = resolve; }); });
+ observable1 = constructorZone1.run(() => { return from(promise); });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push(result);
+ expect(log).toEqual([1]);
+ done();
+ },
+ () => { fail('should not call error'); }, () => {});
+ });
+ res(1);
+
+ expect(log).toEqual([]);
+ }, Zone.root));
+});
diff --git a/packages/zone.js/test/rxjs/rxjs.interval.spec.ts b/packages/zone.js/test/rxjs/rxjs.interval.spec.ts
new file mode 100644
index 0000000000..449b3a3249
--- /dev/null
+++ b/packages/zone.js/test/rxjs/rxjs.interval.spec.ts
@@ -0,0 +1,37 @@
+/**
+ * @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
+ */
+import {Observable, interval} from 'rxjs';
+
+import {asyncTest} from '../test-util';
+
+describe('Observable.interval', () => {
+ let log: any[];
+ let observable1: Observable;
+
+ beforeEach(() => { log = []; });
+
+ it('interval func callback should run in the correct zone', asyncTest((done: any) => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 = constructorZone1.run(() => { return interval(10); });
+
+ subscriptionZone.run(() => {
+ const subscriber = observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ if (result >= 3) {
+ subscriber.unsubscribe();
+ expect(log).toEqual([0, 1, 2, 3]);
+ done();
+ }
+ },
+ () => { fail('should not call error'); }, () => {});
+ });
+ }, Zone.root));
+});
diff --git a/packages/zone.js/test/rxjs/rxjs.merge.spec.ts b/packages/zone.js/test/rxjs/rxjs.merge.spec.ts
new file mode 100644
index 0000000000..3ffc465612
--- /dev/null
+++ b/packages/zone.js/test/rxjs/rxjs.merge.spec.ts
@@ -0,0 +1,47 @@
+/**
+ * @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
+ */
+import {Observable, interval, merge} from 'rxjs';
+import {map, take} from 'rxjs/operators';
+
+import {asyncTest} from '../test-util';
+
+describe('Observable.merge', () => {
+ let log: any[];
+
+ beforeEach(() => { log = []; });
+
+ it('merge func callback should run in the correct zone', asyncTest((done: any) => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const constructorZone2: Zone = Zone.current.fork({name: 'Constructor Zone2'});
+ const constructorZone3: Zone = Zone.current.fork({name: 'Constructor Zone3'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ const observable1: any = constructorZone1.run(
+ () => { return interval(8).pipe(map(v => 'observable1' + v), take(1)); });
+
+ const observable2: any = constructorZone2.run(
+ () => { return interval(10).pipe(map(v => 'observable2' + v), take(1)); });
+
+ const observable3: any =
+ constructorZone3.run(() => { return merge(observable1, observable2); });
+
+ subscriptionZone.run(() => {
+ const subscriber = observable3.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ () => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual(['observable10', 'observable20', 'completed']);
+ done();
+ });
+ });
+ }, Zone.root));
+});
diff --git a/packages/zone.js/test/rxjs/rxjs.never.spec.ts b/packages/zone.js/test/rxjs/rxjs.never.spec.ts
new file mode 100644
index 0000000000..32b1c9b42b
--- /dev/null
+++ b/packages/zone.js/test/rxjs/rxjs.never.spec.ts
@@ -0,0 +1,33 @@
+/**
+ * @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
+ */
+import {NEVER, Observable} from 'rxjs';
+import {startWith} from 'rxjs/operators';
+
+describe('Observable.never', () => {
+ let log: any[];
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ let observable1: Observable;
+
+ beforeEach(() => { log = []; });
+
+ it('never func callback should run in the correct zone', () => {
+ observable1 = constructorZone1.run(() => { return NEVER.pipe(startWith(7)); });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push(result);
+ },
+ () => { fail('should not call error'); }, () => { fail('should not call complete'); });
+ });
+
+ expect(log).toEqual([7]);
+ });
+});
diff --git a/packages/zone.js/test/rxjs/rxjs.of.spec.ts b/packages/zone.js/test/rxjs/rxjs.of.spec.ts
new file mode 100644
index 0000000000..c48353916c
--- /dev/null
+++ b/packages/zone.js/test/rxjs/rxjs.of.spec.ts
@@ -0,0 +1,36 @@
+/**
+ * @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
+ */
+import {Observable, of } from 'rxjs';
+
+describe('Observable.of', () => {
+ let log: any[];
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ let observable1: Observable;
+
+ beforeEach(() => { log = []; });
+
+ it('of func callback should run in the correct zone', () => {
+ observable1 = constructorZone1.run(() => { return of (1, 2, 3); });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push(result);
+ },
+ () => { fail('should not call error'); },
+ () => {
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ log.push('completed');
+ });
+ });
+
+ expect(log).toEqual([1, 2, 3, 'completed']);
+ });
+});
diff --git a/packages/zone.js/test/rxjs/rxjs.range.spec.ts b/packages/zone.js/test/rxjs/rxjs.range.spec.ts
new file mode 100644
index 0000000000..4625e77e06
--- /dev/null
+++ b/packages/zone.js/test/rxjs/rxjs.range.spec.ts
@@ -0,0 +1,61 @@
+/**
+ * @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
+ */
+import {Observable, asapScheduler, range} from 'rxjs';
+
+import {asyncTest} from '../test-util';
+
+describe('Observable.range', () => {
+ let log: any[];
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ let observable1: Observable;
+
+ beforeEach(() => { log = []; });
+
+ it('range func callback should run in the correct zone', () => {
+ observable1 = constructorZone1.run(() => { return range(1, 3); });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ () => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ });
+ });
+
+ expect(log).toEqual([1, 2, 3, 'completed']);
+ });
+
+ it('range func callback should run in the correct zone with scheduler', asyncTest((done: any) => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 = constructorZone1.run(() => { return range(1, 3, asapScheduler); });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ () => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([1, 2, 3, 'completed']);
+ done();
+ });
+ });
+
+ expect(log).toEqual([]);
+ }, Zone.root));
+});
diff --git a/packages/zone.js/test/rxjs/rxjs.spec.ts b/packages/zone.js/test/rxjs/rxjs.spec.ts
new file mode 100644
index 0000000000..a1feb09306
--- /dev/null
+++ b/packages/zone.js/test/rxjs/rxjs.spec.ts
@@ -0,0 +1,54 @@
+/**
+ * @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
+ */
+(Object as any).setPrototypeOf = (Object as any).setPrototypeOf || function(obj: any, proto: any) {
+ obj.__proto__ = proto;
+ return obj;
+};
+import '../../lib/rxjs/rxjs';
+import './rxjs.common.spec';
+import './rxjs.asap.spec';
+import './rxjs.bindCallback.spec';
+import './rxjs.bindNodeCallback.spec';
+import './rxjs.combineLatest.spec';
+import './rxjs.concat.spec';
+import './rxjs.defer.spec';
+import './rxjs.empty.spec';
+import './rxjs.forkjoin.spec';
+import './rxjs.from.spec';
+import './rxjs.fromEvent.spec';
+import './rxjs.fromPromise.spec';
+import './rxjs.interval.spec';
+import './rxjs.merge.spec';
+import './rxjs.never.spec';
+import './rxjs.of.spec';
+import './rxjs.range.spec';
+import './rxjs.throw.spec';
+import './rxjs.timer.spec';
+import './rxjs.zip.spec';
+import './rxjs.Observable.audit.spec';
+import './rxjs.Observable.buffer.spec';
+import './rxjs.Observable.catch.spec';
+import './rxjs.Observable.combine.spec';
+import './rxjs.Observable.concat.spec';
+import './rxjs.Observable.count.spec';
+import './rxjs.Observable.debounce.spec';
+import './rxjs.Observable.default.spec';
+import './rxjs.Observable.delay.spec';
+import './rxjs.Observable.notification.spec';
+import './rxjs.Observable.distinct.spec';
+import './rxjs.Observable.do.spec';
+import './rxjs.Observable.collection.spec';
+// // TODO: @JiaLiPassion, add exhaust test
+import './rxjs.Observable.merge.spec';
+import './rxjs.Observable.multicast.spec';
+import './rxjs.Observable.map.spec';
+import './rxjs.Observable.race.spec';
+import './rxjs.Observable.sample.spec';
+import './rxjs.Observable.take.spec';
+import './rxjs.Observable.timeout.spec';
+import './rxjs.Observable.window.spec';
diff --git a/packages/zone.js/test/rxjs/rxjs.throw.spec.ts b/packages/zone.js/test/rxjs/rxjs.throw.spec.ts
new file mode 100644
index 0000000000..1bc0013a18
--- /dev/null
+++ b/packages/zone.js/test/rxjs/rxjs.throw.spec.ts
@@ -0,0 +1,57 @@
+/**
+ * @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
+ */
+import {Observable, asapScheduler, throwError} from 'rxjs';
+
+import {asyncTest} from '../test-util';
+
+describe('Observable.throw', () => {
+ let log: any[];
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ let observable1: Observable;
+
+ beforeEach(() => { log = []; });
+
+ it('throw func callback should run in the correct zone', () => {
+ let error = new Error('test');
+ observable1 = constructorZone1.run(() => { return throwError(error); });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => { fail('should not call next'); },
+ (error: any) => {
+ log.push(error);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ () => { fail('should not call complete'); });
+ });
+
+ expect(log).toEqual([error]);
+ });
+
+ it('throw func callback should run in the correct zone with scheduler', asyncTest((done: any) => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ let error = new Error('test');
+ observable1 = constructorZone1.run(() => { return throwError(error, asapScheduler); });
+
+ subscriptionZone.run(() => {
+ observable1.subscribe(
+ (result: any) => { fail('should not call next'); },
+ (error: any) => {
+ log.push(error);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([error]);
+ done();
+ },
+ () => { fail('should not call complete'); });
+ });
+
+ expect(log).toEqual([]);
+ }, Zone.root));
+});
diff --git a/packages/zone.js/test/rxjs/rxjs.timer.spec.ts b/packages/zone.js/test/rxjs/rxjs.timer.spec.ts
new file mode 100644
index 0000000000..deac3916b8
--- /dev/null
+++ b/packages/zone.js/test/rxjs/rxjs.timer.spec.ts
@@ -0,0 +1,40 @@
+/**
+ * @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
+ */
+import {Observable, timer} from 'rxjs';
+import {asyncTest} from '../test-util';
+
+describe('Observable.timer', () => {
+ let log: any[];
+ let observable1: Observable;
+
+ beforeEach(() => { log = []; });
+
+ it('timer func callback should run in the correct zone', asyncTest((done: any) => {
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+ observable1 = constructorZone1.run(() => { return timer(10, 20); });
+
+ subscriptionZone.run(() => {
+ const subscriber = observable1.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ if (result >= 3) {
+ // subscriber.complete();
+ subscriber.unsubscribe();
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ expect(log).toEqual([0, 1, 2, 3, 'completed']);
+ done();
+ }
+ },
+ () => { fail('should not call error'); });
+ expect(log).toEqual([]);
+ });
+ }, Zone.root));
+});
diff --git a/packages/zone.js/test/rxjs/rxjs.util.ts b/packages/zone.js/test/rxjs/rxjs.util.ts
new file mode 100644
index 0000000000..036ecc5bb9
--- /dev/null
+++ b/packages/zone.js/test/rxjs/rxjs.util.ts
@@ -0,0 +1,12 @@
+/**
+ * @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
+ */
+
+export function supportFeature(Observable: any, method: string) {
+ const func = function() { return !!Observable.prototype[method]; };
+ (func as any).message = `Observable.${method} not support`;
+}
diff --git a/packages/zone.js/test/rxjs/rxjs.zip.spec.ts b/packages/zone.js/test/rxjs/rxjs.zip.spec.ts
new file mode 100644
index 0000000000..0081d5f793
--- /dev/null
+++ b/packages/zone.js/test/rxjs/rxjs.zip.spec.ts
@@ -0,0 +1,44 @@
+/**
+ * @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
+ */
+
+import {of , range, zip} from 'rxjs';
+
+describe('Observable.zip', () => {
+ let log: any[];
+ const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'});
+ const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'});
+
+ beforeEach(() => { log = []; });
+
+ it('zip func callback should run in the correct zone', () => {
+ const observable1: any = constructorZone1.run(() => { return range(1, 3); });
+ const observable2: any = constructorZone1.run(() => { return of ('foo', 'bar', 'beer'); });
+
+ const observable3: any = constructorZone1.run(() => {
+ return zip(observable1, observable2, function(n: number, str: string) {
+ expect(Zone.current.name).toEqual(constructorZone1.name);
+ return {n: n, str: str};
+ });
+ });
+
+ subscriptionZone.run(() => {
+ observable3.subscribe(
+ (result: any) => {
+ log.push(result);
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ },
+ () => { fail('should not call error'); },
+ () => {
+ log.push('completed');
+ expect(Zone.current.name).toEqual(subscriptionZone.name);
+ });
+ });
+
+ expect(log).toEqual([{n: 1, str: 'foo'}, {n: 2, str: 'bar'}, {n: 3, str: 'beer'}, 'completed']);
+ });
+});
diff --git a/packages/zone.js/test/saucelabs.js b/packages/zone.js/test/saucelabs.js
new file mode 100644
index 0000000000..a2f6b7be4f
--- /dev/null
+++ b/packages/zone.js/test/saucelabs.js
@@ -0,0 +1,9 @@
+/**
+ * @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
+ */
+
+window.saucelabs = true;
\ No newline at end of file
diff --git a/packages/zone.js/test/test-env-setup-jasmine-no-patch-clock.ts b/packages/zone.js/test/test-env-setup-jasmine-no-patch-clock.ts
new file mode 100644
index 0000000000..3697af8c9d
--- /dev/null
+++ b/packages/zone.js/test/test-env-setup-jasmine-no-patch-clock.ts
@@ -0,0 +1,8 @@
+/**
+ * @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
+ */
+(global as any)[(global as any).Zone.__symbol__('fakeAsyncAutoFakeAsyncWhenClockPatched')] = false;
diff --git a/packages/zone.js/test/test-env-setup-jasmine.ts b/packages/zone.js/test/test-env-setup-jasmine.ts
new file mode 100644
index 0000000000..85ea4d9d93
--- /dev/null
+++ b/packages/zone.js/test/test-env-setup-jasmine.ts
@@ -0,0 +1,9 @@
+/**
+ * @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
+ */
+
+(jasmine).DEFAULT_TIMEOUT_INTERVAL = 5000;
diff --git a/packages/zone.js/test/test-env-setup-mocha.ts b/packages/zone.js/test/test-env-setup-mocha.ts
new file mode 100644
index 0000000000..b3fa41cdc6
--- /dev/null
+++ b/packages/zone.js/test/test-env-setup-mocha.ts
@@ -0,0 +1,186 @@
+/**
+ * @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
+ */
+
+import '../lib/mocha/mocha';
+declare const global: any;
+
+((context: any) => {
+ context['jasmine'] = context['jasmine'] || {};
+ context['jasmine'].createSpy = function(spyName: string) {
+ let spy: any = function(...params: any[]) {
+ spy.countCall++;
+ spy.callArgs = params;
+ };
+
+ spy.countCall = 0;
+
+ return spy;
+ };
+
+ function eq(a: any, b: any) {
+ if (a === b) {
+ return true;
+ } else if (Array.isArray(a) && Array.isArray(b)) {
+ if (a.length !== b.length) {
+ return false;
+ }
+
+ let isEqual = true;
+
+ for (let prop in a) {
+ if (a.hasOwnProperty(prop)) {
+ if (!eq(a[prop], b[prop])) {
+ isEqual = false;
+ break;
+ }
+ }
+ }
+
+ return isEqual;
+ } else if (typeof a === 'object' && typeof b === 'object') {
+ if (Object.keys(a).length !== Object.keys(b).length) {
+ return false;
+ }
+
+ let isEqual = true;
+
+ for (let prop in a) {
+ if (a.hasOwnProperty(prop)) {
+ if (!eq(a[prop], b[prop])) {
+ isEqual = false;
+ break;
+ }
+ }
+ }
+
+ return isEqual;
+ }
+
+ return false;
+ }
+
+ context['expect'] = function(expected: any) {
+ return {
+ toBe: function(actual: any) {
+ if (expected !== actual) {
+ throw new Error(`Expected ${expected} to be ${actual}`);
+ }
+ },
+ toEqual: function(actual: any) {
+ if (!eq(expected, actual)) {
+ throw new Error(`Expected ${expected} to be ${actual}`);
+ }
+ },
+ toBeGreaterThan: function(actual: number) {
+ if (expected <= actual) {
+ throw new Error(`Expected ${expected} to be greater than ${actual}`);
+ }
+ },
+ toBeLessThan: function(actual: number) {
+ if (expected >= actual) {
+ throw new Error(`Expected ${expected} to be lesser than ${actual}`);
+ }
+ },
+ toBeDefined: function() {
+ if (!expected) {
+ throw new Error(`Expected ${expected} to be defined`);
+ }
+ },
+ toThrow: function() {
+ try {
+ expected();
+ } catch (error) {
+ return;
+ }
+
+ throw new Error(`Expected ${expected} to throw`);
+ },
+ toThrowError: function(errorToBeThrow: any) {
+ try {
+ expected();
+ } catch (error) {
+ return;
+ }
+
+ throw Error(`Expected ${expected} to throw: ${errorToBeThrow}`);
+ },
+ toBeTruthy: function() {
+ if (!expected) {
+ throw new Error(`Expected ${expected} to be truthy`);
+ }
+ },
+ toBeFalsy: function(actual: any) {
+ if (!!actual) {
+ throw new Error(`Expected ${actual} to be falsy`);
+ }
+ },
+ toContain: function(actual: any) {
+ if (expected.indexOf(actual) === -1) {
+ throw new Error(`Expected ${expected} to contain ${actual}`);
+ }
+ },
+ toHaveBeenCalled: function() {
+ if (expected.countCall === 0) {
+ throw new Error(`Expected ${expected} to been called`);
+ }
+ },
+ toHaveBeenCalledWith: function(...params: any[]) {
+ if (!eq(expected.callArgs, params)) {
+ throw new Error(`Expected ${expected} to been called with ${
+ expected.callArgs}, called with: ${params}`);
+ }
+ },
+ toMatch: function(actual: any) {
+ if (!new RegExp(actual).test(expected)) {
+ throw new Error(`Expected ${expected} to match ${actual}`);
+ }
+ },
+ not: {
+ toBe: function(actual: any) {
+ if (expected === actual) {
+ throw new Error(`Expected ${expected} not to be ${actual}`);
+ }
+ },
+ toHaveBeenCalled: function() {
+ if (expected.countCall > 0) {
+ throw new Error(`Expected ${expected} to not been called`);
+ }
+ },
+ toThrow: function() {
+ try {
+ expected();
+ } catch (error) {
+ throw new Error(`Expected ${expected} to not throw`);
+ }
+ },
+ toThrowError: function() {
+ try {
+ expected();
+ } catch (error) {
+ throw Error(`Expected ${expected} to not throw error`);
+ }
+ },
+ toBeGreaterThan: function(actual: number) {
+ if (expected > actual) {
+ throw new Error(`Expected ${expected} not to be greater than ${actual}`);
+ }
+ },
+ toBeLessThan: function(actual: number) {
+ if (expected < actual) {
+ throw new Error(`Expected ${expected} not to be lesser than ${actual}`);
+ }
+ },
+ toHaveBeenCalledWith: function(params: any[]) {
+ if (!eq(expected.callArgs, params)) {
+ throw new Error(`Expected ${expected} to not been called with ${params}`);
+ }
+ }
+ }
+ };
+ };
+})(typeof window !== 'undefined' && window || typeof self !== 'undefined' && self || global);
\ No newline at end of file
diff --git a/packages/zone.js/test/test-util.ts b/packages/zone.js/test/test-util.ts
new file mode 100644
index 0000000000..d3cc321242
--- /dev/null
+++ b/packages/zone.js/test/test-util.ts
@@ -0,0 +1,142 @@
+/**
+ * @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
+ */
+
+/*
+ * Usage:
+ *
+ * function supportsOnClick() {
+ * const div = document.createElement('div');
+ * const clickPropDesc = Object.getOwnPropertyDescriptor(div, 'onclick');
+ * return !(EventTarget &&
+ * div instanceof EventTarget &&
+ * clickPropDesc && clickPropDesc.value === null);
+ * }
+ * (supportsOnClick).message = 'Supports Element#onclick patching';
+ *
+ *
+ * ifEnvSupports(supportsOnClick, function() { ... });
+ */
+import {isNode, zoneSymbol} from '../lib/common/utils';
+
+// Re-export for convenience.
+export {zoneSymbol};
+
+declare const global: any;
+export function ifEnvSupports(test: any, block: Function): () => void {
+ return _ifEnvSupports(test, block);
+}
+
+export function ifEnvSupportsWithDone(test: any, block: Function): (done: Function) => void {
+ return _ifEnvSupports(test, block, true);
+}
+
+function _ifEnvSupports(test: any, block: Function, withDone = false) {
+ if (withDone) {
+ return function(done?: Function) { _runTest(test, block, done); };
+ } else {
+ return function() { _runTest(test, block, undefined); };
+ }
+}
+
+function _runTest(test: any, block: Function, done?: Function) {
+ const message = (test.message || test.name || test);
+ if (typeof test === 'string' ? !!global[test] : test()) {
+ if (done) {
+ block(done);
+ } else {
+ block();
+ }
+ } else {
+ console.log('WARNING: skipping ' + message + ' tests (missing this API)');
+ done && done();
+ }
+}
+
+export function supportPatchXHROnProperty() {
+ let desc = Object.getOwnPropertyDescriptor(XMLHttpRequest.prototype, 'onload');
+ if (!desc && (window as any)['XMLHttpRequestEventTarget']) {
+ desc = Object.getOwnPropertyDescriptor(global['XMLHttpRequestEventTarget'].prototype, 'onload');
+ }
+ if (!desc || !desc.configurable) {
+ return false;
+ }
+ return true;
+}
+
+let supportSetErrorStack = true;
+
+export function isSupportSetErrorStack() {
+ try {
+ throw new Error('test');
+ } catch (err) {
+ try {
+ err.stack = 'new stack';
+ supportSetErrorStack = err.stack === 'new stack';
+ } catch (error) {
+ supportSetErrorStack = false;
+ }
+ }
+ return supportSetErrorStack;
+}
+
+(isSupportSetErrorStack as any).message = 'supportSetErrorStack';
+
+export function asyncTest(testFn: Function, zone: Zone = Zone.current) {
+ const AsyncTestZoneSpec = (Zone as any)['AsyncTestZoneSpec'];
+ return (done: Function) => {
+ let asyncTestZone: Zone =
+ zone.fork(new AsyncTestZoneSpec(() => {}, (error: Error) => { fail(error); }, 'asyncTest'));
+ asyncTestZone.run(testFn, this, [done]);
+ };
+}
+
+export function getIEVersion() {
+ const userAgent = navigator.userAgent.toLowerCase();
+ if (userAgent.indexOf('msie') != -1) {
+ return parseInt(userAgent.split('msie')[1]);
+ }
+ return null;
+}
+
+export function isFirefox() {
+ const userAgent = navigator.userAgent.toLowerCase();
+ if (userAgent.indexOf('firefox') != -1) {
+ return true;
+ }
+ return false;
+}
+
+export function isSafari() {
+ const userAgent = navigator.userAgent.toLowerCase();
+ if (userAgent.indexOf('safari') != -1) {
+ return true;
+ }
+ return false;
+}
+
+export function isEdge() {
+ const userAgent = navigator.userAgent.toLowerCase();
+ return userAgent.indexOf('edge') !== -1;
+}
+
+export function getEdgeVersion() {
+ const ua = navigator.userAgent.toLowerCase();
+ const edge = ua.indexOf('edge/');
+ if (edge === -1) {
+ return -1;
+ }
+ return parseInt(ua.substring(edge + 5, ua.indexOf('.', edge)), 10);
+}
+
+export function isPhantomJS() {
+ if (isNode) {
+ return false;
+ }
+ const ua = navigator.userAgent.toLowerCase();
+ return ua.indexOf('phantomjs') !== -1;
+}
diff --git a/packages/zone.js/test/test_fake_polyfill.ts b/packages/zone.js/test/test_fake_polyfill.ts
new file mode 100644
index 0000000000..a0787692bd
--- /dev/null
+++ b/packages/zone.js/test/test_fake_polyfill.ts
@@ -0,0 +1,82 @@
+/**
+ * @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
+ */
+
+///
+
+'use strict';
+(function(global: any) {
+ // add custom properties to Native Error
+ const NativeError = global['Error'];
+ NativeError.customProperty = 'customProperty';
+ NativeError.customFunction = function() {};
+
+ // add fake cordova polyfill for test
+ const fakeCordova = function() {};
+
+ (fakeCordova as any).exec = function(
+ success: Function, error: Function, service: string, action: string, args: any[]) {
+ if (action === 'successAction') {
+ success();
+ } else {
+ error();
+ }
+ };
+
+ global.cordova = fakeCordova;
+
+ const TestTarget = global.TestTarget = function() {};
+
+ Object.defineProperties(TestTarget.prototype, {
+ 'onprop1': {configurable: true, writable: true},
+ 'onprop2': {configurable: true, writable: true},
+ 'onprop3': {
+ configurable: true,
+ get: function() { return this._onprop3; },
+ set: function(_value) { this._onprop3 = _value; }
+ },
+ '_onprop3': {configurable: true, writable: true, value: function() {}},
+ 'addEventListener': {
+ configurable: true,
+ writable: true,
+ value: function(eventName: string, callback: Function) {
+ if (!this.events) {
+ this.events = {};
+ }
+ const Zone = global.Zone;
+ this.events.eventName = {zone: Zone.current, callback: callback};
+ }
+ },
+ 'removeEventListener': {
+ configurable: true,
+ writable: true,
+ value: function(eventName: string, callback: Function) {
+ if (!this.events) {
+ return;
+ }
+ this.events.eventName = null;
+ }
+ },
+ 'dispatchEvent': {
+ configurable: true,
+ writable: true,
+ value: function(eventName: string) {
+ const zoneCallback = this.events && this.events.eventName;
+ zoneCallback && zoneCallback.zone.run(zoneCallback.callback, this, [{type: eventName}]);
+ }
+ }
+ });
+
+ // Zone symbol prefix may be set in *-env-setup.ts (browser & node),
+ // but this file is used in multiple scenarios, and Zone isn't loaded at this point yet.
+ const zoneSymbolPrefix = global['__Zone_symbol_prefix'] || '__zone_symbol__';
+
+ global['__Zone_ignore_on_properties'] =
+ [{target: TestTarget.prototype, ignoreProperties: ['prop1']}];
+ global[zoneSymbolPrefix + 'FakeAsyncTestMacroTask'] = [{source: 'TestClass.myTimeout'}];
+ global[zoneSymbolPrefix + 'UNPATCHED_EVENTS'] = ['scroll'];
+})(typeof window === 'object' && window || typeof self === 'object' && self || global);
diff --git a/packages/zone.js/test/webdriver/test-es2015.html b/packages/zone.js/test/webdriver/test-es2015.html
new file mode 100644
index 0000000000..36cf44e694
--- /dev/null
+++ b/packages/zone.js/test/webdriver/test-es2015.html
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+Hello Zones!
+
+
diff --git a/packages/zone.js/test/webdriver/test.html b/packages/zone.js/test/webdriver/test.html
new file mode 100644
index 0000000000..be600e7903
--- /dev/null
+++ b/packages/zone.js/test/webdriver/test.html
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+Hello Zones!
+
+
diff --git a/packages/zone.js/test/webdriver/test.js b/packages/zone.js/test/webdriver/test.js
new file mode 100644
index 0000000000..0250b91be5
--- /dev/null
+++ b/packages/zone.js/test/webdriver/test.js
@@ -0,0 +1,30 @@
+/**
+ * @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
+ */
+
+// TODO: @JiaLiPassion, try to add it into travis/saucelabs test after saucelabs support Firefox 52+
+// requirement, Firefox 52+, webdriver-manager 12.0.4+, selenium-webdriver 3.3.0+
+// test step,
+// webdriver-manager update
+// webdriver-manager start
+// http-server test/webdriver
+// node test/webdriver/test.js
+
+// testcase1: removeEventHandler in firefox cross site context
+const webdriver = require('selenium-webdriver');
+const capabilities = webdriver.Capabilities.firefox();
+const driver = new webdriver.Builder()
+ .usingServer('http://localhost:4444/wd/hub')
+ .withCapabilities(capabilities)
+ .build();
+driver.get('http://localhost:8080/test.html');
+driver.executeAsyncScript((cb) => {window.setTimeout(cb, 1000)});
+
+// test case2 addEventHandler in firefox cross site context
+driver.findElement(webdriver.By.css('#thetext')).getText().then(function(text) {
+ console.log(text);
+});
diff --git a/packages/zone.js/test/webdriver/test.sauce.es2015.js b/packages/zone.js/test/webdriver/test.sauce.es2015.js
new file mode 100644
index 0000000000..6e898e187c
--- /dev/null
+++ b/packages/zone.js/test/webdriver/test.sauce.es2015.js
@@ -0,0 +1,101 @@
+/**
+ * @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 webdriverio = require('webdriverio');
+const desiredCapabilities = {
+ android60: {
+ deviceName: 'Android GoogleAPI Emulator',
+ browserName: 'Chrome',
+ platformName: 'Android',
+ platformVersion: '6.0',
+ deviceOrientation: 'portrait',
+ appiumVersion: '1.12.1'
+ },
+ android71: {
+ deviceName: 'Android GoogleAPI Emulator',
+ browserName: 'Chrome',
+ platformName: 'Android',
+ platformVersion: '7.1',
+ deviceOrientation: 'portrait',
+ appiumVersion: '1.12.1'
+ }
+};
+
+const errors = [];
+const tasks = [];
+
+if (process.env.TRAVIS) {
+ process.env.SAUCE_ACCESS_KEY = process.env.SAUCE_ACCESS_KEY.split('').reverse().join('');
+}
+
+Object.keys(desiredCapabilities).forEach(key => {
+ console.log('begin webdriver test', key);
+ if (process.env.TRAVIS) {
+ desiredCapabilities[key]['tunnel-identifier'] = process.env.TRAVIS_JOB_NUMBER;
+ }
+ const client = require('webdriverio').remote({
+ user: process.env.SAUCE_USERNAME,
+ key: process.env.SAUCE_ACCESS_KEY,
+ host: 'localhost',
+ port: 4445,
+ desiredCapabilities: desiredCapabilities[key]
+ });
+
+ const p = client.init()
+ .timeouts('script', 60000)
+ .url('http://localhost:8080/test/webdriver/test-es2015.html')
+ .executeAsync(function(done) { window.setTimeout(done, 1000) })
+ .execute(function() {
+ const elem = document.getElementById('thetext');
+ const zone = window['Zone'] ? Zone.current.fork({name: 'webdriver'}) : null;
+ if (zone) {
+ zone.run(function() {
+ elem.addEventListener('click', function(e) {
+ e.target.innerText = 'clicked' + Zone.current.name;
+ });
+ });
+ } else {
+ elem.addEventListener('click', function(e) { e.target.innerText = 'clicked'; });
+ }
+ })
+ .click('#thetext')
+ .getText('#thetext')
+ .then(
+ (text => {
+ if (text !== 'clickedwebdriver') {
+ errors.push(`Env: ${key}, expected clickedwebdriver, get ${text}`);
+ }
+ }),
+ (error) => { errors.push(`Env: ${key}, error occurs: ${error}`); })
+ .end();
+ tasks.push(p);
+});
+
+function exit(exitCode) {
+ const http = require('http');
+ http.get('http://localhost:8080/close', () => { process.exit(exitCode); });
+}
+
+Promise.all(tasks).then(() => {
+ if (errors.length > 0) {
+ let nonTimeoutError = false;
+ errors.forEach(error => {
+ console.log(error);
+ if (error.toString().lastIndexOf('timeout') === -1) {
+ nonTimeoutError = true;
+ }
+ });
+ if (nonTimeoutError) {
+ exit(1);
+ } else {
+ exit(0);
+ }
+ } else {
+ exit(0);
+ }
+});
diff --git a/packages/zone.js/test/webdriver/test.sauce.js b/packages/zone.js/test/webdriver/test.sauce.js
new file mode 100644
index 0000000000..8c5ffddbaa
--- /dev/null
+++ b/packages/zone.js/test/webdriver/test.sauce.js
@@ -0,0 +1,112 @@
+/**
+ * @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 webdriverio = require('webdriverio');
+const desiredCapabilities = {
+ firefox52Win7: {browserName: 'firefox', platform: 'Windows 7', version: '52'},
+ firefox53Win7: {browserName: 'firefox', platform: 'Windows 7', version: '53'},
+ edge14: {browserName: 'MicrosoftEdge', platform: 'Windows 10', version: '14.14393'},
+ edge15: {browserName: 'MicrosoftEdge', platform: 'Windows 10', version: '15.15063'},
+ chrome48: {browserName: 'chrome', version: '48'},
+ safari8: {browserName: 'safari', platform: 'OS X 10.10', version: '8.0'},
+ safari9: {browserName: 'safari', platform: 'OS X 10.11', version: '9.0'},
+ safari10: {browserName: 'safari', platform: 'OS X 10.11', version: '10.0'},
+ safari11: {browserName: 'safari', platform: 'macOS 10.13', version: '11.1'},
+ /*ios84: {browserName: 'iphone', platform: 'OS X 10.10', version: '8.4'},*/
+ ios10: {browserName: 'iphone', platform: 'OS X 10.10', version: '10.3'},
+ ios11: {browserName: 'iphone', platform: 'OS X 10.12', version: '11.2'},
+ /*
+ ie9: {
+ browserName: 'internet explorer',
+ platform: 'Windows 2008',
+ version: '9'
+ },*/
+ /*
+ ie10: {
+ browserName: 'internet explorer',
+ platform: 'Windows 2012',
+ version: '10'
+ },*/
+ ie11: {browserName: 'internet explorer', platform: 'Windows 10', version: '11'},
+ // andriod44: {browserName: 'android', platform: 'Linux', version: '4.4'},
+ android51: {browserName: 'android', platform: 'Linux', version: '5.1'},
+};
+
+const errors = [];
+const tasks = [];
+
+if (process.env.TRAVIS) {
+ process.env.SAUCE_ACCESS_KEY = process.env.SAUCE_ACCESS_KEY.split('').reverse().join('');
+}
+
+Object.keys(desiredCapabilities).forEach(key => {
+ console.log('begin webdriver test', key);
+ if (process.env.TRAVIS) {
+ desiredCapabilities[key]['tunnel-identifier'] = process.env.TRAVIS_JOB_NUMBER;
+ }
+ const client = require('webdriverio').remote({
+ user: process.env.SAUCE_USERNAME,
+ key: process.env.SAUCE_ACCESS_KEY,
+ host: 'localhost',
+ port: 4445,
+ desiredCapabilities: desiredCapabilities[key]
+ });
+
+ const p = client.init()
+ .timeouts('script', 60000)
+ .url('http://localhost:8080/test/webdriver/test.html')
+ .executeAsync(function(done) { window.setTimeout(done, 1000) })
+ .execute(function() {
+ const elem = document.getElementById('thetext');
+ const zone = window['Zone'] ? Zone.current.fork({name: 'webdriver'}) : null;
+ if (zone) {
+ zone.run(function() {
+ elem.addEventListener('click', function(e) {
+ e.target.innerText = 'clicked' + Zone.current.name;
+ });
+ });
+ } else {
+ elem.addEventListener('click', function(e) { e.target.innerText = 'clicked'; });
+ }
+ })
+ .click('#thetext')
+ .getText('#thetext')
+ .then(
+ (text => {
+ if (text !== 'clickedwebdriver') {
+ errors.push(`Env: ${key}, expected clickedwebdriver, get ${text}`);
+ }
+ }),
+ (error) => { errors.push(`Env: ${key}, error occurs: ${error}`); })
+ .end();
+ tasks.push(p);
+});
+
+function exit(exitCode) {
+ const http = require('http');
+ http.get('http://localhost:8080/close', () => { process.exit(exitCode); });
+}
+
+Promise.all(tasks).then(() => {
+ if (errors.length > 0) {
+ let nonTimeoutError = false;
+ errors.forEach(error => {
+ console.log(error);
+ if (error.toString().lastIndexOf('timeout') === -1) {
+ nonTimeoutError = true;
+ }
+ });
+ if (nonTimeoutError) {
+ exit(1);
+ } else {
+ exit(0);
+ }
+ } else {
+ exit(0);
+ }
+});
diff --git a/packages/zone.js/test/ws-client.js b/packages/zone.js/test/ws-client.js
new file mode 100644
index 0000000000..3677fe84ed
--- /dev/null
+++ b/packages/zone.js/test/ws-client.js
@@ -0,0 +1,14 @@
+/**
+ * @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 ws = require('nodejs-websocket');
+
+const conn = ws.connect('ws://localhost:8001', {}, function() {
+ conn.send('close');
+ conn.close();
+});
diff --git a/packages/zone.js/test/ws-server.js b/packages/zone.js/test/ws-server.js
new file mode 100644
index 0000000000..3ca72505d6
--- /dev/null
+++ b/packages/zone.js/test/ws-server.js
@@ -0,0 +1,20 @@
+/**
+ * @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 ws = require('nodejs-websocket');
+
+// simple echo server
+const server = ws.createServer(function(conn) {
+ conn.on('text', function(str) {
+ if (str === 'close') {
+ server.close();
+ return;
+ }
+ conn.sendText(str.toString());
+ });
+ }).listen(8001);
diff --git a/packages/zone.js/test/ws-webworker-context.ts b/packages/zone.js/test/ws-webworker-context.ts
new file mode 100644
index 0000000000..b52632ea4c
--- /dev/null
+++ b/packages/zone.js/test/ws-webworker-context.ts
@@ -0,0 +1,13 @@
+/**
+ * @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
+ */
+
+declare function importScripts(path: string): void;
+
+ importScripts('/base/build/lib/zone.js');
+ importScripts('/base/node_modules/systemjs/dist/system.src.js');
+ importScripts('/base/build/test/zone_worker_entry_point.js');
diff --git a/packages/zone.js/test/wtf_mock.ts b/packages/zone.js/test/wtf_mock.ts
new file mode 100644
index 0000000000..b8c9d3fed9
--- /dev/null
+++ b/packages/zone.js/test/wtf_mock.ts
@@ -0,0 +1,89 @@
+/**
+ * @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
+ */
+
+///
+
+'use strict';
+(function(global) {
+ const log: string[] = [];
+ const logArgs: any[][] = [];
+ const wtfMock = {
+ log: log,
+ logArgs: logArgs,
+ reset: function() {
+ log.length = 0;
+ logArgs.length = 0;
+ },
+ trace: {
+ leaveScope: function(scope: any, returnValue: any) { return scope(returnValue); },
+ beginTimeRange: function(type: any, action: any) {
+ logArgs.push([]);
+ log.push('>>> ' + type + '[' + action + ']');
+ return function() {
+ logArgs.push([]);
+ log.push('<<< ' + type);
+ };
+ },
+ endTimeRange: function(range: Function) { range(); },
+ events: {
+ createScope: function(signature: string, flags: any) {
+ const parts = signature.split('(');
+ const name = parts[0];
+ return function scopeFn() {
+ const args = [];
+ for (let i = arguments.length - 1; i >= 0; i--) {
+ const arg = arguments[i];
+ if (arg !== undefined) {
+ args.unshift(__stringify(arg));
+ }
+ }
+ log.push('> ' + name + '(' + args.join(', ') + ')');
+ logArgs.push(args);
+ return function(retValue: any) {
+ log.push('< ' + name + (retValue == undefined ? '' : ' => ' + retValue));
+ logArgs.push(retValue);
+ return retValue;
+ };
+ };
+ },
+ createInstance: function(signature: string, flags: any) {
+ const parts = signature.split('(');
+ const name = parts[0];
+ return function eventFn() {
+ const args = [];
+ for (let i = arguments.length - 1; i >= 0; i--) {
+ const arg = arguments[i];
+ if (arg !== undefined) {
+ args.unshift(__stringify(arg));
+ }
+ }
+ log.push('# ' + name + '(' + args.join(', ') + ')');
+ logArgs.push(args);
+ };
+ }
+ }
+ }
+ };
+
+ function __stringify(obj: any): string {
+ let str = typeof obj == 'string' || !obj ? JSON.stringify(obj) : obj.toString();
+ if (str == '[object Arguments]') {
+ str = JSON.stringify(Array.prototype.slice.call(obj));
+ } else if (str == '[object Object]') {
+ str = JSON.stringify(obj);
+ }
+ return str;
+ }
+
+ beforeEach(function() { wtfMock.reset(); });
+
+ (global).wtfMock = wtfMock;
+ (global).wtf = wtfMock;
+})(typeof window === 'object' && window || typeof self === 'object' && self || global);
+
+declare const wtfMock: any;
diff --git a/packages/zone.js/test/zone-spec/async-test.spec.ts b/packages/zone.js/test/zone-spec/async-test.spec.ts
new file mode 100644
index 0000000000..d697bbc408
--- /dev/null
+++ b/packages/zone.js/test/zone-spec/async-test.spec.ts
@@ -0,0 +1,413 @@
+/**
+ * @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
+ */
+
+import {ifEnvSupports} from '../test-util';
+
+describe('AsyncTestZoneSpec', function() {
+ let log: string[];
+ const AsyncTestZoneSpec = (Zone as any)['AsyncTestZoneSpec'];
+
+ function finishCallback() { log.push('finish'); }
+
+ function failCallback() { log.push('fail'); }
+
+ beforeEach(() => { log = []; });
+
+ it('should call finish after zone is run in sync call', (done) => {
+ let finished = false;
+ const testZoneSpec = new AsyncTestZoneSpec(() => {
+ expect(finished).toBe(true);
+ done();
+ }, failCallback, 'name');
+
+ const atz = Zone.current.fork(testZoneSpec);
+
+ atz.run(function() { finished = true; });
+ });
+
+ it('should call finish after a setTimeout is done', (done) => {
+ let finished = false;
+
+ const testZoneSpec = new AsyncTestZoneSpec(
+ () => {
+ expect(finished).toBe(true);
+ done();
+ },
+ () => { done.fail('async zone called failCallback unexpectedly'); }, 'name');
+
+ const atz = Zone.current.fork(testZoneSpec);
+
+ atz.run(function() { setTimeout(() => { finished = true; }, 10); });
+ });
+
+ it('should call finish after microtasks are done', (done) => {
+ let finished = false;
+
+ const testZoneSpec = new AsyncTestZoneSpec(
+ () => {
+ expect(finished).toBe(true);
+ done();
+ },
+ () => { done.fail('async zone called failCallback unexpectedly'); }, 'name');
+
+ const atz = Zone.current.fork(testZoneSpec);
+
+ atz.run(function() { Promise.resolve().then(() => { finished = true; }); });
+ });
+
+ it('should call finish after both micro and macrotasks are done', (done) => {
+ let finished = false;
+
+ const testZoneSpec = new AsyncTestZoneSpec(
+ () => {
+ expect(finished).toBe(true);
+ done();
+ },
+ () => { done.fail('async zone called failCallback unexpectedly'); }, 'name');
+
+ const atz = Zone.current.fork(testZoneSpec);
+
+ atz.run(function() {
+ new Promise((resolve) => { setTimeout(() => { resolve(); }, 10); }).then(() => {
+ finished = true;
+ });
+ });
+ });
+
+ it('should call finish after both macro and microtasks are done', (done) => {
+ let finished = false;
+
+ const testZoneSpec = new AsyncTestZoneSpec(
+ () => {
+ expect(finished).toBe(true);
+ done();
+ },
+ () => { done.fail('async zone called failCallback unexpectedly'); }, 'name');
+
+ const atz = Zone.current.fork(testZoneSpec);
+
+ atz.run(function() {
+ Promise.resolve().then(() => { setTimeout(() => { finished = true; }, 10); });
+ });
+ });
+
+ describe('event tasks', ifEnvSupports('document', () => {
+ let button: HTMLButtonElement;
+ beforeEach(function() {
+ button = document.createElement('button');
+ document.body.appendChild(button);
+ });
+ afterEach(function() { document.body.removeChild(button); });
+
+ it('should call finish because an event task is considered as sync', (done) => {
+ let finished = false;
+
+ const testZoneSpec = new AsyncTestZoneSpec(
+ () => {
+ expect(finished).toBe(true);
+ done();
+ },
+ () => { done.fail('async zone called failCallback unexpectedly'); }, 'name');
+
+ const atz = Zone.current.fork(testZoneSpec);
+
+ atz.run(function() {
+ const listener = () => { finished = true; };
+ button.addEventListener('click', listener);
+
+ const clickEvent = document.createEvent('Event');
+ clickEvent.initEvent('click', true, true);
+
+ button.dispatchEvent(clickEvent);
+ });
+ });
+
+ it('should call finish after an event task is done asynchronously', (done) => {
+ let finished = false;
+
+ const testZoneSpec = new AsyncTestZoneSpec(
+ () => {
+ expect(finished).toBe(true);
+ done();
+ },
+ () => { done.fail('async zone called failCallback unexpectedly'); }, 'name');
+
+ const atz = Zone.current.fork(testZoneSpec);
+
+ atz.run(function() {
+ button.addEventListener(
+ 'click', () => { setTimeout(() => { finished = true; }, 10); });
+
+ const clickEvent = document.createEvent('Event');
+ clickEvent.initEvent('click', true, true);
+
+ button.dispatchEvent(clickEvent);
+ });
+ });
+ }));
+
+ describe('XHRs', ifEnvSupports('XMLHttpRequest', () => {
+ it('should wait for XHRs to complete', function(done) {
+ let req: XMLHttpRequest;
+ let finished = false;
+
+ const testZoneSpec = new AsyncTestZoneSpec(
+ () => {
+ expect(finished).toBe(true);
+ done();
+ },
+ (err: Error) => { done.fail('async zone called failCallback unexpectedly'); },
+ 'name');
+
+ const atz = Zone.current.fork(testZoneSpec);
+
+ atz.run(function() {
+ req = new XMLHttpRequest();
+
+ req.onreadystatechange = () => {
+ if (req.readyState === XMLHttpRequest.DONE) {
+ finished = true;
+ }
+ };
+
+ req.open('get', '/', true);
+ req.send();
+ });
+ });
+
+ it('should fail if an xhr fails', function(done) {
+ let req: XMLHttpRequest;
+
+ const testZoneSpec = new AsyncTestZoneSpec(
+ () => { done.fail('expected failCallback to be called'); },
+ (err: Error) => {
+ expect(err.message).toEqual('bad url failure');
+ done();
+ },
+ 'name');
+
+ const atz = Zone.current.fork(testZoneSpec);
+
+ atz.run(function() {
+ req = new XMLHttpRequest();
+ req.onload = () => {
+ if (req.status != 200) {
+ throw new Error('bad url failure');
+ }
+ };
+ req.open('get', '/bad-url', true);
+ req.send();
+ });
+ });
+ }));
+
+ it('should not fail if setInterval is used and canceled', (done) => {
+ const testZoneSpec = new AsyncTestZoneSpec(
+ () => { done(); },
+ (err: Error) => { done.fail('async zone called failCallback unexpectedly'); }, 'name');
+
+ const atz = Zone.current.fork(testZoneSpec);
+
+ atz.run(function() { let id = setInterval(() => { clearInterval(id); }, 100); });
+ });
+
+ it('should fail if an error is thrown asynchronously', (done) => {
+ const testZoneSpec = new AsyncTestZoneSpec(
+ () => { done.fail('expected failCallback to be called'); },
+ (err: Error) => {
+ expect(err.message).toEqual('my error');
+ done();
+ },
+ 'name');
+
+ const 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) => {
+ const testZoneSpec = new AsyncTestZoneSpec(
+ () => { done.fail('expected failCallback to be called'); },
+ (err: Error) => {
+ expect(err.message).toEqual('Uncaught (in promise): my reason');
+ done();
+ },
+ 'name');
+
+ const atz = Zone.current.fork(testZoneSpec);
+
+ atz.run(function() { Promise.reject('my reason'); });
+ });
+
+ const asyncTest: any = (Zone as any)[Zone.__symbol__('asyncTest')];
+
+ 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('non zone aware async task in promise should be detected', () => {
+ let finished = false;
+ const _global: any =
+ typeof window !== 'undefined' && window || typeof self !== 'undefined' && self || global;
+ beforeEach(() => { _global[Zone.__symbol__('supportWaitUnResolvedChainedPromise')] = true; });
+ afterEach(() => { _global[Zone.__symbol__('supportWaitUnResolvedChainedPromise')] = false; });
+ it('should be able to detect non zone aware async task in promise',
+ wrapAsyncTest(
+ () => {
+ new Promise((res, rej) => {
+ const g: any = typeof window === 'undefined' ? global : window;
+ g[Zone.__symbol__('setTimeout')](res, 100);
+ }).then(() => { finished = true; });
+ },
+ () => { expect(finished).toBe(true); }));
+ });
+
+
+ describe('test without beforeEach', () => {
+ const logs: string[] = [];
+ 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',
+ wrapAsyncTest(
+ () => { setTimeout(() => { logs.push('timeout'); }, 100); },
+ () => {
+ expect(logs).toEqual(['beforeEach', 'timeout']);
+ }));
+ });
+
+ describe('test with async beforeEach', () => {
+ const logs: string[] = [];
+
+ beforeEach(wrapAsyncTest(() => {
+ setTimeout(() => {
+ logs.splice(0);
+ logs.push('beforeEach');
+ }, 100);
+ }));
+
+ 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(wrapAsyncTest(() => {
+ setTimeout(() => {
+ expect(logs).toEqual([]);
+ logs.push('beforeEach');
+ }, 100);
+ }));
+
+ afterEach(() => { logs.splice(0); });
+
+ 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(wrapAsyncTest(() => {
+ setTimeout(() => {
+ expect(logs).toEqual([]);
+ logs.push('beforeEach');
+ }, 100);
+ }));
+
+ afterEach(wrapAsyncTest(() => { setTimeout(() => { logs.splice(0); }, 100); }));
+
+ it('should automatically done after async tasks finished',
+ wrapAsyncTest(
+ () => { setTimeout(() => { logs.push('timeout'); }, 100); },
+ () => {
+ expect(logs).toEqual(['beforeEach', 'timeout']);
+ }));
+ });
+ });
+});
diff --git a/packages/zone.js/test/zone-spec/fake-async-test.spec.ts b/packages/zone.js/test/zone-spec/fake-async-test.spec.ts
new file mode 100644
index 0000000000..f538f6af88
--- /dev/null
+++ b/packages/zone.js/test/zone-spec/fake-async-test.spec.ts
@@ -0,0 +1,1473 @@
+/**
+ * @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
+ */
+
+import '../../lib/rxjs/rxjs-fake-async';
+
+import {Observable} from 'rxjs';
+import {delay} from 'rxjs/operators';
+
+import {isNode, patchMacroTask, zoneSymbol} from '../../lib/common/utils';
+import {ifEnvSupports} from '../test-util';
+
+function supportNode() {
+ return isNode;
+}
+
+(supportNode as any).message = 'support node';
+
+function supportClock() {
+ const _global: any = typeof window === 'undefined' ? global : window;
+ return typeof jasmine.clock === 'function' &&
+ _global[zoneSymbol('fakeAsyncAutoFakeAsyncWhenClockPatched')];
+}
+
+(supportClock as any).message = 'support patch clock';
+
+describe('FakeAsyncTestZoneSpec', () => {
+ let FakeAsyncTestZoneSpec = (Zone as any)['FakeAsyncTestZoneSpec'];
+ let testZoneSpec: any;
+ let fakeAsyncTestZone: Zone;
+
+ beforeEach(() => {
+ testZoneSpec = new FakeAsyncTestZoneSpec('name');
+ fakeAsyncTestZone = Zone.current.fork(testZoneSpec);
+ });
+
+ it('sets the FakeAsyncTestZoneSpec property', () => {
+ fakeAsyncTestZone.run(
+ () => { expect(Zone.current.get('FakeAsyncTestZoneSpec')).toEqual(testZoneSpec); });
+ });
+
+ describe('synchronous code', () => {
+ it('should run', () => {
+ let ran = false;
+ fakeAsyncTestZone.run(() => { ran = true; });
+
+ expect(ran).toEqual(true);
+ });
+
+ it('should throw the error in the code', () => {
+ expect(() => {
+ fakeAsyncTestZone.run(() => { throw new Error('sync'); });
+ }).toThrowError('sync');
+ });
+
+ it('should throw error on Rejected promise', () => {
+ expect(() => {
+ fakeAsyncTestZone.run(() => {
+ Promise.reject('myError');
+ testZoneSpec.flushMicrotasks();
+ });
+ }).toThrowError('Uncaught (in promise): myError');
+ });
+ });
+
+ describe('asynchronous code', () => {
+ it('should run', () => {
+ fakeAsyncTestZone.run(() => {
+ let thenRan = false;
+ Promise.resolve(null).then((_) => { thenRan = true; });
+
+ expect(thenRan).toEqual(false);
+
+ testZoneSpec.flushMicrotasks();
+ expect(thenRan).toEqual(true);
+ });
+ });
+
+ it('should rethrow the exception on flushMicroTasks for error thrown in Promise callback',
+ () => {
+ fakeAsyncTestZone.run(() => {
+ Promise.resolve(null).then((_) => { throw new Error('async'); });
+ expect(() => {
+ testZoneSpec.flushMicrotasks();
+ }).toThrowError(/Uncaught \(in promise\): Error: async/);
+ });
+ });
+
+ it('should run chained thens', () => {
+ fakeAsyncTestZone.run(() => {
+ let log: number[] = [];
+
+ Promise.resolve(null).then((_) => log.push(1)).then((_) => log.push(2));
+
+ expect(log).toEqual([]);
+
+ testZoneSpec.flushMicrotasks();
+ expect(log).toEqual([1, 2]);
+ });
+ });
+
+ it('should run Promise created in Promise', () => {
+ fakeAsyncTestZone.run(() => {
+ let log: number[] = [];
+
+ Promise.resolve(null).then((_) => {
+ log.push(1);
+ Promise.resolve(null).then((_) => log.push(2));
+ });
+
+ expect(log).toEqual([]);
+
+ testZoneSpec.flushMicrotasks();
+ expect(log).toEqual([1, 2]);
+ });
+ });
+ });
+
+ describe('timers', () => {
+ it('should run queued zero duration timer on zero tick', () => {
+ fakeAsyncTestZone.run(() => {
+ let ran = false;
+ setTimeout(() => { ran = true; }, 0);
+
+ expect(ran).toEqual(false);
+
+ testZoneSpec.tick();
+ expect(ran).toEqual(true);
+ });
+ });
+
+ it('should run queued immediate timer on zero tick', ifEnvSupports('setImmediate', () => {
+ fakeAsyncTestZone.run(() => {
+ let ran = false;
+ setImmediate(() => { ran = true; });
+
+ expect(ran).toEqual(false);
+
+ testZoneSpec.tick();
+ expect(ran).toEqual(true);
+ });
+ }));
+
+ it('should run queued timer after sufficient clock ticks', () => {
+ fakeAsyncTestZone.run(() => {
+ let ran = false;
+ setTimeout(() => { ran = true; }, 10);
+
+ testZoneSpec.tick(6);
+ expect(ran).toEqual(false);
+
+ testZoneSpec.tick(4);
+ expect(ran).toEqual(true);
+ });
+ });
+
+ it('should run doTick callback even if no work ran', () => {
+ fakeAsyncTestZone.run(() => {
+ let totalElapsed = 0;
+ function doTick(elapsed: number) { totalElapsed += elapsed; }
+ setTimeout(() => {}, 10);
+
+ testZoneSpec.tick(6, doTick);
+ expect(totalElapsed).toEqual(6);
+
+ testZoneSpec.tick(6, doTick);
+ expect(totalElapsed).toEqual(12);
+
+ testZoneSpec.tick(6, doTick);
+ expect(totalElapsed).toEqual(18);
+ });
+ });
+
+ it('should run queued timer created by timer callback', () => {
+ fakeAsyncTestZone.run(() => {
+ let counter = 0;
+ const startCounterLoop = () => {
+ counter++;
+ setTimeout(startCounterLoop, 10);
+ };
+
+ startCounterLoop();
+
+ expect(counter).toEqual(1);
+
+ testZoneSpec.tick(10);
+ expect(counter).toEqual(2);
+
+ testZoneSpec.tick(10);
+ expect(counter).toEqual(3);
+
+ testZoneSpec.tick(30);
+ expect(counter).toEqual(6);
+ });
+ });
+
+ it('should run queued timer only once', () => {
+ fakeAsyncTestZone.run(() => {
+ let cycles = 0;
+ setTimeout(() => { cycles++; }, 10);
+
+ testZoneSpec.tick(10);
+ expect(cycles).toEqual(1);
+
+ testZoneSpec.tick(10);
+ expect(cycles).toEqual(1);
+
+ testZoneSpec.tick(10);
+ expect(cycles).toEqual(1);
+ });
+ expect(testZoneSpec.pendingTimers.length).toBe(0);
+ });
+
+ it('should not run cancelled timer', () => {
+ fakeAsyncTestZone.run(() => {
+ let ran = false;
+ let id: any = setTimeout(() => { ran = true; }, 10);
+ clearTimeout(id);
+
+ testZoneSpec.tick(10);
+ expect(ran).toEqual(false);
+ });
+ });
+
+ it('should pass arguments to times', () => {
+ fakeAsyncTestZone.run(() => {
+ let value = 'genuine value';
+ let id = setTimeout((arg1, arg2) => { value = arg1 + arg2; }, 0, 'expected', ' value');
+
+ testZoneSpec.tick();
+ expect(value).toEqual('expected value');
+ });
+ });
+
+ it('should pass arguments to setImmediate', ifEnvSupports('setImmediate', () => {
+ fakeAsyncTestZone.run(() => {
+ let value = 'genuine value';
+ let id = setImmediate((arg1, arg2) => { value = arg1 + arg2; }, 'expected', ' value');
+
+ testZoneSpec.tick();
+ expect(value).toEqual('expected value');
+ });
+ }));
+
+ it('should run periodic timers', () => {
+ fakeAsyncTestZone.run(() => {
+ let cycles = 0;
+ let id = setInterval(() => { cycles++; }, 10);
+
+ expect(id).toBeGreaterThan(0);
+
+ testZoneSpec.tick(10);
+ expect(cycles).toEqual(1);
+
+ testZoneSpec.tick(10);
+ expect(cycles).toEqual(2);
+
+ testZoneSpec.tick(10);
+ expect(cycles).toEqual(3);
+
+ testZoneSpec.tick(30);
+ expect(cycles).toEqual(6);
+ });
+ });
+
+ it('should pass arguments to periodic timers', () => {
+ fakeAsyncTestZone.run(() => {
+ let value = 'genuine value';
+ let id = setInterval((arg1, arg2) => { value = arg1 + arg2; }, 10, 'expected', ' value');
+
+ testZoneSpec.tick(10);
+ expect(value).toEqual('expected value');
+ });
+ });
+
+ it('should not run cancelled periodic timer', () => {
+ fakeAsyncTestZone.run(() => {
+ let ran = false;
+ let id = setInterval(() => { ran = true; }, 10);
+
+ testZoneSpec.tick(10);
+ expect(ran).toEqual(true);
+
+ ran = false;
+ clearInterval(id);
+ testZoneSpec.tick(10);
+ expect(ran).toEqual(false);
+ });
+ });
+
+ it('should be able to cancel periodic timers from a callback', () => {
+ fakeAsyncTestZone.run(() => {
+ let cycles = 0;
+ let id: number;
+
+ id = setInterval(() => {
+ cycles++;
+ clearInterval(id);
+ }, 10) as any as number;
+
+ testZoneSpec.tick(10);
+ expect(cycles).toEqual(1);
+
+ testZoneSpec.tick(10);
+ expect(cycles).toEqual(1);
+ });
+ });
+
+ it('should process microtasks before timers', () => {
+ fakeAsyncTestZone.run(() => {
+ let log: string[] = [];
+
+ Promise.resolve(null).then((_) => log.push('microtask'));
+
+ setTimeout(() => log.push('timer'), 9);
+
+ setInterval(() => log.push('periodic timer'), 10);
+
+ expect(log).toEqual([]);
+
+ testZoneSpec.tick(10);
+ expect(log).toEqual(['microtask', 'timer', 'periodic timer']);
+ });
+ });
+
+ it('should process micro-tasks created in timers before next timers', () => {
+ fakeAsyncTestZone.run(() => {
+ let log: string[] = [];
+
+ Promise.resolve(null).then((_) => log.push('microtask'));
+
+ setTimeout(() => {
+ log.push('timer');
+ Promise.resolve(null).then((_) => log.push('t microtask'));
+ }, 9);
+
+ let id = setInterval(() => {
+ log.push('periodic timer');
+ Promise.resolve(null).then((_) => log.push('pt microtask'));
+ }, 10);
+
+ testZoneSpec.tick(10);
+ expect(log).toEqual(
+ ['microtask', 'timer', 't microtask', 'periodic timer', 'pt microtask']);
+
+ testZoneSpec.tick(10);
+ expect(log).toEqual([
+ 'microtask', 'timer', 't microtask', 'periodic timer', 'pt microtask', 'periodic timer',
+ 'pt microtask'
+ ]);
+ });
+ });
+
+ it('should throw the exception from tick for error thrown in timer callback', () => {
+ fakeAsyncTestZone.run(() => {
+ setTimeout(() => { throw new Error('timer'); }, 10);
+ expect(() => { testZoneSpec.tick(10); }).toThrowError('timer');
+ });
+ // There should be no pending timers after the error in timer callback.
+ expect(testZoneSpec.pendingTimers.length).toBe(0);
+ });
+
+ it('should throw the exception from tick for error thrown in periodic timer callback', () => {
+ fakeAsyncTestZone.run(() => {
+ let count = 0;
+ setInterval(() => {
+ count++;
+ throw new Error(count.toString());
+ }, 10);
+
+ expect(() => { testZoneSpec.tick(10); }).toThrowError('1');
+
+ // Periodic timer is cancelled on first error.
+ expect(count).toBe(1);
+ testZoneSpec.tick(10);
+ expect(count).toBe(1);
+ });
+ // Periodic timer is removed from pending queue on error.
+ expect(testZoneSpec.pendingPeriodicTimers.length).toBe(0);
+ });
+ });
+
+ it('should be able to resume processing timer callbacks after handling an error', () => {
+ fakeAsyncTestZone.run(() => {
+ let ran = false;
+ setTimeout(() => { throw new Error('timer'); }, 10);
+ setTimeout(() => { ran = true; }, 10);
+ expect(() => { testZoneSpec.tick(10); }).toThrowError('timer');
+ expect(ran).toBe(false);
+
+ // Restart timer queue processing.
+ testZoneSpec.tick(0);
+ expect(ran).toBe(true);
+ });
+ // There should be no pending timers after the error in timer callback.
+ expect(testZoneSpec.pendingTimers.length).toBe(0);
+ });
+
+ describe('flushing all tasks', () => {
+ it('should flush all pending timers', () => {
+ fakeAsyncTestZone.run(() => {
+ let x = false;
+ let y = false;
+ let z = false;
+
+ setTimeout(() => { x = true; }, 10);
+ setTimeout(() => { y = true; }, 100);
+ setTimeout(() => { z = true; }, 70);
+
+ let elapsed = testZoneSpec.flush();
+
+ expect(elapsed).toEqual(100);
+ expect(x).toBe(true);
+ expect(y).toBe(true);
+ expect(z).toBe(true);
+ });
+ });
+
+ it('should flush nested timers', () => {
+ fakeAsyncTestZone.run(() => {
+ let x = true;
+ let y = true;
+ setTimeout(() => {
+ x = true;
+ setTimeout(() => { y = true; }, 100);
+ }, 200);
+
+ let elapsed = testZoneSpec.flush();
+
+ expect(elapsed).toEqual(300);
+ expect(x).toBe(true);
+ expect(y).toBe(true);
+ });
+ });
+
+ it('should advance intervals', () => {
+ fakeAsyncTestZone.run(() => {
+ let x = false;
+ let y = false;
+ let z = 0;
+
+ setTimeout(() => { x = true; }, 50);
+ setTimeout(() => { y = true; }, 141);
+ setInterval(() => { z++; }, 10);
+
+ let elapsed = testZoneSpec.flush();
+
+ expect(elapsed).toEqual(141);
+ expect(x).toBe(true);
+ expect(y).toBe(true);
+ expect(z).toEqual(14);
+ });
+ });
+
+ it('should not wait for intervals', () => {
+ fakeAsyncTestZone.run(() => {
+ let z = 0;
+
+ setInterval(() => { z++; }, 10);
+
+ let elapsed = testZoneSpec.flush();
+
+ expect(elapsed).toEqual(0);
+ expect(z).toEqual(0);
+ });
+ });
+
+
+ it('should process micro-tasks created in timers before next timers', () => {
+ fakeAsyncTestZone.run(() => {
+ let log: string[] = [];
+
+ Promise.resolve(null).then((_) => log.push('microtask'));
+
+ setTimeout(() => {
+ log.push('timer');
+ Promise.resolve(null).then((_) => log.push('t microtask'));
+ }, 20);
+
+ let id = setInterval(() => {
+ log.push('periodic timer');
+ Promise.resolve(null).then((_) => log.push('pt microtask'));
+ }, 10);
+
+ testZoneSpec.flush();
+ expect(log).toEqual(
+ ['microtask', 'periodic timer', 'pt microtask', 'timer', 't microtask']);
+ });
+ });
+
+ it('should throw the exception from tick for error thrown in timer callback', () => {
+ fakeAsyncTestZone.run(() => {
+ setTimeout(() => { throw new Error('timer'); }, 10);
+ expect(() => { testZoneSpec.flush(); }).toThrowError('timer');
+ });
+ // There should be no pending timers after the error in timer callback.
+ expect(testZoneSpec.pendingTimers.length).toBe(0);
+ });
+
+ it('should do something reasonable with polling timeouts', () => {
+ expect(() => {
+ fakeAsyncTestZone.run(() => {
+ let z = 0;
+
+ let poll = () => {
+ setTimeout(() => {
+ z++;
+ poll();
+ }, 10);
+ };
+
+ poll();
+ testZoneSpec.flush();
+ });
+ })
+ .toThrowError(
+ 'flush failed after reaching the limit of 20 tasks. Does your code use a polling timeout?');
+ });
+
+ it('accepts a custom limit', () => {
+ expect(() => {
+ fakeAsyncTestZone.run(() => {
+ let z = 0;
+
+ let poll = () => {
+ setTimeout(() => {
+ z++;
+ poll();
+ }, 10);
+ };
+
+ poll();
+ testZoneSpec.flush(10);
+ });
+ })
+ .toThrowError(
+ 'flush failed after reaching the limit of 10 tasks. Does your code use a polling timeout?');
+ });
+
+ it('can flush periodic timers if flushPeriodic is true', () => {
+ fakeAsyncTestZone.run(() => {
+ let x = 0;
+
+ setInterval(() => { x++; }, 10);
+
+ let elapsed = testZoneSpec.flush(20, true);
+
+ expect(elapsed).toEqual(10);
+ expect(x).toEqual(1);
+ });
+ });
+
+ it('can flush multiple periodic timers if flushPeriodic is true', () => {
+ fakeAsyncTestZone.run(() => {
+ let x = 0;
+ let y = 0;
+
+ setInterval(() => { x++; }, 10);
+
+ setInterval(() => { y++; }, 100);
+
+ let elapsed = testZoneSpec.flush(20, true);
+
+ expect(elapsed).toEqual(100);
+ expect(x).toEqual(10);
+ expect(y).toEqual(1);
+ });
+ });
+
+ it('can flush till the last periodic task is processed', () => {
+ fakeAsyncTestZone.run(() => {
+ let x = 0;
+ let y = 0;
+
+ setInterval(() => { x++; }, 10);
+
+ // This shouldn't cause the flush to throw an exception even though
+ // it would require 100 iterations of the shorter timer.
+ setInterval(() => { y++; }, 1000);
+
+ let elapsed = testZoneSpec.flush(20, true);
+
+ // Should stop right after the longer timer has been processed.
+ expect(elapsed).toEqual(1000);
+
+ expect(x).toEqual(100);
+ expect(y).toEqual(1);
+ });
+ });
+ });
+
+ describe('outside of FakeAsync Zone', () => {
+ it('calling flushMicrotasks should throw exception', () => {
+ expect(() => {
+ testZoneSpec.flushMicrotasks();
+ }).toThrowError('The code should be running in the fakeAsync zone to call this function');
+ });
+ it('calling tick should throw exception', () => {
+ expect(() => {
+ testZoneSpec.tick();
+ }).toThrowError('The code should be running in the fakeAsync zone to call this function');
+ });
+ });
+
+ describe('requestAnimationFrame', () => {
+ const functions =
+ ['requestAnimationFrame', 'webkitRequestAnimationFrame', 'mozRequestAnimationFrame'];
+ functions.forEach((fnName) => {
+ describe(fnName, ifEnvSupports(fnName, () => {
+ it('should schedule a requestAnimationFrame with timeout of 16ms', () => {
+ fakeAsyncTestZone.run(() => {
+ let ran = false;
+ requestAnimationFrame(() => { ran = true; });
+
+ testZoneSpec.tick(6);
+ expect(ran).toEqual(false);
+
+ testZoneSpec.tick(10);
+ expect(ran).toEqual(true);
+ });
+ });
+ it('does not count as a pending timer', () => {
+ fakeAsyncTestZone.run(() => { requestAnimationFrame(() => {}); });
+ expect(testZoneSpec.pendingTimers.length).toBe(0);
+ expect(testZoneSpec.pendingPeriodicTimers.length).toBe(0);
+ });
+ it('should cancel a scheduled requestAnimatiomFrame', () => {
+ fakeAsyncTestZone.run(() => {
+ let ran = false;
+ const id = requestAnimationFrame(() => { ran = true; });
+
+ testZoneSpec.tick(6);
+ expect(ran).toEqual(false);
+
+ cancelAnimationFrame(id);
+
+ testZoneSpec.tick(10);
+ expect(ran).toEqual(false);
+ });
+ });
+ it('is not flushed when flushPeriodic is false', () => {
+ let ran = false;
+ fakeAsyncTestZone.run(() => {
+ requestAnimationFrame(() => { ran = true; });
+ testZoneSpec.flush(20);
+ expect(ran).toEqual(false);
+ });
+ });
+ it('is flushed when flushPeriodic is true', () => {
+ let ran = false;
+ fakeAsyncTestZone.run(() => {
+ requestAnimationFrame(() => { ran = true; });
+ const elapsed = testZoneSpec.flush(20, true);
+ expect(elapsed).toEqual(16);
+ expect(ran).toEqual(true);
+ });
+ });
+ it('should pass timestamp as parameter', () => {
+ let timestamp = 0;
+ let timestamp1 = 0;
+ fakeAsyncTestZone.run(() => {
+ requestAnimationFrame((ts) => {
+ timestamp = ts;
+ requestAnimationFrame(ts1 => { timestamp1 = ts1; });
+ });
+ const elapsed = testZoneSpec.flush(20, true);
+ const elapsed1 = testZoneSpec.flush(20, true);
+ expect(elapsed).toEqual(16);
+ expect(elapsed1).toEqual(16);
+ expect(timestamp).toEqual(16);
+ expect(timestamp1).toEqual(32);
+ });
+ });
+ }));
+ });
+ });
+
+ describe(
+ 'XHRs', ifEnvSupports('XMLHttpRequest', () => {
+ it('should throw an exception if an XHR is initiated in the zone', () => {
+ expect(() => {
+ fakeAsyncTestZone.run(() => {
+ let finished = false;
+ let req = new XMLHttpRequest();
+
+ req.onreadystatechange = () => {
+ if (req.readyState === XMLHttpRequest.DONE) {
+ finished = true;
+ }
+ };
+
+ req.open('GET', '/test', true);
+ req.send();
+ });
+ }).toThrowError('Cannot make XHRs from within a fake async test. Request URL: /test');
+ });
+ }));
+
+ describe('node process', ifEnvSupports(supportNode, () => {
+ it('should be able to schedule microTask with additional arguments', () => {
+ const process = global['process'];
+ const nextTick = process && process['nextTick'];
+ if (!nextTick) {
+ return;
+ }
+ fakeAsyncTestZone.run(() => {
+ let tickRun = false;
+ let cbArgRun = false;
+ nextTick(
+ (strArg: string, cbArg: Function) => {
+ tickRun = true;
+ expect(strArg).toEqual('stringArg');
+ cbArg();
+ },
+ 'stringArg', () => { cbArgRun = true; });
+
+ expect(tickRun).toEqual(false);
+
+ testZoneSpec.flushMicrotasks();
+ expect(tickRun).toEqual(true);
+ expect(cbArgRun).toEqual(true);
+ });
+ });
+ }));
+
+ describe('should allow user define which macroTask fakeAsyncTest', () => {
+ let FakeAsyncTestZoneSpec = (Zone as any)['FakeAsyncTestZoneSpec'];
+ let testZoneSpec: any;
+ let fakeAsyncTestZone: Zone;
+ it('should support custom non perodic macroTask', () => {
+ testZoneSpec = new FakeAsyncTestZoneSpec(
+ 'name', false, [{source: 'TestClass.myTimeout', callbackArgs: ['test']}]);
+ class TestClass {
+ myTimeout(callback: Function) {}
+ }
+ fakeAsyncTestZone = Zone.current.fork(testZoneSpec);
+ fakeAsyncTestZone.run(() => {
+ let ran = false;
+ patchMacroTask(
+ TestClass.prototype, 'myTimeout',
+ (self: any, args: any[]) =>
+ ({name: 'TestClass.myTimeout', target: self, cbIdx: 0, args: args}));
+
+ const testClass = new TestClass();
+ testClass.myTimeout(function(callbackArgs: any) {
+ ran = true;
+ expect(callbackArgs).toEqual('test');
+ });
+
+ expect(ran).toEqual(false);
+
+ testZoneSpec.tick();
+ expect(ran).toEqual(true);
+ });
+ });
+
+ it('should support custom non perodic macroTask by global flag', () => {
+ testZoneSpec = new FakeAsyncTestZoneSpec('name');
+ class TestClass {
+ myTimeout(callback: Function) {}
+ }
+ fakeAsyncTestZone = Zone.current.fork(testZoneSpec);
+ fakeAsyncTestZone.run(() => {
+ let ran = false;
+ patchMacroTask(
+ TestClass.prototype, 'myTimeout',
+ (self: any, args: any[]) =>
+ ({name: 'TestClass.myTimeout', target: self, cbIdx: 0, args: args}));
+
+ const testClass = new TestClass();
+ testClass.myTimeout(() => { ran = true; });
+
+ expect(ran).toEqual(false);
+
+ testZoneSpec.tick();
+ expect(ran).toEqual(true);
+ });
+ });
+
+
+ it('should support custom perodic macroTask', () => {
+ testZoneSpec = new FakeAsyncTestZoneSpec(
+ 'name', false, [{source: 'TestClass.myInterval', isPeriodic: true}]);
+ fakeAsyncTestZone = Zone.current.fork(testZoneSpec);
+ fakeAsyncTestZone.run(() => {
+ let cycle = 0;
+ class TestClass {
+ myInterval(callback: Function, interval: number): any { return null; }
+ }
+ patchMacroTask(
+ TestClass.prototype, 'myInterval',
+ (self: any, args: any[]) =>
+ ({name: 'TestClass.myInterval', target: self, cbIdx: 0, args: args}));
+
+ const testClass = new TestClass();
+ const id = testClass.myInterval(() => { cycle++; }, 10);
+
+ expect(cycle).toEqual(0);
+
+ testZoneSpec.tick(10);
+ expect(cycle).toEqual(1);
+
+ testZoneSpec.tick(10);
+ expect(cycle).toEqual(2);
+ clearInterval(id);
+ });
+ });
+ });
+
+ describe('return promise', () => {
+ let log: string[];
+ beforeEach(() => { log = []; });
+
+ it('should wait for promise to resolve', () => {
+ return new Promise((res, _) => {
+ setTimeout(() => {
+ log.push('resolved');
+ res();
+ }, 100);
+ });
+ });
+
+ afterEach(() => { expect(log).toEqual(['resolved']); });
+ });
+
+ describe('fakeAsyncTest should patch Date', () => {
+ let FakeAsyncTestZoneSpec = (Zone as any)['FakeAsyncTestZoneSpec'];
+ let testZoneSpec: any;
+ let fakeAsyncTestZone: Zone;
+
+ beforeEach(() => {
+ testZoneSpec = new FakeAsyncTestZoneSpec('name', false);
+ fakeAsyncTestZone = Zone.current.fork(testZoneSpec);
+ });
+
+ it('should get date diff correctly', () => {
+ fakeAsyncTestZone.run(() => {
+ const start = Date.now();
+ testZoneSpec.tick(100);
+ const end = Date.now();
+ expect(end - start).toBe(100);
+ });
+ });
+
+ it('should check date type correctly', () => {
+ fakeAsyncTestZone.run(() => {
+ const d: any = new Date();
+ expect(d instanceof Date).toBe(true);
+ });
+ });
+
+ it('should new Date with parameter correctly', () => {
+ fakeAsyncTestZone.run(() => {
+ const d: Date = new Date(0);
+ expect(d.getFullYear()).toBeLessThan(1971);
+ const d1: Date = new Date('December 17, 1995 03:24:00');
+ expect(d1.getFullYear()).toEqual(1995);
+ const d2: Date = new Date(1995, 11, 17, 3, 24, 0);
+ expect(d2.getFullYear()).toEqual(1995);
+
+ d2.setFullYear(1985);
+ expect(isNaN(d2.getTime())).toBeFalsy();
+ expect(d2.getFullYear()).toBe(1985);
+ expect(d2.getMonth()).toBe(11);
+ expect(d2.getDate()).toBe(17);
+ });
+ });
+
+ it('should get Date.UTC() correctly', () => {
+ fakeAsyncTestZone.run(() => {
+ const utcDate = new Date(Date.UTC(96, 11, 1, 0, 0, 0));
+ expect(utcDate.getFullYear()).toBe(1996);
+ });
+ });
+
+ it('should call Date.parse() correctly', () => {
+ fakeAsyncTestZone.run(() => {
+ const unixTimeZero = Date.parse('01 Jan 1970 00:00:00 GMT');
+ expect(unixTimeZero).toBe(0);
+ });
+ });
+ });
+
+ describe(
+ 'fakeAsyncTest should work without patch jasmine.clock',
+ ifEnvSupports(
+ () => { return !supportClock() && supportNode(); },
+ () => {
+ const fakeAsync = (Zone as any)[Zone.__symbol__('fakeAsyncTest')].fakeAsync;
+ let spy: any;
+ beforeEach(() => {
+ spy = jasmine.createSpy('timer');
+ jasmine.clock().install();
+ });
+
+ afterEach(() => { jasmine.clock().uninstall(); });
+
+ it('should check date type correctly', fakeAsync(() => {
+ const d: any = new Date();
+ expect(d instanceof Date).toBe(true);
+ }));
+
+ it('should check date type correctly without fakeAsync', () => {
+ const d: any = new Date();
+ expect(d instanceof Date).toBe(true);
+ });
+
+ it('should tick correctly', fakeAsync(() => {
+ jasmine.clock().mockDate();
+ const start = Date.now();
+ jasmine.clock().tick(100);
+ const end = Date.now();
+ expect(end - start).toBe(100);
+ }));
+
+ it('should tick correctly without fakeAsync', () => {
+ jasmine.clock().mockDate();
+ const start = Date.now();
+ jasmine.clock().tick(100);
+ const end = Date.now();
+ expect(end - start).toBe(100);
+ });
+
+ it('should mock date correctly', fakeAsync(() => {
+ 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);
+ expect(new Date().getFullYear()).toEqual(2013);
+ }));
+
+ it('should mock date correctly without fakeAsync', () => {
+ 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);
+ expect(new Date().getFullYear()).toEqual(2013);
+ });
+
+ it('should handle new Date correctly', fakeAsync(() => {
+ 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 handle new Date correctly without fakeAsync', () => {
+ 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 handle setTimeout correctly', fakeAsync(() => {
+ setTimeout(spy, 100);
+ expect(spy).not.toHaveBeenCalled();
+ jasmine.clock().tick(100);
+ expect(spy).toHaveBeenCalled();
+ }));
+
+ it('should handle setTimeout correctly without fakeAsync', () => {
+ setTimeout(spy, 100);
+ expect(spy).not.toHaveBeenCalled();
+ jasmine.clock().tick(100);
+ expect(spy).toHaveBeenCalled();
+ });
+ }));
+
+ describe('fakeAsyncTest should patch jasmine.clock', ifEnvSupports(supportClock, () => {
+ let spy: any;
+ beforeEach(() => {
+ spy = jasmine.createSpy('timer');
+ jasmine.clock().install();
+ });
+
+ afterEach(() => { jasmine.clock().uninstall(); });
+
+ it('should check date type correctly', () => {
+ const d: any = new Date();
+ expect(d instanceof Date).toBe(true);
+ });
+
+ 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 tick 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 handle setTimeout correctly', () => {
+ setTimeout(spy, 100);
+ expect(spy).not.toHaveBeenCalled();
+ jasmine.clock().tick(100);
+ expect(spy).toHaveBeenCalled();
+ });
+ }));
+
+ describe('fakeAsyncTest should patch rxjs scheduler', ifEnvSupports(supportClock, () => {
+ let FakeAsyncTestZoneSpec = (Zone as any)['FakeAsyncTestZoneSpec'];
+ let testZoneSpec: any;
+ let fakeAsyncTestZone: Zone;
+
+ beforeEach(() => {
+ testZoneSpec = new FakeAsyncTestZoneSpec('name', false);
+ fakeAsyncTestZone = Zone.current.fork(testZoneSpec);
+ });
+
+ it('should get date diff correctly', (done) => {
+ fakeAsyncTestZone.run(() => {
+ let result: any = null;
+ const observable = new Observable((subscribe: any) => {
+ subscribe.next('hello');
+ subscribe.complete();
+ });
+ observable.pipe(delay(1000)).subscribe((v: any) => { result = v; });
+ expect(result).toBe(null);
+ testZoneSpec.tick(1000);
+ expect(result).toBe('hello');
+ done();
+ });
+ });
+ }));
+});
+
+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'];
+const fakeAsyncTestModule = (Zone as any)[Zone.__symbol__('fakeAsyncTest')];
+const {fakeAsync, tick, discardPeriodicTasks, flush, flushMicrotasks} = fakeAsyncTestModule;
+
+{
+ 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('fakeAsync should work with Date', () => {
+ it('should get date diff correctly', fakeAsync(() => {
+ const start = Date.now();
+ tick(100);
+ const end = Date.now();
+ expect(end - start).toBe(100);
+ }));
+
+ it('should check date type correctly', fakeAsync(() => {
+ const d: any = new Date();
+ expect(d instanceof Date).toBe(true);
+ }));
+
+ it('should new Date with parameter correctly', fakeAsync(() => {
+ const d: Date = new Date(0);
+ expect(d.getFullYear()).toBeLessThan(1971);
+ const d1: Date = new Date('December 17, 1995 03:24:00');
+ expect(d1.getFullYear()).toEqual(1995);
+ const d2: Date = new Date(1995, 11, 17, 3, 24, 0);
+ expect(isNaN(d2.getTime())).toBeFalsy();
+ expect(d2.getFullYear()).toEqual(1995);
+ d2.setFullYear(1985);
+ expect(d2.getFullYear()).toBe(1985);
+ expect(d2.getMonth()).toBe(11);
+ expect(d2.getDate()).toBe(17);
+ }));
+
+ it('should get Date.UTC() correctly', fakeAsync(() => {
+ const utcDate = new Date(Date.UTC(96, 11, 1, 0, 0, 0));
+ expect(utcDate.getFullYear()).toBe(1996);
+ }));
+
+ it('should call Date.parse() correctly', fakeAsync(() => {
+ const unixTimeZero = Date.parse('01 Jan 1970 00:00:00 GMT');
+ expect(unixTimeZero).toBe(0);
+ }));
+ });
+ });
+
+ 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');
+ });
+ });
+}
diff --git a/packages/zone.js/test/zone-spec/long-stack-trace-zone.spec.ts b/packages/zone.js/test/zone-spec/long-stack-trace-zone.spec.ts
new file mode 100644
index 0000000000..4509f6b0dd
--- /dev/null
+++ b/packages/zone.js/test/zone-spec/long-stack-trace-zone.spec.ts
@@ -0,0 +1,185 @@
+/**
+ * @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
+ */
+
+import {isBrowser, isIE, zoneSymbol} from '../../lib/common/utils';
+import {ifEnvSupports, isSafari, isSupportSetErrorStack} from '../test-util';
+
+const defineProperty = (Object as any)[zoneSymbol('defineProperty')] || Object.defineProperty;
+
+describe(
+ 'longStackTraceZone', ifEnvSupports(isSupportSetErrorStack, function() {
+ let log: Error[];
+ let lstz: Zone;
+ let longStackTraceZoneSpec = (Zone as any)['longStackTraceZoneSpec'];
+ let defaultTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
+
+ beforeEach(function() {
+ lstz = Zone.current.fork(longStackTraceZoneSpec).fork({
+ name: 'long-stack-trace-zone-test',
+ onHandleError: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
+ error: any): boolean => {
+ parentZoneDelegate.handleError(targetZone, error);
+ log.push(error);
+ return false;
+ }
+ });
+
+ log = [];
+ jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
+ });
+
+ afterEach(function() { jasmine.DEFAULT_TIMEOUT_INTERVAL = defaultTimeout; });
+
+ function expectElapsed(stack: string, expectedCount: number) {
+ try {
+ let actualCount = stack.split('_Elapsed_').length;
+ if (actualCount !== expectedCount) {
+ expect(actualCount).toEqual(expectedCount);
+ console.log(stack);
+ }
+ } catch (e) {
+ expect(e).toBe(null);
+ }
+ }
+
+ it('should produce long stack traces', function(done) {
+ lstz.run(function() {
+ setTimeout(function() {
+ setTimeout(function() {
+ setTimeout(function() {
+ expectElapsed(log[0].stack !, 3);
+ done();
+ }, 0);
+ throw new Error('Hello');
+ }, 0);
+ }, 0);
+ });
+ });
+
+ it('should produce long stack traces for optimized eventTask',
+ ifEnvSupports(() => isBrowser, function() {
+ lstz.run(function() {
+ const button = document.createElement('button');
+ const clickEvent = document.createEvent('Event');
+ clickEvent.initEvent('click', true, true);
+ document.body.appendChild(button);
+
+ button.addEventListener('click', function() { expectElapsed(log[0].stack !, 1); });
+
+ button.dispatchEvent(clickEvent);
+
+ document.body.removeChild(button);
+ });
+ }));
+
+ it('should not overwrite long stack traces data for different optimized eventTasks',
+ ifEnvSupports(() => isBrowser, function() {
+ lstz.run(function() {
+ const button = document.createElement('button');
+ const clickEvent = document.createEvent('Event');
+ clickEvent.initEvent('click', true, true);
+ document.body.appendChild(button);
+
+ const div = document.createElement('div');
+ const enterEvent = document.createEvent('Event');
+ enterEvent.initEvent('mouseenter', true, true);
+ document.body.appendChild(div);
+
+ button.addEventListener('click', function() { throw new Error('clickError'); });
+
+ div.addEventListener('mouseenter', function() { throw new Error('enterError'); });
+
+ button.dispatchEvent(clickEvent);
+ div.dispatchEvent(enterEvent);
+
+ expect(log.length).toBe(2);
+ if (!isSafari() && !isIE()) {
+ expect(log[0].stack === log[1].stack).toBe(false);
+ }
+
+ document.body.removeChild(button);
+ document.body.removeChild(div);
+ });
+ }));
+
+ it('should produce a long stack trace even if stack setter throws', (done) => {
+ let wasStackAssigned = false;
+ let error = new Error('Expected error');
+ defineProperty(error, 'stack', {
+ configurable: false,
+ get: () => 'someStackTrace',
+ set: (v: any) => { throw new Error('no writes'); }
+ });
+ lstz.run(() => { setTimeout(() => { throw error; }); });
+ setTimeout(() => {
+ const e = log[0];
+ expect((e as any).longStack).toBeTruthy();
+ done();
+ });
+ });
+
+ it('should produce long stack traces when has uncaught error in promise', function(done) {
+ lstz.runGuarded(function() {
+ setTimeout(function() {
+ setTimeout(function() {
+ let promise = new Promise(function(resolve, reject) {
+ setTimeout(function() { reject(new Error('Hello Promise')); }, 0);
+ });
+ promise.then(function() { fail('should not get here'); });
+ setTimeout(function() {
+ expectElapsed(log[0].stack !, 5);
+ done();
+ }, 0);
+ }, 0);
+ }, 0);
+ });
+ });
+
+ it('should produce long stack traces when handling error in promise', function(done) {
+ lstz.runGuarded(function() {
+ setTimeout(function() {
+ setTimeout(function() {
+ let promise = new Promise(function(resolve, reject) {
+ setTimeout(function() {
+ try {
+ throw new Error('Hello Promise');
+ } catch (err) {
+ reject(err);
+ }
+ }, 0);
+ });
+ promise.catch(function(error) {
+ // should be able to get long stack trace
+ const longStackFrames: string = longStackTraceZoneSpec.getLongStackTrace(error);
+ expectElapsed(longStackFrames, 4);
+ done();
+ });
+ }, 0);
+ }, 0);
+ });
+ });
+
+ it('should not produce long stack traces if Error.stackTraceLimit = 0', function(done) {
+ const originalStackTraceLimit = Error.stackTraceLimit;
+ lstz.run(function() {
+ setTimeout(function() {
+ setTimeout(function() {
+ setTimeout(function() {
+ if (log[0].stack) {
+ expectElapsed(log[0].stack !, 1);
+ }
+ Error.stackTraceLimit = originalStackTraceLimit;
+ done();
+ }, 0);
+ Error.stackTraceLimit = 0;
+ throw new Error('Hello');
+ }, 0);
+ }, 0);
+ });
+ });
+ }));
diff --git a/packages/zone.js/test/zone-spec/proxy.spec.ts b/packages/zone.js/test/zone-spec/proxy.spec.ts
new file mode 100644
index 0000000000..a6820a5505
--- /dev/null
+++ b/packages/zone.js/test/zone-spec/proxy.spec.ts
@@ -0,0 +1,179 @@
+/**
+ * @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
+ */
+
+describe('ProxySpec', () => {
+ let ProxyZoneSpec: any;
+ let delegate: ZoneSpec;
+ let proxyZoneSpec: any;
+ let proxyZone: Zone;
+
+ beforeEach(() => {
+ ProxyZoneSpec = (Zone as any)['ProxyZoneSpec'];
+ expect(typeof ProxyZoneSpec).toBe('function');
+ delegate = {name: 'delegate'};
+ proxyZoneSpec = new ProxyZoneSpec(delegate);
+ proxyZone = Zone.current.fork(proxyZoneSpec);
+ });
+
+ describe('properties', () => {
+ it('should expose ProxyZone in the properties',
+ () => { expect(proxyZone.get('ProxyZoneSpec')).toBe(proxyZoneSpec); });
+
+ it('should assert that it is in or out of ProxyZone', () => {
+ let rootZone = Zone.current;
+ while (rootZone.parent) {
+ rootZone = rootZone.parent;
+ }
+ rootZone.run(() => {
+ expect(() => ProxyZoneSpec.assertPresent()).toThrow();
+ expect(ProxyZoneSpec.isLoaded()).toBe(false);
+ expect(ProxyZoneSpec.get()).toBe(undefined);
+ proxyZone.run(() => {
+ expect(ProxyZoneSpec.isLoaded()).toBe(true);
+ expect(() => ProxyZoneSpec.assertPresent()).not.toThrow();
+ expect(ProxyZoneSpec.get()).toBe(proxyZoneSpec);
+ });
+ });
+ });
+
+ it('should reset properties', () => {
+ expect(proxyZone.get('myTestKey')).toBe(undefined);
+ proxyZoneSpec.setDelegate({name: 'd1', properties: {'myTestKey': 'myTestValue'}});
+ expect(proxyZone.get('myTestKey')).toBe('myTestValue');
+ proxyZoneSpec.resetDelegate();
+ expect(proxyZone.get('myTestKey')).toBe(undefined);
+ });
+ });
+
+ describe('delegate', () => {
+ it('should set/reset delegate', () => {
+ const defaultDelegate: ZoneSpec = {name: 'defaultDelegate'};
+ const otherDelegate: ZoneSpec = {name: 'otherDelegate'};
+ const proxyZoneSpec = new ProxyZoneSpec(defaultDelegate);
+ const proxyZone = Zone.current.fork(proxyZoneSpec);
+
+ expect(proxyZoneSpec.getDelegate()).toEqual(defaultDelegate);
+
+ proxyZoneSpec.setDelegate(otherDelegate);
+ expect(proxyZoneSpec.getDelegate()).toEqual(otherDelegate);
+ proxyZoneSpec.resetDelegate();
+ expect(proxyZoneSpec.getDelegate()).toEqual(defaultDelegate);
+ });
+ });
+
+ describe('forwarding', () => {
+ beforeEach(() => {
+ proxyZoneSpec = new ProxyZoneSpec();
+ proxyZone = Zone.current.fork(proxyZoneSpec);
+ });
+
+ it('should fork', () => {
+ const forkedZone = proxyZone.fork({name: 'fork'});
+ expect(forkedZone).not.toBe(proxyZone);
+ expect(forkedZone.name).toBe('fork');
+ let called = false;
+ proxyZoneSpec.setDelegate({
+ name: '.',
+ onFork: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
+ zoneSpec: ZoneSpec) => {
+ expect(currentZone).toBe(proxyZone);
+ expect(targetZone).toBe(proxyZone), expect(zoneSpec.name).toBe('fork2');
+ called = true;
+ }
+ });
+ proxyZone.fork({name: 'fork2'});
+ expect(called).toBe(true);
+ });
+
+ it('should intercept', () => {
+ const fn = (a: any) => a;
+ expect(proxyZone.wrap(fn, 'test')('works')).toEqual('works');
+ proxyZoneSpec.setDelegate({
+ name: '.',
+ onIntercept: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
+ delegate: Function, source: string): Function => { return () => '(works)'; }
+ });
+ expect(proxyZone.wrap(fn, 'test')('works')).toEqual('(works)');
+ });
+
+ it('should invoke', () => {
+ const fn = () => 'works';
+ expect(proxyZone.run(fn)).toEqual('works');
+ proxyZoneSpec.setDelegate({
+ name: '.',
+ onInvoke: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
+ delegate: Function, applyThis: any, applyArgs: any[], source: string) => {
+ return `(${
+ parentZoneDelegate.invoke(targetZone, delegate, applyThis, applyArgs, source)})`;
+ }
+ });
+ expect(proxyZone.run(fn)).toEqual('(works)');
+ });
+
+ it('should handleError', () => {
+ const error = new Error('TestError');
+ const fn = () => { throw error; };
+ expect(() => proxyZone.run(fn)).toThrow(error);
+ proxyZoneSpec.setDelegate({
+ name: '.',
+ onHandleError: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
+ error: any): boolean => {
+ expect(error).toEqual(error);
+ return false;
+ }
+ });
+ expect(() => proxyZone.runGuarded(fn)).not.toThrow();
+ });
+
+ it('should Task', () => {
+ const fn = (): any => null;
+ const task = proxyZone.scheduleMacroTask('test', fn, {}, () => null, () => null);
+ expect(task.source).toEqual('test');
+ proxyZone.cancelTask(task);
+ });
+ });
+
+ describe('delegateSpec change', () => {
+ let log: string[] = [];
+ beforeEach(() => { log = []; });
+ it('should trigger hasTask when invoke', (done: Function) => {
+ const zoneSpec1 = {
+ name: 'zone1',
+ onHasTask: (delegate: ZoneDelegate, curr: Zone, target: Zone, hasTask: HasTaskState) => {
+ log.push(`zoneSpec1 hasTask: ${hasTask.microTask},${hasTask.macroTask}`);
+ return delegate.hasTask(target, hasTask);
+ }
+ };
+ const zoneSpec2 = {
+ name: 'zone2',
+ onHasTask: (delegate: ZoneDelegate, curr: Zone, target: Zone, hasTask: HasTaskState) => {
+ log.push(`zoneSpec2 hasTask: ${hasTask.microTask},${hasTask.macroTask}`);
+ return delegate.hasTask(target, hasTask);
+ }
+ };
+ proxyZoneSpec.setDelegate(zoneSpec1);
+ proxyZone.run(() => { setTimeout(() => { log.push('timeout in zoneSpec1'); }, 50); });
+ proxyZoneSpec.setDelegate(zoneSpec2);
+ proxyZone.run(() => { Promise.resolve(1).then(() => { log.push('then in zoneSpec2'); }); });
+ proxyZoneSpec.setDelegate(null);
+ proxyZone.run(() => { setTimeout(() => { log.push('timeout in null spec'); }, 50); });
+ proxyZoneSpec.setDelegate(zoneSpec2);
+ proxyZone.run(() => { Promise.resolve(1).then(() => { log.push('then in zoneSpec2'); }); });
+
+ 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'
+ ]);
+ done();
+ }, 300);
+ });
+ });
+});
diff --git a/packages/zone.js/test/zone-spec/sync-test.spec.ts b/packages/zone.js/test/zone-spec/sync-test.spec.ts
new file mode 100644
index 0000000000..207657e278
--- /dev/null
+++ b/packages/zone.js/test/zone-spec/sync-test.spec.ts
@@ -0,0 +1,57 @@
+/**
+ * @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
+ */
+
+import {ifEnvSupports} from '../test-util';
+
+describe('SyncTestZoneSpec', () => {
+ const SyncTestZoneSpec = (Zone as any)['SyncTestZoneSpec'];
+ let testZoneSpec;
+ let syncTestZone: Zone;
+
+ beforeEach(() => {
+ testZoneSpec = new SyncTestZoneSpec('name');
+ syncTestZone = Zone.current.fork(testZoneSpec);
+ });
+
+ it('should fail on Promise.then', () => {
+ syncTestZone.run(() => {
+ expect(() => {
+ Promise.resolve().then(function() {});
+ }).toThrow(new Error('Cannot call Promise.then from within a sync test.'));
+ });
+ });
+
+ it('should fail on setTimeout', () => {
+ syncTestZone.run(() => {
+ expect(() => {
+ setTimeout(() => {}, 100);
+ }).toThrow(new Error('Cannot call setTimeout from within a sync test.'));
+ });
+ });
+
+ describe('event tasks', ifEnvSupports('document', () => {
+ it('should work with event tasks', () => {
+ syncTestZone.run(() => {
+ const button = document.createElement('button');
+ document.body.appendChild(button);
+ let x = 1;
+ try {
+ button.addEventListener('click', () => { x++; });
+
+ button.click();
+ expect(x).toEqual(2);
+
+ button.click();
+ expect(x).toEqual(3);
+ } finally {
+ document.body.removeChild(button);
+ }
+ });
+ });
+ }));
+});
diff --git a/packages/zone.js/test/zone-spec/task-tracking.spec.ts b/packages/zone.js/test/zone-spec/task-tracking.spec.ts
new file mode 100644
index 0000000000..dbfe563daf
--- /dev/null
+++ b/packages/zone.js/test/zone-spec/task-tracking.spec.ts
@@ -0,0 +1,76 @@
+/**
+ * @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
+ */
+
+import {supportPatchXHROnProperty} from '../test-util';
+
+declare const global: any;
+
+describe('TaskTrackingZone', function() {
+ let _TaskTrackingZoneSpec: typeof TaskTrackingZoneSpec = (Zone as any)['TaskTrackingZoneSpec'];
+ let taskTrackingZoneSpec: TaskTrackingZoneSpec|null = null;
+ let taskTrackingZone: Zone;
+
+ beforeEach(() => {
+ taskTrackingZoneSpec = new _TaskTrackingZoneSpec();
+ taskTrackingZone = Zone.current.fork(taskTrackingZoneSpec);
+ });
+
+ it('should track tasks', (done: Function) => {
+ taskTrackingZone.run(() => {
+ taskTrackingZone.scheduleMicroTask('test1', () => {});
+ expect(taskTrackingZoneSpec !.microTasks.length).toBe(1);
+ expect(taskTrackingZoneSpec !.microTasks[0].source).toBe('test1');
+
+ setTimeout(() => {});
+ expect(taskTrackingZoneSpec !.macroTasks.length).toBe(1);
+ expect(taskTrackingZoneSpec !.macroTasks[0].source).toBe('setTimeout');
+ taskTrackingZone.cancelTask(taskTrackingZoneSpec !.macroTasks[0]);
+ expect(taskTrackingZoneSpec !.macroTasks.length).toBe(0);
+
+ setTimeout(() => {
+ // assert on execution it is null
+ expect(taskTrackingZoneSpec !.macroTasks.length).toBe(0);
+ expect(taskTrackingZoneSpec !.microTasks.length).toBe(0);
+
+ // If a browser does not have XMLHttpRequest, then end test here.
+ if (typeof global['XMLHttpRequest'] == 'undefined') return done();
+ const xhr = new XMLHttpRequest();
+ xhr.open('get', '/', true);
+ xhr.onreadystatechange = () => {
+ if (xhr.readyState == 4) {
+ // clear current event tasks using setTimeout
+ setTimeout(() => {
+ expect(taskTrackingZoneSpec !.macroTasks.length).toBe(0);
+ expect(taskTrackingZoneSpec !.microTasks.length).toBe(0);
+ if (supportPatchXHROnProperty()) {
+ expect(taskTrackingZoneSpec !.eventTasks.length).not.toBe(0);
+ }
+ taskTrackingZoneSpec !.clearEvents();
+ expect(taskTrackingZoneSpec !.eventTasks.length).toBe(0);
+ done();
+ });
+ }
+ };
+ xhr.send();
+ expect(taskTrackingZoneSpec !.macroTasks.length).toBe(1);
+ expect(taskTrackingZoneSpec !.macroTasks[0].source).toBe('XMLHttpRequest.send');
+ if (supportPatchXHROnProperty()) {
+ expect(taskTrackingZoneSpec !.eventTasks[0].source)
+ .toMatch(/\.addEventListener:readystatechange/);
+ }
+ });
+ });
+ });
+
+ it('should capture task creation stacktrace', (done) => {
+ taskTrackingZone.run(() => {
+ setTimeout(() => { done(); });
+ expect((taskTrackingZoneSpec !.macroTasks[0] as any)['creationLocation']).toBeTruthy();
+ });
+ });
+});
diff --git a/packages/zone.js/test/zone_worker_entry_point.ts b/packages/zone.js/test/zone_worker_entry_point.ts
new file mode 100644
index 0000000000..d12473781f
--- /dev/null
+++ b/packages/zone.js/test/zone_worker_entry_point.ts
@@ -0,0 +1,31 @@
+/**
+ * @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
+ */
+
+// Setup tests for Zone without microtask support
+System.config({defaultJSExtensions: true});
+System.import('../lib/browser/api-util').then(() => {
+ System.import('../lib/browser/browser-legacy').then(() => {
+ System.import('../lib/browser/browser').then(() => {
+ const _global = typeof window !== 'undefined' ? window : self;
+ Zone.current.fork({name: 'webworker'}).run(() => {
+ const websocket = new WebSocket('ws://localhost:8001');
+ websocket.addEventListener('open', () => {
+ websocket.onmessage = () => {
+ if ((self).Zone.current.name === 'webworker') {
+ (