From 5eb7426216b1597cca3a7923cc714e59464fec89 Mon Sep 17 00:00:00 2001 From: JiaLiPassion Date: Sat, 1 Jun 2019 00:56:07 +0900 Subject: [PATCH] build: move zone.js to angular repo (#30962) PR Close #30962 --- .circleci/config.yml | 14 + BUILD.bazel | 1 + package.json | 9 +- packages/tsconfig.json | 1 + packages/zone.js/BUILD.bazel | 47 + packages/zone.js/CHANGELOG.md | 1322 +++++++++ packages/zone.js/DEVELOPER.md | 90 + packages/zone.js/MODULE.md | 139 + packages/zone.js/NON-STANDARD-APIS.md | 229 ++ packages/zone.js/README.md | 92 + packages/zone.js/SAMPLE.md | 23 + packages/zone.js/STANDARD-APIS.md | 148 ++ packages/zone.js/bundles.bzl | 50 + packages/zone.js/check-file-size.js | 28 + packages/zone.js/dist/BUILD.bazel | 200 ++ packages/zone.js/doc/error.png | Bin 0 -> 26146 bytes packages/zone.js/doc/error.puml | 9 + packages/zone.js/doc/eventtask.png | Bin 0 -> 33058 bytes packages/zone.js/doc/eventtask.puml | 21 + packages/zone.js/doc/microtask.png | Bin 0 -> 21724 bytes packages/zone.js/doc/microtask.puml | 14 + .../zone.js/doc/non-periodical-macrotask.png | Bin 0 -> 31203 bytes .../zone.js/doc/non-periodical-macrotask.puml | 18 + packages/zone.js/doc/override-task.png | Bin 0 -> 31190 bytes packages/zone.js/doc/override-task.puml | 18 + packages/zone.js/doc/periodical-macrotask.png | Bin 0 -> 30867 bytes .../zone.js/doc/periodical-macrotask.puml | 18 + packages/zone.js/doc/reschedule-task.png | Bin 0 -> 33185 bytes packages/zone.js/doc/reschedule-task.puml | 20 + packages/zone.js/doc/task.md | 80 + packages/zone.js/example/basic.html | 59 + .../example/benchmarks/addEventListener.html | 65 + .../example/benchmarks/event_emitter.js | 50 + packages/zone.js/example/counting.html | 95 + packages/zone.js/example/css/style.css | 8 + packages/zone.js/example/index.html | 21 + packages/zone.js/example/js/counting-zone.js | 33 + packages/zone.js/example/profiling.html | 126 + packages/zone.js/example/throttle.html | 91 + packages/zone.js/example/web-socket.html | 38 + packages/zone.js/file-size-limit.json | 14 + packages/zone.js/karma-base.conf.js | 51 + .../karma-build-jasmine-phantomjs.conf.js | 9 + packages/zone.js/karma-build-jasmine.conf.js | 7 + .../karma-build-jasmine.es2015.conf.js | 11 + packages/zone.js/karma-build-mocha.conf.js | 11 + .../zone.js/karma-build-sauce-mocha.conf.js | 12 + .../karma-build-sauce-selenium3-mocha.conf.js | 12 + packages/zone.js/karma-build.conf.js | 18 + packages/zone.js/karma-dist-jasmine.conf.js | 7 + packages/zone.js/karma-dist-mocha.conf.js | 23 + .../zone.js/karma-dist-sauce-jasmine.conf.js | 12 + .../karma-dist-sauce-jasmine.es2015.conf.js | 28 + .../zone.js/karma-dist-sauce-jasmine3.conf.js | 16 + ...karma-dist-sauce-selenium3-jasmine.conf.js | 12 + packages/zone.js/karma-dist.conf.js | 27 + .../karma-evergreen-dist-jasmine.conf.js | 7 + ...karma-evergreen-dist-sauce-jasmine.conf.js | 12 + packages/zone.js/karma-evergreen-dist.conf.js | 35 + packages/zone.js/lib/BUILD.bazel | 18 + packages/zone.js/lib/browser/api-util.ts | 52 + .../zone.js/lib/browser/browser-legacy.ts | 31 + packages/zone.js/lib/browser/browser-util.ts | 38 + packages/zone.js/lib/browser/browser.ts | 280 ++ packages/zone.js/lib/browser/canvas.ts | 16 + .../zone.js/lib/browser/custom-elements.ts | 19 + .../zone.js/lib/browser/define-property.ts | 111 + .../lib/browser/event-target-legacy.ts | 110 + packages/zone.js/lib/browser/event-target.ts | 39 + .../lib/browser/property-descriptor-legacy.ts | 124 + .../lib/browser/property-descriptor.ts | 334 +++ .../zone.js/lib/browser/register-element.ts | 19 + packages/zone.js/lib/browser/rollup-common.ts | 12 + .../zone.js/lib/browser/rollup-legacy-main.ts | 11 + .../lib/browser/rollup-legacy-test-main.ts | 12 + packages/zone.js/lib/browser/rollup-main.ts | 10 + .../zone.js/lib/browser/rollup-test-main.ts | 12 + packages/zone.js/lib/browser/shadydom.ts | 24 + .../lib/browser/webapis-media-query.ts | 65 + .../lib/browser/webapis-notification.ts | 18 + .../lib/browser/webapis-resize-observer.ts | 91 + .../browser/webapis-rtc-peer-connection.ts | 26 + .../zone.js/lib/browser/webapis-user-media.ts | 20 + packages/zone.js/lib/browser/websocket.ts | 59 + packages/zone.js/lib/closure/zone_externs.js | 445 ++++ packages/zone.js/lib/common/error-rewrite.ts | 378 +++ packages/zone.js/lib/common/events.ts | 679 +++++ packages/zone.js/lib/common/fetch.ts | 112 + packages/zone.js/lib/common/promise.ts | 481 ++++ packages/zone.js/lib/common/timers.ts | 133 + packages/zone.js/lib/common/to-string.ts | 57 + packages/zone.js/lib/common/utils.ts | 509 ++++ packages/zone.js/lib/extra/bluebird.ts | 55 + packages/zone.js/lib/extra/cordova.ts | 39 + packages/zone.js/lib/extra/electron.ts | 31 + packages/zone.js/lib/extra/jsonp.ts | 77 + packages/zone.js/lib/extra/socket-io.ts | 22 + packages/zone.js/lib/jasmine/jasmine.ts | 302 +++ packages/zone.js/lib/mix/rollup-mix.ts | 13 + packages/zone.js/lib/mocha/mocha.ts | 161 ++ packages/zone.js/lib/node/events.ts | 63 + packages/zone.js/lib/node/fs.ts | 41 + packages/zone.js/lib/node/node.ts | 154 ++ packages/zone.js/lib/node/node_util.ts | 17 + packages/zone.js/lib/node/rollup-main.ts | 12 + packages/zone.js/lib/node/rollup-test-main.ts | 12 + packages/zone.js/lib/rxjs/rxjs-fake-async.ts | 21 + packages/zone.js/lib/rxjs/rxjs.ts | 182 ++ packages/zone.js/lib/testing/async-testing.ts | 99 + packages/zone.js/lib/testing/fake-async.ts | 153 ++ .../zone.js/lib/testing/promise-testing.ts | 68 + packages/zone.js/lib/testing/zone-testing.ts | 16 + packages/zone.js/lib/zone-spec/async-test.ts | 149 ++ .../zone.js/lib/zone-spec/fake-async-test.ts | 560 ++++ .../zone.js/lib/zone-spec/long-stack-trace.ts | 183 ++ packages/zone.js/lib/zone-spec/proxy.ts | 195 ++ packages/zone.js/lib/zone-spec/sync-test.ts | 33 + .../zone.js/lib/zone-spec/task-tracking.ts | 80 + packages/zone.js/lib/zone-spec/wtf.ts | 161 ++ packages/zone.js/lib/zone.ts | 1404 ++++++++++ packages/zone.js/package.json | 36 + packages/zone.js/presentation.png | Bin 0 -> 107844 bytes packages/zone.js/promise-adapter.js | 18 + packages/zone.js/promise-test.js | 10 + packages/zone.js/promise.finally.spec.js | 358 +++ packages/zone.js/sauce-evergreen.conf.js | 66 + packages/zone.js/sauce-selenium3.conf.js | 49 + packages/zone.js/sauce.conf.js | 151 ++ packages/zone.js/sauce.es2015.conf.js | 57 + .../scripts/closure/closure_compiler.sh | 31 + .../zone.js/scripts/closure/closure_flagfile | 5 + packages/zone.js/scripts/grab-blink-idl.sh | 36 + .../scripts/sauce/sauce_connect_block.sh | 10 + .../scripts/sauce/sauce_connect_setup.sh | 49 + packages/zone.js/simple-server.js | 34 + packages/zone.js/test/BUILD.bazel | 376 +++ packages/zone.js/test/assets/import.html | 1 + packages/zone.js/test/assets/sample.json | 1 + packages/zone.js/test/assets/worker.js | 8 + packages/zone.js/test/browser-env-setup.ts | 6 + packages/zone.js/test/browser-zone-setup.ts | 29 + .../zone.js/test/browser/FileReader.spec.ts | 106 + .../zone.js/test/browser/HTMLImports.spec.ts | 79 + .../zone.js/test/browser/MediaQuery.spec.ts | 26 + .../test/browser/MutationObserver.spec.ts | 67 + .../zone.js/test/browser/Notification.spec.ts | 26 + .../zone.js/test/browser/WebSocket.spec.ts | 138 + packages/zone.js/test/browser/Worker.spec.ts | 39 + .../test/browser/XMLHttpRequest.spec.ts | 381 +++ packages/zone.js/test/browser/browser.spec.ts | 2363 +++++++++++++++++ .../test/browser/custom-element.spec.js | 96 + .../test/browser/define-property.spec.ts | 27 + packages/zone.js/test/browser/element.spec.ts | 312 +++ .../test/browser/geolocation.spec.manual.ts | 38 + .../test/browser/registerElement.spec.ts | 164 ++ .../browser/requestAnimationFrame.spec.ts | 52 + packages/zone.js/test/browser_entry_point.ts | 30 + .../test/browser_es2015_entry_point.ts | 9 + packages/zone.js/test/browser_symbol_setup.ts | 3 + packages/zone.js/test/closure/zone.closure.ts | 130 + packages/zone.js/test/common/Error.spec.ts | 425 +++ packages/zone.js/test/common/Promise.spec.ts | 521 ++++ packages/zone.js/test/common/fetch.spec.ts | 205 ++ .../zone.js/test/common/microtasks.spec.ts | 100 + .../zone.js/test/common/setInterval.spec.ts | 89 + .../zone.js/test/common/setTimeout.spec.ts | 123 + packages/zone.js/test/common/task.spec.ts | 965 +++++++ packages/zone.js/test/common/toString.spec.ts | 88 + packages/zone.js/test/common/util.spec.ts | 271 ++ packages/zone.js/test/common/zone.spec.ts | 388 +++ packages/zone.js/test/common_tests.ts | 27 + packages/zone.js/test/extra/bluebird.spec.ts | 703 +++++ packages/zone.js/test/extra/cordova.spec.ts | 35 + packages/zone.js/test/fake_entry.js | 1 + packages/zone.js/test/jasmine-patch.spec.ts | 76 + packages/zone.js/test/main.ts | 74 + packages/zone.js/test/mocha-patch.spec.ts | 104 + packages/zone.js/test/node-env-setup.ts | 2 + packages/zone.js/test/node/Error.spec.ts | 40 + packages/zone.js/test/node/console.spec.ts | 39 + packages/zone.js/test/node/crypto.spec.ts | 63 + packages/zone.js/test/node/events.spec.ts | 188 ++ packages/zone.js/test/node/fs.spec.ts | 145 + packages/zone.js/test/node/http.spec.ts | 31 + packages/zone.js/test/node/process.spec.ts | 116 + packages/zone.js/test/node/timer.spec.ts | 31 + .../zone.js/test/node_bluebird_entry_point.ts | 54 + packages/zone.js/test/node_entry_point.ts | 37 + .../test/node_entry_point_no_patch_clock.ts | 36 + .../node_error_disable_policy_entry_point.ts | 11 + .../zone.js/test/node_error_entry_point.ts | 35 + .../node_error_lazy_policy_entry_point.ts | 10 + packages/zone.js/test/node_tests.ts | 16 + .../test/npm_package/npm_package.spec.ts | 143 + packages/zone.js/test/patch/IndexedDB.spec.js | 133 + .../zone.js/test/performance/eventTarget.js | 80 + .../zone.js/test/performance/performance.html | 73 + .../test/performance/performance_setup.js | 284 ++ .../test/performance/performance_ui.js | 156 ++ packages/zone.js/test/performance/promise.js | 57 + .../test/performance/requestAnimationFrame.js | 56 + packages/zone.js/test/performance/timeout.js | 55 + packages/zone.js/test/performance/xhr.js | 47 + .../test/rxjs/rxjs.Observable.audit.spec.ts | 79 + .../test/rxjs/rxjs.Observable.buffer.spec.ts | 173 ++ .../test/rxjs/rxjs.Observable.catch.spec.ts | 83 + .../rxjs/rxjs.Observable.collection.spec.ts | 643 +++++ .../test/rxjs/rxjs.Observable.combine.spec.ts | 128 + .../test/rxjs/rxjs.Observable.concat.spec.ts | 178 ++ .../test/rxjs/rxjs.Observable.count.spec.ts | 41 + .../rxjs/rxjs.Observable.debounce.spec.ts | 65 + .../test/rxjs/rxjs.Observable.default.spec.ts | 40 + .../test/rxjs/rxjs.Observable.delay.spec.ts | 62 + .../rxjs/rxjs.Observable.distinct.spec.ts | 87 + .../test/rxjs/rxjs.Observable.do.spec.ts | 45 + .../test/rxjs/rxjs.Observable.map.spec.ts | 99 + .../test/rxjs/rxjs.Observable.merge.spec.ts | 209 ++ .../rxjs/rxjs.Observable.multicast.spec.ts | 68 + .../rxjs/rxjs.Observable.notification.spec.ts | 54 + .../test/rxjs/rxjs.Observable.race.spec.ts | 41 + .../test/rxjs/rxjs.Observable.sample.spec.ts | 67 + .../test/rxjs/rxjs.Observable.take.spec.ts | 115 + .../test/rxjs/rxjs.Observable.timeout.spec.ts | 59 + .../test/rxjs/rxjs.Observable.window.spec.ts | 135 + packages/zone.js/test/rxjs/rxjs.asap.spec.ts | 59 + .../test/rxjs/rxjs.bindCallback.spec.ts | 86 + .../test/rxjs/rxjs.bindNodeCallback.spec.ts | 108 + .../test/rxjs/rxjs.combineLatest.spec.ts | 86 + .../zone.js/test/rxjs/rxjs.common.spec.ts | 209 ++ .../zone.js/test/rxjs/rxjs.concat.spec.ts | 86 + packages/zone.js/test/rxjs/rxjs.defer.spec.ts | 44 + packages/zone.js/test/rxjs/rxjs.empty.spec.ts | 28 + .../zone.js/test/rxjs/rxjs.forkjoin.spec.ts | 61 + packages/zone.js/test/rxjs/rxjs.from.spec.ts | 81 + .../zone.js/test/rxjs/rxjs.fromEvent.spec.ts | 95 + .../test/rxjs/rxjs.fromPromise.spec.ts | 41 + .../zone.js/test/rxjs/rxjs.interval.spec.ts | 37 + packages/zone.js/test/rxjs/rxjs.merge.spec.ts | 47 + packages/zone.js/test/rxjs/rxjs.never.spec.ts | 33 + packages/zone.js/test/rxjs/rxjs.of.spec.ts | 36 + packages/zone.js/test/rxjs/rxjs.range.spec.ts | 61 + packages/zone.js/test/rxjs/rxjs.spec.ts | 54 + packages/zone.js/test/rxjs/rxjs.throw.spec.ts | 57 + packages/zone.js/test/rxjs/rxjs.timer.spec.ts | 40 + packages/zone.js/test/rxjs/rxjs.util.ts | 12 + packages/zone.js/test/rxjs/rxjs.zip.spec.ts | 44 + packages/zone.js/test/saucelabs.js | 9 + .../test-env-setup-jasmine-no-patch-clock.ts | 8 + .../zone.js/test/test-env-setup-jasmine.ts | 9 + packages/zone.js/test/test-env-setup-mocha.ts | 186 ++ packages/zone.js/test/test-util.ts | 142 + packages/zone.js/test/test_fake_polyfill.ts | 82 + .../zone.js/test/webdriver/test-es2015.html | 9 + packages/zone.js/test/webdriver/test.html | 10 + packages/zone.js/test/webdriver/test.js | 30 + .../test/webdriver/test.sauce.es2015.js | 101 + packages/zone.js/test/webdriver/test.sauce.js | 112 + packages/zone.js/test/ws-client.js | 14 + packages/zone.js/test/ws-server.js | 20 + packages/zone.js/test/ws-webworker-context.ts | 13 + packages/zone.js/test/wtf_mock.ts | 89 + .../zone.js/test/zone-spec/async-test.spec.ts | 413 +++ .../test/zone-spec/fake-async-test.spec.ts | 1473 ++++++++++ .../zone-spec/long-stack-trace-zone.spec.ts | 185 ++ packages/zone.js/test/zone-spec/proxy.spec.ts | 179 ++ .../zone.js/test/zone-spec/sync-test.spec.ts | 57 + .../test/zone-spec/task-tracking.spec.ts | 76 + .../zone.js/test/zone_worker_entry_point.ts | 31 + packages/zone.js/tsconfig.json | 40 + tools/gulp-tasks/lint.js | 5 + yarn.lock | 41 +- 271 files changed, 30890 insertions(+), 19 deletions(-) create mode 100644 packages/zone.js/BUILD.bazel create mode 100644 packages/zone.js/CHANGELOG.md create mode 100644 packages/zone.js/DEVELOPER.md create mode 100644 packages/zone.js/MODULE.md create mode 100644 packages/zone.js/NON-STANDARD-APIS.md create mode 100644 packages/zone.js/README.md create mode 100644 packages/zone.js/SAMPLE.md create mode 100644 packages/zone.js/STANDARD-APIS.md create mode 100644 packages/zone.js/bundles.bzl create mode 100644 packages/zone.js/check-file-size.js create mode 100644 packages/zone.js/dist/BUILD.bazel create mode 100644 packages/zone.js/doc/error.png create mode 100644 packages/zone.js/doc/error.puml create mode 100644 packages/zone.js/doc/eventtask.png create mode 100644 packages/zone.js/doc/eventtask.puml create mode 100644 packages/zone.js/doc/microtask.png create mode 100644 packages/zone.js/doc/microtask.puml create mode 100644 packages/zone.js/doc/non-periodical-macrotask.png create mode 100644 packages/zone.js/doc/non-periodical-macrotask.puml create mode 100644 packages/zone.js/doc/override-task.png create mode 100644 packages/zone.js/doc/override-task.puml create mode 100644 packages/zone.js/doc/periodical-macrotask.png create mode 100644 packages/zone.js/doc/periodical-macrotask.puml create mode 100644 packages/zone.js/doc/reschedule-task.png create mode 100644 packages/zone.js/doc/reschedule-task.puml create mode 100644 packages/zone.js/doc/task.md create mode 100644 packages/zone.js/example/basic.html create mode 100644 packages/zone.js/example/benchmarks/addEventListener.html create mode 100644 packages/zone.js/example/benchmarks/event_emitter.js create mode 100644 packages/zone.js/example/counting.html create mode 100644 packages/zone.js/example/css/style.css create mode 100644 packages/zone.js/example/index.html create mode 100644 packages/zone.js/example/js/counting-zone.js create mode 100644 packages/zone.js/example/profiling.html create mode 100644 packages/zone.js/example/throttle.html create mode 100644 packages/zone.js/example/web-socket.html create mode 100644 packages/zone.js/file-size-limit.json create mode 100644 packages/zone.js/karma-base.conf.js create mode 100644 packages/zone.js/karma-build-jasmine-phantomjs.conf.js create mode 100644 packages/zone.js/karma-build-jasmine.conf.js create mode 100644 packages/zone.js/karma-build-jasmine.es2015.conf.js create mode 100644 packages/zone.js/karma-build-mocha.conf.js create mode 100644 packages/zone.js/karma-build-sauce-mocha.conf.js create mode 100644 packages/zone.js/karma-build-sauce-selenium3-mocha.conf.js create mode 100644 packages/zone.js/karma-build.conf.js create mode 100644 packages/zone.js/karma-dist-jasmine.conf.js create mode 100644 packages/zone.js/karma-dist-mocha.conf.js create mode 100644 packages/zone.js/karma-dist-sauce-jasmine.conf.js create mode 100644 packages/zone.js/karma-dist-sauce-jasmine.es2015.conf.js create mode 100644 packages/zone.js/karma-dist-sauce-jasmine3.conf.js create mode 100644 packages/zone.js/karma-dist-sauce-selenium3-jasmine.conf.js create mode 100644 packages/zone.js/karma-dist.conf.js create mode 100644 packages/zone.js/karma-evergreen-dist-jasmine.conf.js create mode 100644 packages/zone.js/karma-evergreen-dist-sauce-jasmine.conf.js create mode 100644 packages/zone.js/karma-evergreen-dist.conf.js create mode 100644 packages/zone.js/lib/BUILD.bazel create mode 100644 packages/zone.js/lib/browser/api-util.ts create mode 100644 packages/zone.js/lib/browser/browser-legacy.ts create mode 100644 packages/zone.js/lib/browser/browser-util.ts create mode 100644 packages/zone.js/lib/browser/browser.ts create mode 100644 packages/zone.js/lib/browser/canvas.ts create mode 100644 packages/zone.js/lib/browser/custom-elements.ts create mode 100644 packages/zone.js/lib/browser/define-property.ts create mode 100644 packages/zone.js/lib/browser/event-target-legacy.ts create mode 100644 packages/zone.js/lib/browser/event-target.ts create mode 100644 packages/zone.js/lib/browser/property-descriptor-legacy.ts create mode 100644 packages/zone.js/lib/browser/property-descriptor.ts create mode 100644 packages/zone.js/lib/browser/register-element.ts create mode 100644 packages/zone.js/lib/browser/rollup-common.ts create mode 100644 packages/zone.js/lib/browser/rollup-legacy-main.ts create mode 100644 packages/zone.js/lib/browser/rollup-legacy-test-main.ts create mode 100644 packages/zone.js/lib/browser/rollup-main.ts create mode 100644 packages/zone.js/lib/browser/rollup-test-main.ts create mode 100644 packages/zone.js/lib/browser/shadydom.ts create mode 100644 packages/zone.js/lib/browser/webapis-media-query.ts create mode 100644 packages/zone.js/lib/browser/webapis-notification.ts create mode 100644 packages/zone.js/lib/browser/webapis-resize-observer.ts create mode 100644 packages/zone.js/lib/browser/webapis-rtc-peer-connection.ts create mode 100644 packages/zone.js/lib/browser/webapis-user-media.ts create mode 100644 packages/zone.js/lib/browser/websocket.ts create mode 100644 packages/zone.js/lib/closure/zone_externs.js create mode 100644 packages/zone.js/lib/common/error-rewrite.ts create mode 100644 packages/zone.js/lib/common/events.ts create mode 100644 packages/zone.js/lib/common/fetch.ts create mode 100644 packages/zone.js/lib/common/promise.ts create mode 100644 packages/zone.js/lib/common/timers.ts create mode 100644 packages/zone.js/lib/common/to-string.ts create mode 100644 packages/zone.js/lib/common/utils.ts create mode 100644 packages/zone.js/lib/extra/bluebird.ts create mode 100644 packages/zone.js/lib/extra/cordova.ts create mode 100644 packages/zone.js/lib/extra/electron.ts create mode 100644 packages/zone.js/lib/extra/jsonp.ts create mode 100644 packages/zone.js/lib/extra/socket-io.ts create mode 100644 packages/zone.js/lib/jasmine/jasmine.ts create mode 100644 packages/zone.js/lib/mix/rollup-mix.ts create mode 100644 packages/zone.js/lib/mocha/mocha.ts create mode 100644 packages/zone.js/lib/node/events.ts create mode 100644 packages/zone.js/lib/node/fs.ts create mode 100644 packages/zone.js/lib/node/node.ts create mode 100644 packages/zone.js/lib/node/node_util.ts create mode 100644 packages/zone.js/lib/node/rollup-main.ts create mode 100644 packages/zone.js/lib/node/rollup-test-main.ts create mode 100644 packages/zone.js/lib/rxjs/rxjs-fake-async.ts create mode 100644 packages/zone.js/lib/rxjs/rxjs.ts create mode 100644 packages/zone.js/lib/testing/async-testing.ts create mode 100644 packages/zone.js/lib/testing/fake-async.ts create mode 100644 packages/zone.js/lib/testing/promise-testing.ts create mode 100644 packages/zone.js/lib/testing/zone-testing.ts create mode 100644 packages/zone.js/lib/zone-spec/async-test.ts create mode 100644 packages/zone.js/lib/zone-spec/fake-async-test.ts create mode 100644 packages/zone.js/lib/zone-spec/long-stack-trace.ts create mode 100644 packages/zone.js/lib/zone-spec/proxy.ts create mode 100644 packages/zone.js/lib/zone-spec/sync-test.ts create mode 100644 packages/zone.js/lib/zone-spec/task-tracking.ts create mode 100644 packages/zone.js/lib/zone-spec/wtf.ts create mode 100644 packages/zone.js/lib/zone.ts create mode 100644 packages/zone.js/package.json create mode 100644 packages/zone.js/presentation.png create mode 100644 packages/zone.js/promise-adapter.js create mode 100644 packages/zone.js/promise-test.js create mode 100644 packages/zone.js/promise.finally.spec.js create mode 100644 packages/zone.js/sauce-evergreen.conf.js create mode 100644 packages/zone.js/sauce-selenium3.conf.js create mode 100644 packages/zone.js/sauce.conf.js create mode 100644 packages/zone.js/sauce.es2015.conf.js create mode 100755 packages/zone.js/scripts/closure/closure_compiler.sh create mode 100644 packages/zone.js/scripts/closure/closure_flagfile create mode 100755 packages/zone.js/scripts/grab-blink-idl.sh create mode 100755 packages/zone.js/scripts/sauce/sauce_connect_block.sh create mode 100755 packages/zone.js/scripts/sauce/sauce_connect_setup.sh create mode 100644 packages/zone.js/simple-server.js create mode 100644 packages/zone.js/test/BUILD.bazel create mode 100644 packages/zone.js/test/assets/import.html create mode 100644 packages/zone.js/test/assets/sample.json create mode 100644 packages/zone.js/test/assets/worker.js create mode 100644 packages/zone.js/test/browser-env-setup.ts create mode 100644 packages/zone.js/test/browser-zone-setup.ts create mode 100644 packages/zone.js/test/browser/FileReader.spec.ts create mode 100644 packages/zone.js/test/browser/HTMLImports.spec.ts create mode 100644 packages/zone.js/test/browser/MediaQuery.spec.ts create mode 100644 packages/zone.js/test/browser/MutationObserver.spec.ts create mode 100644 packages/zone.js/test/browser/Notification.spec.ts create mode 100644 packages/zone.js/test/browser/WebSocket.spec.ts create mode 100644 packages/zone.js/test/browser/Worker.spec.ts create mode 100644 packages/zone.js/test/browser/XMLHttpRequest.spec.ts create mode 100644 packages/zone.js/test/browser/browser.spec.ts create mode 100644 packages/zone.js/test/browser/custom-element.spec.js create mode 100644 packages/zone.js/test/browser/define-property.spec.ts create mode 100644 packages/zone.js/test/browser/element.spec.ts create mode 100644 packages/zone.js/test/browser/geolocation.spec.manual.ts create mode 100644 packages/zone.js/test/browser/registerElement.spec.ts create mode 100644 packages/zone.js/test/browser/requestAnimationFrame.spec.ts create mode 100644 packages/zone.js/test/browser_entry_point.ts create mode 100644 packages/zone.js/test/browser_es2015_entry_point.ts create mode 100644 packages/zone.js/test/browser_symbol_setup.ts create mode 100644 packages/zone.js/test/closure/zone.closure.ts create mode 100644 packages/zone.js/test/common/Error.spec.ts create mode 100644 packages/zone.js/test/common/Promise.spec.ts create mode 100644 packages/zone.js/test/common/fetch.spec.ts create mode 100644 packages/zone.js/test/common/microtasks.spec.ts create mode 100644 packages/zone.js/test/common/setInterval.spec.ts create mode 100644 packages/zone.js/test/common/setTimeout.spec.ts create mode 100644 packages/zone.js/test/common/task.spec.ts create mode 100644 packages/zone.js/test/common/toString.spec.ts create mode 100644 packages/zone.js/test/common/util.spec.ts create mode 100644 packages/zone.js/test/common/zone.spec.ts create mode 100644 packages/zone.js/test/common_tests.ts create mode 100644 packages/zone.js/test/extra/bluebird.spec.ts create mode 100644 packages/zone.js/test/extra/cordova.spec.ts create mode 100644 packages/zone.js/test/fake_entry.js create mode 100644 packages/zone.js/test/jasmine-patch.spec.ts create mode 100644 packages/zone.js/test/main.ts create mode 100644 packages/zone.js/test/mocha-patch.spec.ts create mode 100644 packages/zone.js/test/node-env-setup.ts create mode 100644 packages/zone.js/test/node/Error.spec.ts create mode 100644 packages/zone.js/test/node/console.spec.ts create mode 100644 packages/zone.js/test/node/crypto.spec.ts create mode 100644 packages/zone.js/test/node/events.spec.ts create mode 100644 packages/zone.js/test/node/fs.spec.ts create mode 100644 packages/zone.js/test/node/http.spec.ts create mode 100644 packages/zone.js/test/node/process.spec.ts create mode 100644 packages/zone.js/test/node/timer.spec.ts create mode 100644 packages/zone.js/test/node_bluebird_entry_point.ts create mode 100644 packages/zone.js/test/node_entry_point.ts create mode 100644 packages/zone.js/test/node_entry_point_no_patch_clock.ts create mode 100644 packages/zone.js/test/node_error_disable_policy_entry_point.ts create mode 100644 packages/zone.js/test/node_error_entry_point.ts create mode 100644 packages/zone.js/test/node_error_lazy_policy_entry_point.ts create mode 100644 packages/zone.js/test/node_tests.ts create mode 100644 packages/zone.js/test/npm_package/npm_package.spec.ts create mode 100644 packages/zone.js/test/patch/IndexedDB.spec.js create mode 100644 packages/zone.js/test/performance/eventTarget.js create mode 100644 packages/zone.js/test/performance/performance.html create mode 100644 packages/zone.js/test/performance/performance_setup.js create mode 100644 packages/zone.js/test/performance/performance_ui.js create mode 100644 packages/zone.js/test/performance/promise.js create mode 100644 packages/zone.js/test/performance/requestAnimationFrame.js create mode 100644 packages/zone.js/test/performance/timeout.js create mode 100644 packages/zone.js/test/performance/xhr.js create mode 100644 packages/zone.js/test/rxjs/rxjs.Observable.audit.spec.ts create mode 100644 packages/zone.js/test/rxjs/rxjs.Observable.buffer.spec.ts create mode 100644 packages/zone.js/test/rxjs/rxjs.Observable.catch.spec.ts create mode 100644 packages/zone.js/test/rxjs/rxjs.Observable.collection.spec.ts create mode 100644 packages/zone.js/test/rxjs/rxjs.Observable.combine.spec.ts create mode 100644 packages/zone.js/test/rxjs/rxjs.Observable.concat.spec.ts create mode 100644 packages/zone.js/test/rxjs/rxjs.Observable.count.spec.ts create mode 100644 packages/zone.js/test/rxjs/rxjs.Observable.debounce.spec.ts create mode 100644 packages/zone.js/test/rxjs/rxjs.Observable.default.spec.ts create mode 100644 packages/zone.js/test/rxjs/rxjs.Observable.delay.spec.ts create mode 100644 packages/zone.js/test/rxjs/rxjs.Observable.distinct.spec.ts create mode 100644 packages/zone.js/test/rxjs/rxjs.Observable.do.spec.ts create mode 100644 packages/zone.js/test/rxjs/rxjs.Observable.map.spec.ts create mode 100644 packages/zone.js/test/rxjs/rxjs.Observable.merge.spec.ts create mode 100644 packages/zone.js/test/rxjs/rxjs.Observable.multicast.spec.ts create mode 100644 packages/zone.js/test/rxjs/rxjs.Observable.notification.spec.ts create mode 100644 packages/zone.js/test/rxjs/rxjs.Observable.race.spec.ts create mode 100644 packages/zone.js/test/rxjs/rxjs.Observable.sample.spec.ts create mode 100644 packages/zone.js/test/rxjs/rxjs.Observable.take.spec.ts create mode 100644 packages/zone.js/test/rxjs/rxjs.Observable.timeout.spec.ts create mode 100644 packages/zone.js/test/rxjs/rxjs.Observable.window.spec.ts create mode 100644 packages/zone.js/test/rxjs/rxjs.asap.spec.ts create mode 100644 packages/zone.js/test/rxjs/rxjs.bindCallback.spec.ts create mode 100644 packages/zone.js/test/rxjs/rxjs.bindNodeCallback.spec.ts create mode 100644 packages/zone.js/test/rxjs/rxjs.combineLatest.spec.ts create mode 100644 packages/zone.js/test/rxjs/rxjs.common.spec.ts create mode 100644 packages/zone.js/test/rxjs/rxjs.concat.spec.ts create mode 100644 packages/zone.js/test/rxjs/rxjs.defer.spec.ts create mode 100644 packages/zone.js/test/rxjs/rxjs.empty.spec.ts create mode 100644 packages/zone.js/test/rxjs/rxjs.forkjoin.spec.ts create mode 100644 packages/zone.js/test/rxjs/rxjs.from.spec.ts create mode 100644 packages/zone.js/test/rxjs/rxjs.fromEvent.spec.ts create mode 100644 packages/zone.js/test/rxjs/rxjs.fromPromise.spec.ts create mode 100644 packages/zone.js/test/rxjs/rxjs.interval.spec.ts create mode 100644 packages/zone.js/test/rxjs/rxjs.merge.spec.ts create mode 100644 packages/zone.js/test/rxjs/rxjs.never.spec.ts create mode 100644 packages/zone.js/test/rxjs/rxjs.of.spec.ts create mode 100644 packages/zone.js/test/rxjs/rxjs.range.spec.ts create mode 100644 packages/zone.js/test/rxjs/rxjs.spec.ts create mode 100644 packages/zone.js/test/rxjs/rxjs.throw.spec.ts create mode 100644 packages/zone.js/test/rxjs/rxjs.timer.spec.ts create mode 100644 packages/zone.js/test/rxjs/rxjs.util.ts create mode 100644 packages/zone.js/test/rxjs/rxjs.zip.spec.ts create mode 100644 packages/zone.js/test/saucelabs.js create mode 100644 packages/zone.js/test/test-env-setup-jasmine-no-patch-clock.ts create mode 100644 packages/zone.js/test/test-env-setup-jasmine.ts create mode 100644 packages/zone.js/test/test-env-setup-mocha.ts create mode 100644 packages/zone.js/test/test-util.ts create mode 100644 packages/zone.js/test/test_fake_polyfill.ts create mode 100644 packages/zone.js/test/webdriver/test-es2015.html create mode 100644 packages/zone.js/test/webdriver/test.html create mode 100644 packages/zone.js/test/webdriver/test.js create mode 100644 packages/zone.js/test/webdriver/test.sauce.es2015.js create mode 100644 packages/zone.js/test/webdriver/test.sauce.js create mode 100644 packages/zone.js/test/ws-client.js create mode 100644 packages/zone.js/test/ws-server.js create mode 100644 packages/zone.js/test/ws-webworker-context.ts create mode 100644 packages/zone.js/test/wtf_mock.ts create mode 100644 packages/zone.js/test/zone-spec/async-test.spec.ts create mode 100644 packages/zone.js/test/zone-spec/fake-async-test.spec.ts create mode 100644 packages/zone.js/test/zone-spec/long-stack-trace-zone.spec.ts create mode 100644 packages/zone.js/test/zone-spec/proxy.spec.ts create mode 100644 packages/zone.js/test/zone-spec/sync-test.spec.ts create mode 100644 packages/zone.js/test/zone-spec/task-tracking.spec.ts create mode 100644 packages/zone.js/test/zone_worker_entry_point.ts create mode 100644 packages/zone.js/tsconfig.json diff --git a/.circleci/config.yml b/.circleci/config.yml index dd1571d2b4..0384f93ccd 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -641,6 +641,17 @@ jobs: name: "Running Material unit tests" command: ./scripts/ci/run_angular_material_unit_tests.sh + test_zonejs: + <<: *job_defaults + steps: + - *attach_workspace + - *init_environment + # Install + - run: yarn --cwd packages/zone.js install --frozen-lockfile --non-interactive + # Run zone.js tools tests + - run: yarn --cwd packages/zone.js promisetest + - run: yarn --cwd packages/zone.js promisefinallytest + workflows: version: 2 default_workflow: @@ -725,6 +736,9 @@ workflows: - material-unit-tests: requires: - build-ivy-npm-packages + - test_zonejs: + requires: + - setup saucelabs_tests: jobs: diff --git a/BUILD.bazel b/BUILD.bazel index 58ce9abd32..d383ef4f3e 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -74,6 +74,7 @@ karma_web_test( "//packages/core/test:test_lib", "//packages/forms/test:test_lib", "//packages/http/test:test_lib", + "//packages/zone.js/test:karma_jasmine_test_ci", # "//packages/router/test:test_lib", # //packages/router/test:test_lib fails with: # IE 11.0.0 (Windows 8.1.0.0) bootstrap should restore the scrolling position FAILED diff --git a/package.json b/package.json index 44ed90ae7a..e197d602ee 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@schematics/angular": "^8.0.0-beta.15", "@types/angular": "^1.6.47", "@types/base64-js": "1.2.5", + "@types/bluebird": "^3.5.27", "@types/chai": "^4.1.2", "@types/chokidar": "^1.7.5", "@types/convert-source-map": "^1.5.1", @@ -57,6 +58,7 @@ "@types/selenium-webdriver": "3.0.7", "@types/shelljs": "^0.7.8", "@types/source-map": "^0.5.1", + "@types/systemjs": "0.19.32", "@types/yargs": "^11.1.1", "@webcomponents/custom-elements": "^1.0.4", "angular": "npm:angular@1.7", @@ -66,12 +68,14 @@ "angular-mocks-1.5": "npm:angular-mocks@1.5", "angular-mocks-1.6": "npm:angular-mocks@1.6", "base64-js": "1.2.1", + "bluebird": "^3.5.5", "brotli": "^1.3.2", "canonical-path": "1.0.0", "chai": "^4.1.2", "chalk": "^2.3.1", "chokidar": "^2.1.1", "convert-source-map": "^1.5.1", + "core-js": "^2.4.1", "dependency-graph": "^0.7.2", "diff": "^3.5.0", "domino": "2.1.2", @@ -88,6 +92,7 @@ "minimist": "1.2.0", "mock-fs": "^4.10.1", "node-uuid": "1.4.8", + "nodejs-websocket": "^1.7.2", "protractor": "^5.4.2", "reflect-metadata": "^0.1.3", "rollup": "^1.1.0", @@ -121,7 +126,6 @@ "@bazel/buildifier": "^0.25.1", "@bazel/ibazel": "~0.9.0", "@types/minimist": "^1.2.0", - "@types/systemjs": "0.19.32", "browserstacktunnel-wrapper": "2.0.1", "check-side-effects": "0.0.21", "clang-format": "1.0.41", @@ -129,14 +133,13 @@ "cldr-data-downloader": "0.3.2", "cldrjs": "0.5.0", "conventional-changelog": "^2.0.3", - "core-js": "^2.4.1", "cors": "2.8.4", "entities": "1.1.1", "firebase-tools": "5.1.1", "firefox-profile": "1.0.3", "glob": "7.1.2", "gulp": "3.9.1", - "gulp-clang-format": "1.0.23", + "gulp-clang-format": "1.0.27", "gulp-connect": "5.0.0", "gulp-conventional-changelog": "^2.0.3", "gulp-filter": "^5.1.0", diff --git a/packages/tsconfig.json b/packages/tsconfig.json index f951fee5d1..367aa9f7e9 100644 --- a/packages/tsconfig.json +++ b/packages/tsconfig.json @@ -42,5 +42,6 @@ "examples/**/main.ts", "platform-server/integrationtest", "router/test/aot_ngsummary_test", + "zone.js" ] } diff --git a/packages/zone.js/BUILD.bazel b/packages/zone.js/BUILD.bazel new file mode 100644 index 0000000000..ba916f90c6 --- /dev/null +++ b/packages/zone.js/BUILD.bazel @@ -0,0 +1,47 @@ +load("@build_bazel_rules_nodejs//:defs.bzl", "npm_package", "rollup_bundle") +load("@npm_bazel_jasmine//:index.bzl", "jasmine_node_test") +load("@npm_bazel_typescript//:defs.bzl", "ts_library") +load("//packages/zone.js:bundles.bzl", "ES2015_BUNDLES", "ES5_BUNDLES", "ES5_GLOBAL_BUNDLES") + +exports_files([ + "tsconfig.json", +]) + +genrule( + name = "LICENSE_copy", + srcs = ["//:LICENSE"], + outs = ["LICENSE"], + cmd = "cp $< $@", +) + +genrule( + name = "LICENSE_wrapped", + srcs = ["//:LICENSE"], + outs = ["LICENSE.wrapped"], + cmd = "(echo '/**\n @license' && cat $< && echo '*/') > $@", +) + +npm_package( + name = "npm_package", + srcs = [ + "CHANGELOG.md", + "README.md", + "package.json", + ], + visibility = ["//packages/zone.js/test:__pkg__"], + deps = [ + ":LICENSE.wrapped", + ":LICENSE_copy", + "//packages/zone.js/dist:zone_externs", + "//packages/zone.js/lib", + ] + [ + "//packages/zone.js/dist:" + b + "-dist" + for b in ES5_BUNDLES + ] + [ + "//packages/zone.js/dist:" + b + "-dist" + for b in ES2015_BUNDLES + ] + [ + "//packages/zone.js/dist:" + b + "-dist" + for b in ES5_GLOBAL_BUNDLES + ] + ["//packages/zone.js/dist:zone_d_ts"], +) diff --git a/packages/zone.js/CHANGELOG.md b/packages/zone.js/CHANGELOG.md new file mode 100644 index 0000000000..872b2c9eff --- /dev/null +++ b/packages/zone.js/CHANGELOG.md @@ -0,0 +1,1322 @@ + +## [0.9.1](https://github.com/angular/zone.js/compare/v0.9.0...0.9.1) (2019-04-30) + + +### Bug Fixes + +* ensure that EventTarget is patched prior to legacy property descriptor patch ([#1214](https://github.com/angular/zone.js/issues/1214)) ([aca4728](https://github.com/angular/zone.js/commit/aca4728)) +* fakeAsyncTest requestAnimationFrame should pass timestamp as parameter ([#1220](https://github.com/angular/zone.js/issues/1220)) ([62b8525](https://github.com/angular/zone.js/commit/62b8525)), closes [#1216](https://github.com/angular/zone.js/issues/1216) + + +### Features + +* add option to disable jasmine clock patch, also rename the flag of auto jump in FakeAsyncTest ([#1222](https://github.com/angular/zone.js/issues/1222)) ([10e1b0c](https://github.com/angular/zone.js/commit/10e1b0c)) + + + + +# [0.9.0](https://github.com/angular/zone.js/compare/v0.8.29...0.9.0) (2019-03-12) + + +### Bug Fixes + +* **lint:** fix [#1168](https://github.com/angular/zone.js/issues/1168), remove unused = null code ([#1171](https://github.com/angular/zone.js/issues/1171)) ([917e2af](https://github.com/angular/zone.js/commit/917e2af)) +* **test:** fix [#1155](https://github.com/angular/zone.js/issues/1155), try/catch modify error.message ([#1157](https://github.com/angular/zone.js/issues/1157)) ([7e983d1](https://github.com/angular/zone.js/commit/7e983d1)) +* **test:** fix: make fakeAsync test spec timer id global ([d32e79b](https://github.com/angular/zone.js/commit/d32e79b)) +* **build:** fix: closure related fixes ([2a8415d](https://github.com/angular/zone.js/commit/2a8415d)) +* **compile:** fix: remove finally definition from Promise interface ([47dd3f4](https://github.com/angular/zone.js/commit/47dd3f4)) + +### Doc + +* **doc:** [#1181](https://github.com/angular/zone.js/pull/1181), Fix the typo in timer module documentation ([8f78b55](https://github.com/angular/zone.js/commit/8f78b55)) +* **doc:** [#1163](https://github.com/angular/zone.js/pull/1163), Update YouTube video link ([f171821](https://github.com/angular/zone.js/commit/f171821)) +* **doc:** [#1151](https://github.com/angular/zone.js/pull/1151), Re-phrase the lines for better understanding ([2a6444b](https://github.com/angular/zone.js/commit/2a6444b)) +* **doc:** [#1152](https://github.com/angular/zone.js/pull/1152), change the word TimerTask to MacroTask ([f3995de](https://github.com/angular/zone.js/commit/f3995de)) + + +### Features + +* **test:** add benchmark page ([#1076](https://github.com/angular/zone.js/issues/1076)) ([128649a](https://github.com/angular/zone.js/commit/128649a)) +* **test:** test(promise): add test cases for Promise.all with sync then operation ([#1158](https://github.com/angular/zone.js/issues/1158)) ([0b44e83](https://github.com/angular/zone.js/commit/0b44e83)) +* **test:** feat: add an option __zone_symbol__disableDatePatching to allow disabling Date patching ([c378f87](https://github.com/angular/zone.js/commit/c378f87)) + +### Env + +* **env:** change BLACK_LISTED_EVENTS to DISABLE_EVENTS ([9c65d25](https://github.com/angular/zone.js/commit/9c65d25)) + +### Build + +* **build:** build zone-evergreen.js in es2015, add terser minify support ([2ad936b](https://github.com/angular/zone.js/commit/2ad936b)) +* **build:** upgrade to pass jasmine 3.3 test ([82dfd75](https://github.com/angular/zone.js/commit/82dfd75)) +* **build:** upgrade to typescript 3.2.2 ([fcdd559](https://github.com/angular/zone.js/commit/fcdd559)) +* **build:** separate zone.js into evergreen only and legacy included bundles ([ac3851e](https://github.com/angular/zone.js/commit/ac3851e)) +* **build:** make legacy standalone bundle ([a5fe09b](https://github.com/angular/zone.js/commit/a5fe09b)) + + +## [0.8.29](https://github.com/angular/zone.js/compare/v0.8.28...0.8.29) (2019-01-22) + + +### Bug Fixes + +* **core:** fix for tests in angular repo ([fd069db](https://github.com/angular/zone.js/commit/fd069db)) + + + +## [0.8.28](https://github.com/angular/zone.js/compare/v0.8.27...0.8.28) (2019-01-16) + + +### Bug Fixes + +* **jasmine:** patch jasmine beforeAll/afterAll ([9d27abc4](https://github.com/angular/zone.js/commit/9d27abc4)) + + + +## [0.8.27](https://github.com/angular/zone.js/compare/v0.8.26...0.8.27) (2019-01-08) + + +### Bug Fixes + +* **bluebird:** fix [#1112](https://github.com/angular/zone.js/issues/1112), bluebird chained callback should return a Bluebird Promise ([#1114](https://github.com/angular/zone.js/issues/1114)) ([6ba3169](https://github.com/angular/zone.js/commit/6ba3169)) +* **core:** fix [#1108](https://github.com/angular/zone.js/issues/1108), window.onerror should have (message, source, lineno, colno, error) signiture ([#1109](https://github.com/angular/zone.js/issues/1109)) ([49e0548](https://github.com/angular/zone.js/commit/49e0548)) +* **core:** fix [#1153](https://github.com/angular/zone.js/issues/1153), ZoneTask.toString should always be a string ([#1166](https://github.com/angular/zone.js/issues/1166)) ([afa1363](https://github.com/angular/zone.js/commit/afa1363)) +* **core:** fix interval will still run after cancelled error ([#1156](https://github.com/angular/zone.js/issues/1156)) ([eb72ff4](https://github.com/angular/zone.js/commit/eb72ff4)) +* **core:** use then directly when promise is not patchable ([#1079](https://github.com/angular/zone.js/issues/1079)) ([d7e0a31](https://github.com/angular/zone.js/commit/d7e0a31)) +* **duplicate:** fix [#1081](https://github.com/angular/zone.js/issues/1081), load patch should also check the duplicate flag ([#1121](https://github.com/angular/zone.js/issues/1121)) ([8ce5e33](https://github.com/angular/zone.js/commit/8ce5e33)) +* **event:** fix [#1110](https://github.com/angular/zone.js/issues/1110), nodejs EventEmitter should support Symbol eventName ([#1113](https://github.com/angular/zone.js/issues/1113)) ([96420d6](https://github.com/angular/zone.js/commit/96420d6)) +* **event:** should pass boolean to addEventListener if not support passive ([#1053](https://github.com/angular/zone.js/issues/1053)) ([e9536ec](https://github.com/angular/zone.js/commit/e9536ec)) +* **format:** update clang-format to 1.2.3 ([f238908](https://github.com/angular/zone.js/commit/f238908)) +* **memory:** Add protection against excessive on prop patching ([#1106](https://github.com/angular/zone.js/issues/1106)) ([875086f](https://github.com/angular/zone.js/commit/875086f)) +* **node:** fix [#1164](https://github.com/angular/zone.js/issues/1164), don't patch uncaughtException to prevent endless loop ([#1170](https://github.com/angular/zone.js/issues/1170)) ([33a0ad6](https://github.com/angular/zone.js/commit/33a0ad6)) +* **node:** node patched method should copy original delegate's symbol properties ([#1095](https://github.com/angular/zone.js/issues/1095)) ([0a2f6ff](https://github.com/angular/zone.js/commit/0a2f6ff)) +* **onProperty:** user quoted access for __Zone_ignore_on_properties ([#1134](https://github.com/angular/zone.js/issues/1134)) ([7201d44](https://github.com/angular/zone.js/commit/7201d44)) +* **test:** karma-dist should test bundle under dist ([#1049](https://github.com/angular/zone.js/issues/1049)) ([0720d79](https://github.com/angular/zone.js/commit/0720d79)) +* **tsc:** tsconfig.json strict:true ([915042d](https://github.com/angular/zone.js/commit/915042d)) +* **xhr:** fix [#1072](https://github.com/angular/zone.js/issues/1072), should set scheduled flag to target ([#1074](https://github.com/angular/zone.js/issues/1074)) ([34c12e5](https://github.com/angular/zone.js/commit/34c12e5)) +* **xhr:** should invoke xhr task after onload is triggered ([#1055](https://github.com/angular/zone.js/issues/1055)) ([2aab9c8](https://github.com/angular/zone.js/commit/2aab9c8)) + + +### Features + +* **build:** Upgrade to TypeScript 2.9 and rxjs6 ([#1122](https://github.com/angular/zone.js/issues/1122)) ([31fc127](https://github.com/angular/zone.js/commit/31fc127)) +* **core:** upgrade to typescript 3.0.3 ([#1132](https://github.com/angular/zone.js/issues/1132)) ([60adc9c](https://github.com/angular/zone.js/commit/60adc9c)) +* **Core:** fix [#910](https://github.com/angular/zone.js/issues/910), add a flag to allow user to ignore duplicate Zone error ([#1093](https://github.com/angular/zone.js/issues/1093)) ([a86c6d5](https://github.com/angular/zone.js/commit/a86c6d5)) +* **custom-element:** patch customElement v1 APIs ([#1133](https://github.com/angular/zone.js/issues/1133)) ([427705f](https://github.com/angular/zone.js/commit/427705f)) +* **error:** fix [#975](https://github.com/angular/zone.js/issues/975), can config how to load blacklist zone stack frames ([#1045](https://github.com/angular/zone.js/issues/1045)) ([ff3d545](https://github.com/angular/zone.js/commit/ff3d545)) +* **fetch:** schedule macroTask when fetch ([#1075](https://github.com/angular/zone.js/issues/1075)) ([bf88c34](https://github.com/angular/zone.js/commit/bf88c34)) + + + + +## [0.8.26](https://github.com/angular/zone.js/compare/v0.8.25...0.8.26) (2018-04-08) + + +### Bug Fixes + +* **test:** fix [#1069](https://github.com/angular/zone.js/issues/1069), FakeDate should handle constructor parameter ([#1070](https://github.com/angular/zone.js/issues/1070)) ([b3fdd7e](https://github.com/angular/zone.js/commit/b3fdd7e)) + + + + +## [0.8.25](https://github.com/angular/zone.js/compare/v0.8.24...0.8.25) (2018-04-04) + + +### Bug Fixes + +* **test:** add async/fakeAsync into zone-testing bundle ([#1068](https://github.com/angular/zone.js/issues/1068)) ([3bdfdad](https://github.com/angular/zone.js/commit/3bdfdad)) + + + + +## [0.8.24](https://github.com/angular/zone.js/compare/v0.8.23...0.8.24) (2018-04-02) + + +### Bug Fixes + +* **test:** add flag to patch jasmine.clock, move fakeAsync/async into original bundle ([#1067](https://github.com/angular/zone.js/issues/1067)) ([389762c](https://github.com/angular/zone.js/commit/389762c)) + + + + +## [0.8.24](https://github.com/angular/zone.js/compare/v0.8.23...0.8.24) (2018-04-02) + + +### Bug Fixes + +* **test:** add flag to patch jasmine.clock, move fakeAsync/async into original bundle ([#1067](https://github.com/angular/zone.js/issues/1067)) ([389762c](https://github.com/angular/zone.js/commit/389762c)) + + + + +## [0.8.23](https://github.com/angular/zone.js/compare/v0.8.22...0.8.23) (2018-04-01) + + +### Bug Fixes + +* **test:** check setImmediate supports ([6c7e45b](https://github.com/angular/zone.js/commit/6c7e45b)) + + + + +## [0.8.22](https://github.com/angular/zone.js/compare/v0.8.21...0.8.22) (2018-03-31) + + +### Bug Fixes + +* **fakeAsync:** fix [#1050](https://github.com/angular/zone.js/issues/1050), should only reset patched Date.now until fakeAsync exit ([#1051](https://github.com/angular/zone.js/issues/1051)) ([e15d735](https://github.com/angular/zone.js/commit/e15d735)) +* **fakeAsyncTest:** fix [#1061](https://github.com/angular/zone.js/issues/1061), fakeAsync should support setImmediate ([#1062](https://github.com/angular/zone.js/issues/1062)) ([66c6f97](https://github.com/angular/zone.js/commit/66c6f97)) + + + + +## [0.8.21](https://github.com/angular/zone.js/compare/v0.8.20...0.8.21) (2018-03-30) + + +### Bug Fixes + +* add OriginalDelegate prop to Function::toString ([#993](https://github.com/angular/zone.js/issues/993)) ([2dc7e5c](https://github.com/angular/zone.js/commit/2dc7e5c)) +* **core:** fix [#1000](https://github.com/angular/zone.js/issues/1000), check target is null or not when patchOnProperty ([#1004](https://github.com/angular/zone.js/issues/1004)) ([5c139e5](https://github.com/angular/zone.js/commit/5c139e5)) +* **core:** fix [#946](https://github.com/angular/zone.js/issues/946), don't patch promise if it is not writable ([#1041](https://github.com/angular/zone.js/issues/1041)) ([c8c5990](https://github.com/angular/zone.js/commit/c8c5990)) +* **event:** fix [#1021](https://github.com/angular/zone.js/issues/1021), removeListener/removeAllListeners should return eventEmitter ([#1022](https://github.com/angular/zone.js/issues/1022)) ([ab72df6](https://github.com/angular/zone.js/commit/ab72df6)) +* **fakeAsync:** fix [#1056](https://github.com/angular/zone.js/issues/1056), fakeAsync timerId should not be zero ([#1057](https://github.com/angular/zone.js/issues/1057)) ([68682cd](https://github.com/angular/zone.js/commit/68682cd)) +* **jasmine:** fix [#1015](https://github.com/angular/zone.js/issues/1015), make jasmine patch compatible to jasmine 3.x ([#1016](https://github.com/angular/zone.js/issues/1016)) ([e1df4bc](https://github.com/angular/zone.js/commit/e1df4bc)) +* **patch:** fix [#998](https://github.com/angular/zone.js/issues/998), patch mediaQuery for new Safari ([#1003](https://github.com/angular/zone.js/issues/1003)) ([c7c7db5](https://github.com/angular/zone.js/commit/c7c7db5)) +* **proxy:** proxyZone should call onHasTask when change delegate ([#1030](https://github.com/angular/zone.js/issues/1030)) ([40b110d](https://github.com/angular/zone.js/commit/40b110d)) +* **test:** fix mocha compatible issue ([#1028](https://github.com/angular/zone.js/issues/1028)) ([c554e9f](https://github.com/angular/zone.js/commit/c554e9f)) +* **testing:** fix [#1032](https://github.com/angular/zone.js/issues/1032), fakeAsync should pass parameters correctly ([#1033](https://github.com/angular/zone.js/issues/1033)) ([eefe983](https://github.com/angular/zone.js/commit/eefe983)) + + +### Features + +* **bluebird:** fix [#921](https://github.com/angular/zone.js/issues/921), [#977](https://github.com/angular/zone.js/issues/977), support bluebird ([#1039](https://github.com/angular/zone.js/issues/1039)) ([438210c](https://github.com/angular/zone.js/commit/438210c)) +* **build:** use yarn instead of npm ([#1025](https://github.com/angular/zone.js/issues/1025)) ([ebd348c](https://github.com/angular/zone.js/commit/ebd348c)) +* **core:** fix [#996](https://github.com/angular/zone.js/issues/996), expose UncaughtPromiseError ([#1040](https://github.com/angular/zone.js/issues/1040)) ([7f178b1](https://github.com/angular/zone.js/commit/7f178b1)) +* **jasmine:** support Date.now in fakeAsyncTest ([#1009](https://github.com/angular/zone.js/issues/1009)) ([f22065e](https://github.com/angular/zone.js/commit/f22065e)) +* **jsonp:** provide a help method to patch jsonp ([#997](https://github.com/angular/zone.js/issues/997)) ([008fd43](https://github.com/angular/zone.js/commit/008fd43)) +* **patch:** fix [#1011](https://github.com/angular/zone.js/issues/1011), patch ResizeObserver ([#1012](https://github.com/angular/zone.js/issues/1012)) ([8ee88da](https://github.com/angular/zone.js/commit/8ee88da)) +* **patch:** fix [#828](https://github.com/angular/zone.js/issues/828), patch socket.io client ([b3db9f4](https://github.com/angular/zone.js/commit/b3db9f4)) +* **promise:** support Promise.prototype.finally ([#1005](https://github.com/angular/zone.js/issues/1005)) ([6a1a830](https://github.com/angular/zone.js/commit/6a1a830)) +* **rollup:** use new rollup config to prevent warning ([#1006](https://github.com/angular/zone.js/issues/1006)) ([6b6b38a](https://github.com/angular/zone.js/commit/6b6b38a)) +* **test:** can handle non zone aware task in promise ([#1014](https://github.com/angular/zone.js/issues/1014)) ([6852f1d](https://github.com/angular/zone.js/commit/6852f1d)) +* **test:** move async/fakeAsync from angular to zone.js ([#1048](https://github.com/angular/zone.js/issues/1048)) ([a4b42cd](https://github.com/angular/zone.js/commit/a4b42cd)) +* **testing:** can display pending tasks info when test timeout in jasmine/mocha ([#1038](https://github.com/angular/zone.js/issues/1038)) ([57bc80c](https://github.com/angular/zone.js/commit/57bc80c)) + + + + +## [0.8.20](https://github.com/angular/zone.js/compare/v0.8.19...0.8.20) (2018-01-10) + + +### Bug Fixes + +* **core:** add comment for shorter var/function name ([67e8178](https://github.com/angular/zone.js/commit/67e8178)) +* **core:** add file check script in travis build ([615a6c1](https://github.com/angular/zone.js/commit/615a6c1)) +* **core:** add helper method in util.ts to shorter zone.wrap/scehduleMacroTask ([8293c37](https://github.com/angular/zone.js/commit/8293c37)) +* **core:** add rxjs test ([31832a7](https://github.com/angular/zone.js/commit/31832a7)) +* **core:** fix [#989](https://github.com/angular/zone.js/issues/989), remove unuse code, use shorter name to reduce bundle size ([73b0061](https://github.com/angular/zone.js/commit/73b0061)) +* **core:** fix shorter name closure conflict ([00a4e31](https://github.com/angular/zone.js/commit/00a4e31)) +* **core:** remove unreadable short names ([957351e](https://github.com/angular/zone.js/commit/957351e)) + + + + +## [0.8.18](https://github.com/angular/zone.js/compare/v0.8.17...0.8.18) (2017-09-27) + + +### Bug Fixes + +* **event:** EventTarget of SourceBuffer in samsung tv will have null context ([#904](https://github.com/angular/zone.js/issues/904)) ([8718e07](https://github.com/angular/zone.js/commit/8718e07)) +* **event:** fix [#883](https://github.com/angular/zone.js/issues/883), fix RTCPeerConnection Safari event not triggered issue ([#905](https://github.com/angular/zone.js/issues/905)) ([6f74efb](https://github.com/angular/zone.js/commit/6f74efb)) +* **event:** fix [#911](https://github.com/angular/zone.js/issues/911), in IE, event handler event maybe undefined ([#913](https://github.com/angular/zone.js/issues/913)) ([4ba5d97](https://github.com/angular/zone.js/commit/4ba5d97)) +* **event:** should handle event.stopImmediatePropagration ([#903](https://github.com/angular/zone.js/issues/903)) ([dcc285a](https://github.com/angular/zone.js/commit/dcc285a)) +* **patch:** patchOnProperty getter should return original listener ([#887](https://github.com/angular/zone.js/issues/887)) ([d4e5ae8](https://github.com/angular/zone.js/commit/d4e5ae8)) +* **patch:** Worker should patch onProperties ([#915](https://github.com/angular/zone.js/issues/915)) ([418a583](https://github.com/angular/zone.js/commit/418a583)) +* **promise:** can set native promise after loading zone.js ([#899](https://github.com/angular/zone.js/issues/899)) ([956c729](https://github.com/angular/zone.js/commit/956c729)) +* **timer:** fix [#314](https://github.com/angular/zone.js/issues/314), setTimeout/interval should return original timerId ([#894](https://github.com/angular/zone.js/issues/894)) ([aec4bd4](https://github.com/angular/zone.js/commit/aec4bd4)) + + +### Features + +* **compile:** fix [#892](https://github.com/angular/zone.js/issues/892), upgrade to typescript 2.3.4, support for...of when build zone-node ([#897](https://github.com/angular/zone.js/issues/897)) ([e999593](https://github.com/angular/zone.js/commit/e999593)) +* **spec:** log URL in error when attempting XHR from FakeAsyncTestZone ([#893](https://github.com/angular/zone.js/issues/893)) ([874bfdc](https://github.com/angular/zone.js/commit/874bfdc)) + + + + +## [0.8.17](https://github.com/angular/zone.js/compare/v0.8.16...0.8.17) (2017-08-23) + + +### Bug Fixes + +* readonly property should not be patched ([#860](https://github.com/angular/zone.js/issues/860)) ([7fbd655](https://github.com/angular/zone.js/commit/7fbd655)) +* suppress closure warnings/errors ([#861](https://github.com/angular/zone.js/issues/861)) ([deae751](https://github.com/angular/zone.js/commit/deae751)) +* **module:** fix [#875](https://github.com/angular/zone.js/issues/875), can disable requestAnimationFrame ([#876](https://github.com/angular/zone.js/issues/876)) ([fcf187c](https://github.com/angular/zone.js/commit/fcf187c)) +* **node:** remove reference to 'noop' ([#865](https://github.com/angular/zone.js/issues/865)) ([4032ddf](https://github.com/angular/zone.js/commit/4032ddf)) +* **patch:** fix [#869](https://github.com/angular/zone.js/issues/869), should not patch readonly method ([#871](https://github.com/angular/zone.js/issues/871)) ([31d38c1](https://github.com/angular/zone.js/commit/31d38c1)) +* **rxjs:** asap should runGuarded to let error inZone ([#884](https://github.com/angular/zone.js/issues/884)) ([ce3f12f](https://github.com/angular/zone.js/commit/ce3f12f)) +* **rxjs:** fix [#863](https://github.com/angular/zone.js/issues/863), fix asap scheduler issue, add testcases ([#848](https://github.com/angular/zone.js/issues/848)) ([cbc58c1](https://github.com/angular/zone.js/commit/cbc58c1)) +* **spec:** fix flush() behavior in handling periodic timers ([#881](https://github.com/angular/zone.js/issues/881)) ([eed776c](https://github.com/angular/zone.js/commit/eed776c)) +* **task:** fix closure compatibility issue with ZoneDelegate._updateTaskCount ([#878](https://github.com/angular/zone.js/issues/878)) ([a03b84b](https://github.com/angular/zone.js/commit/a03b84b)) + + +### Features + +* **cordova:** fix [#868](https://github.com/angular/zone.js/issues/868), patch cordova FileReader ([#879](https://github.com/angular/zone.js/issues/879)) ([b1e5970](https://github.com/angular/zone.js/commit/b1e5970)) +* **onProperty:** fix [#875](https://github.com/angular/zone.js/issues/875), can disable patch specified onProperties ([#877](https://github.com/angular/zone.js/issues/877)) ([a733688](https://github.com/angular/zone.js/commit/a733688)) +* **patch:** fix [#833](https://github.com/angular/zone.js/issues/833), add IntersectionObserver support ([#880](https://github.com/angular/zone.js/issues/880)) ([f27ff14](https://github.com/angular/zone.js/commit/f27ff14)) +* **performance:** onProperty handler use global wrapFn, other performance improve. ([#872](https://github.com/angular/zone.js/issues/872)) ([a66595a](https://github.com/angular/zone.js/commit/a66595a)) +* **performance:** reuse microTaskQueue native promise ([#874](https://github.com/angular/zone.js/issues/874)) ([7ee8bcd](https://github.com/angular/zone.js/commit/7ee8bcd)) +* **spec:** add a 'tick' callback to flush() ([#866](https://github.com/angular/zone.js/issues/866)) ([02cd40e](https://github.com/angular/zone.js/commit/02cd40e)) + + + + +## [0.8.16](https://github.com/angular/zone.js/compare/v0.8.15...0.8.16) (2017-07-27) + + +### Bug Fixes + +* **console:** console.log in nodejs should run in root Zone ([#855](https://github.com/angular/zone.js/issues/855)) ([5900d3a](https://github.com/angular/zone.js/commit/5900d3a)) +* **promise:** fix [#850](https://github.com/angular/zone.js/issues/850), check Promise.then writable ([#851](https://github.com/angular/zone.js/issues/851)) ([6e44cab](https://github.com/angular/zone.js/commit/6e44cab)) +* **spec:** do not count requestAnimationFrame as a pending timer ([#854](https://github.com/angular/zone.js/issues/854)) ([eca04b0](https://github.com/angular/zone.js/commit/eca04b0)) + + +### Features + +* **spec:** add an option to FakeAsyncTestZoneSpec to flush periodic timers ([#857](https://github.com/angular/zone.js/issues/857)) ([5c5ca1a](https://github.com/angular/zone.js/commit/5c5ca1a)) + + + + +## [0.8.15](https://github.com/angular/zone.js/compare/v0.8.13...0.8.15) (2017-07-27) + + +### Features + +* **rxjs:** fix [#830](https://github.com/angular/zone.js/issues/830), monkey patch rxjs to make rxjs run in correct zone ([#843](https://github.com/angular/zone.js/issues/843)) ([1ed83d0](https://github.com/angular/zone.js/commit/1ed83d0)) + + + + +## [0.8.14](https://github.com/angular/zone.js/compare/v0.8.13...0.8.14) (2017-07-20) + + +### Bug Fixes + +* **event:** fix [#836](https://github.com/angular/zone.js/issues/836), handle event callback call removeEventListener case ([#839](https://github.com/angular/zone.js/issues/839)) ([f301fa2](https://github.com/angular/zone.js/commit/f301fa2)) +* **event:** fix memory leak for once, add more test cases ([#841](https://github.com/angular/zone.js/issues/841)) ([2143d9c](https://github.com/angular/zone.js/commit/2143d9c)) +* **task:** fix [#832](https://github.com/angular/zone.js/issues/832), fix [#835](https://github.com/angular/zone.js/issues/835), task.data should be an object ([#834](https://github.com/angular/zone.js/issues/834)) ([3a4bfbd](https://github.com/angular/zone.js/commit/3a4bfbd)) + + +### Features + +* **rxjs:** fix [#830](https://github.com/angular/zone.js/issues/830), monkey patch rxjs to make rxjs run in correct zone ([#843](https://github.com/angular/zone.js/issues/843)) ([1ed83d0](https://github.com/angular/zone.js/commit/1ed83d0)) + + + + +## [0.8.14](https://github.com/angular/zone.js/compare/v0.8.13...0.8.14) (2017-07-18) + + +### Bug Fixes + +* **event:** fix [#836](https://github.com/angular/zone.js/issues/836), handle event callback call removeEventListener case ([#839](https://github.com/angular/zone.js/issues/839)) ([f301fa2](https://github.com/angular/zone.js/commit/f301fa2)) +* **event:** fix memory leak for once, add more test cases ([#841](https://github.com/angular/zone.js/issues/841)) ([2143d9c](https://github.com/angular/zone.js/commit/2143d9c)) +* **task:** fix [#832](https://github.com/angular/zone.js/issues/832), fix [#835](https://github.com/angular/zone.js/issues/835), task.data should be an object ([#834](https://github.com/angular/zone.js/issues/834)) ([3a4bfbd](https://github.com/angular/zone.js/commit/3a4bfbd)) + + + + +## [0.8.13](https://github.com/angular/zone.js/compare/v0.8.12...0.8.13) (2017-07-12) + + +### Bug Fixes + +* **promise:** fix [#806](https://github.com/angular/zone.js/issues/806), remove duplicate consolelog ([#807](https://github.com/angular/zone.js/issues/807)) ([f439fe2](https://github.com/angular/zone.js/commit/f439fe2)) +* **spec:** fakeAsyncTestSpec should handle requestAnimationFrame ([#805](https://github.com/angular/zone.js/issues/805)) ([8260f1d](https://github.com/angular/zone.js/commit/8260f1d)), closes [#804](https://github.com/angular/zone.js/issues/804) +* **websocket:** fix [#824](https://github.com/angular/zone.js/issues/824), patch websocket onproperties correctly in PhantomJS ([#826](https://github.com/angular/zone.js/issues/826)) ([273cb85](https://github.com/angular/zone.js/commit/273cb85)) + + +### Features + +* **FakeAsyncTestZoneSpec:** FakeAsyncTestZoneSpec.flush() passes limit along to scheduler ([#831](https://github.com/angular/zone.js/issues/831)) ([667cd6f](https://github.com/angular/zone.js/commit/667cd6f)) + + +### Performance Improvements + +* **eventListener:** fix [#798](https://github.com/angular/zone.js/issues/798), improve EventTarget.addEventListener performance ([#812](https://github.com/angular/zone.js/issues/812)) ([b3a76d3](https://github.com/angular/zone.js/commit/b3a76d3)) + + + + +## [0.8.12](https://github.com/angular/zone.js/compare/v0.8.11...0.8.12) (2017-06-07) + + +### Bug Fixes + +* **doc:** fix [#793](https://github.com/angular/zone.js/issues/793), fix confuseing bluebird patch doc ([#794](https://github.com/angular/zone.js/issues/794)) ([0c5da04](https://github.com/angular/zone.js/commit/0c5da04)) +* **patch:** fix [#791](https://github.com/angular/zone.js/issues/791), fix mediaQuery/Notification patch uses wrong global ([#792](https://github.com/angular/zone.js/issues/792)) ([67634ae](https://github.com/angular/zone.js/commit/67634ae)) +* **toString:** fix [#802](https://github.com/angular/zone.js/issues/802), fix ios 9 MutationObserver toString error ([#803](https://github.com/angular/zone.js/issues/803)) ([68aa03e](https://github.com/angular/zone.js/commit/68aa03e)) +* **xhr:** inner onreadystatechange should not triigger Zone callback ([#800](https://github.com/angular/zone.js/issues/800)) ([7bd1418](https://github.com/angular/zone.js/commit/7bd1418)) + + +### Features + +* **patch:** fix [#696](https://github.com/angular/zone.js/issues/696), patch HTMLCanvasElement.toBlob as MacroTask ([#788](https://github.com/angular/zone.js/issues/788)) ([7ca3995](https://github.com/angular/zone.js/commit/7ca3995)) +* **patch:** fix [#758](https://github.com/angular/zone.js/issues/758), patch cordova.exec success/error with zone.wrap ([#789](https://github.com/angular/zone.js/issues/789)) ([857929d](https://github.com/angular/zone.js/commit/857929d)) + + + + +## [0.8.11](https://github.com/angular/zone.js/compare/v0.8.10...0.8.11) (2017-05-19) + + +### Bug Fixes + +* **closure:** patchOnProperty with exact eventNames as possible ([#768](https://github.com/angular/zone.js/issues/768)) ([582ff7b](https://github.com/angular/zone.js/commit/582ff7b)) +* **patch:** fix [#744](https://github.com/angular/zone.js/issues/744), add namespace to load patch name ([#774](https://github.com/angular/zone.js/issues/774)) ([89f990a](https://github.com/angular/zone.js/commit/89f990a)) +* **task:** fix [#778](https://github.com/angular/zone.js/issues/778), sometimes task will run after being canceled ([#780](https://github.com/angular/zone.js/issues/780)) ([b7238c8](https://github.com/angular/zone.js/commit/b7238c8)) +* **webcomponents:** fix [#782](https://github.com/angular/zone.js/issues/782), fix conflicts with shadydom of webcomponents ([#784](https://github.com/angular/zone.js/issues/784)) ([245f8e9](https://github.com/angular/zone.js/commit/245f8e9)) +* **webpack:** access `process` through `_global` so that WebPack does not accidently browserify ([#786](https://github.com/angular/zone.js/issues/786)) ([1919b36](https://github.com/angular/zone.js/commit/1919b36)) + + + + +## [0.8.10](https://github.com/angular/zone.js/compare/v0.8.9...0.8.10) (2017-05-03) + + +### Bug Fixes + +* **showError:** fix ignoreConsoleErrorUncaughtError may change during drain microtask ([#763](https://github.com/angular/zone.js/issues/763)) ([4baeb5c](https://github.com/angular/zone.js/commit/4baeb5c)) +* **spec:** fix [#760](https://github.com/angular/zone.js/issues/760), fakeAsyncTestSpec should handle microtask with additional args ([#762](https://github.com/angular/zone.js/issues/762)) ([f8d17ac](https://github.com/angular/zone.js/commit/f8d17ac)) +* Package Error stack rewriting as a separate bundle. ([#770](https://github.com/angular/zone.js/issues/770)) ([b5e33fd](https://github.com/angular/zone.js/commit/b5e33fd)) +* **timer:** fix [#437](https://github.com/angular/zone.js/issues/437), [#744](https://github.com/angular/zone.js/issues/744), fix nativescript timer issue, fix nodejs v0.10.x timer issue ([#772](https://github.com/angular/zone.js/issues/772)) ([3218b5a](https://github.com/angular/zone.js/commit/3218b5a)) + + +### Features + +* make codebase more modular so that only parts of it can be loaded ([#748](https://github.com/angular/zone.js/issues/748)) ([e933cbd](https://github.com/angular/zone.js/commit/e933cbd)) +* **patch:** load non standard api with new load module method ([#764](https://github.com/angular/zone.js/issues/764)) ([97c03b5](https://github.com/angular/zone.js/commit/97c03b5)) + + + + +## [0.8.9](https://github.com/angular/zone.js/compare/v0.8.8...0.8.9) (2017-04-25) + + +### Bug Fixes + +* **patch:** fix [#746](https://github.com/angular/zone.js/issues/746), check desc get is null and only patch window.resize additionally ([#747](https://github.com/angular/zone.js/issues/747)) ([e598310](https://github.com/angular/zone.js/commit/e598310)) + + + + +## [0.8.8](https://github.com/angular/zone.js/compare/v0.8.7...0.8.8) (2017-04-21) + + +### Bug Fixes + +* on handling broken in v0.8.7 ([fbe7b13](https://github.com/angular/zone.js/commit/fbe7b13)) + + + + +## [0.8.7](https://github.com/angular/zone.js/compare/v0.8.5...0.8.7) (2017-04-21) + + +### Bug Fixes + +* **doc:** fix typo in document, fix a typescript warning in test ([#732](https://github.com/angular/zone.js/issues/732)) ([55cf064](https://github.com/angular/zone.js/commit/55cf064)) +* **error:** fix [#706](https://github.com/angular/zone.js/issues/706), handleError when onHasTask throw error ([#709](https://github.com/angular/zone.js/issues/709)) ([06d1ac0](https://github.com/angular/zone.js/commit/06d1ac0)) +* **error:** remove throw in Error constructor to improve performance in IE11 ([#704](https://github.com/angular/zone.js/issues/704)) ([88d1a49](https://github.com/angular/zone.js/commit/88d1a49)), closes [#698](https://github.com/angular/zone.js/issues/698) +* **listener:** fix [#616](https://github.com/angular/zone.js/issues/616), webdriver removeEventListener throw permission denied error ([#699](https://github.com/angular/zone.js/issues/699)) ([e02960d](https://github.com/angular/zone.js/commit/e02960d)) +* **patch:** fix [#707](https://github.com/angular/zone.js/issues/707), should not try to patch non configurable property ([#717](https://github.com/angular/zone.js/issues/717)) ([e422fb1](https://github.com/angular/zone.js/commit/e422fb1)) +* **patch:** fix [#708](https://github.com/angular/zone.js/issues/708), modify the canPatchDescriptor logic when browser don't provide onreadystatechange ([#711](https://github.com/angular/zone.js/issues/711)) ([7d4d07f](https://github.com/angular/zone.js/commit/7d4d07f)) +* **patch:** fix [#719](https://github.com/angular/zone.js/issues/719), window onproperty callback this is undefined ([#723](https://github.com/angular/zone.js/issues/723)) ([160531b](https://github.com/angular/zone.js/commit/160531b)) +* **task:** fix [#705](https://github.com/angular/zone.js/issues/705), don't json task.data to prevent cyclic error ([#712](https://github.com/angular/zone.js/issues/712)) ([92a39e2](https://github.com/angular/zone.js/commit/92a39e2)) +* **test:** fix [#718](https://github.com/angular/zone.js/issues/718), use async test to do unhandle promise rejection test ([#726](https://github.com/angular/zone.js/issues/726)) ([0a06874](https://github.com/angular/zone.js/commit/0a06874)) +* **test:** fix websocket test server will crash when test in chrome ([#733](https://github.com/angular/zone.js/issues/733)) ([5090cf9](https://github.com/angular/zone.js/commit/5090cf9)) +* **toString:** fix [#666](https://github.com/angular/zone.js/issues/666), Zone patched method toString should like before patched ([#686](https://github.com/angular/zone.js/issues/686)) ([0d0ee53](https://github.com/angular/zone.js/commit/0d0ee53)) +* resolve errors with closure ([#722](https://github.com/angular/zone.js/issues/722)) ([51e7ffe](https://github.com/angular/zone.js/commit/51e7ffe)) +* **typo:** fix typo, remove extra semicolons, unify api doc ([#697](https://github.com/angular/zone.js/issues/697)) ([967a991](https://github.com/angular/zone.js/commit/967a991)) + + +### Features + +* **closure:** fix [#727](https://github.com/angular/zone.js/issues/727), add zone_externs.js for closure compiler ([#731](https://github.com/angular/zone.js/issues/731)) ([b60e9e6](https://github.com/angular/zone.js/commit/b60e9e6)) +* **error:** Remove all Zone frames from stack ([#693](https://github.com/angular/zone.js/issues/693)) ([681a017](https://github.com/angular/zone.js/commit/681a017)) +* **EventListenerOptions:** fix [#737](https://github.com/angular/zone.js/issues/737), add support to EventListenerOptions ([#738](https://github.com/angular/zone.js/issues/738)) ([a89830d](https://github.com/angular/zone.js/commit/a89830d)) +* **patch:** fix [#499](https://github.com/angular/zone.js/issues/499), let promise instance toString active like native ([#734](https://github.com/angular/zone.js/issues/734)) ([2f11e67](https://github.com/angular/zone.js/commit/2f11e67)) + + + + +## [0.8.5](https://github.com/angular/zone.js/compare/v0.8.4...0.8.5) (2017-03-21) + + +### Bug Fixes + +* add support for subclassing of Errors ([81297ee](https://github.com/angular/zone.js/commit/81297ee)) +* improve long-stack-trace stack format detection ([6010557](https://github.com/angular/zone.js/commit/6010557)) +* remove left over console.log ([eeaab91](https://github.com/angular/zone.js/commit/eeaab91)) +* **event:** fix [#667](https://github.com/angular/zone.js/issues/667), eventHandler should return result ([#682](https://github.com/angular/zone.js/issues/682)) ([5c4e24d](https://github.com/angular/zone.js/commit/5c4e24d)) +* **jasmine:** modify jasmine test ifEnvSupports message ([#689](https://github.com/angular/zone.js/issues/689)) ([5635ac0](https://github.com/angular/zone.js/commit/5635ac0)) +* **REVERT:** remove zone internal stack frames in error.stack ([#632](https://github.com/angular/zone.js/issues/632)) ([#690](https://github.com/angular/zone.js/issues/690)) ([291d5a0](https://github.com/angular/zone.js/commit/291d5a0)) + + +### Features + +* **dom:** fix [#664](https://github.com/angular/zone.js/issues/664), patch window,document,SVGElement onProperties ([#687](https://github.com/angular/zone.js/issues/687)) ([61aee2e](https://github.com/angular/zone.js/commit/61aee2e)) + + + + +## [0.8.4](https://github.com/angular/zone.js/compare/v0.8.3...0.8.4) (2017-03-16) + + +### Bug Fixes + +* correct declaration which breaks closure ([0e19304](https://github.com/angular/zone.js/commit/0e19304)) +* stack rewriting now works with source maps ([bcd09a0](https://github.com/angular/zone.js/commit/bcd09a0)) + + + + +## [0.8.3](https://github.com/angular/zone.js/compare/v0.8.1...0.8.3) (2017-03-15) + + +### Bug Fixes + +* **zone:** consistent access to __symbol__ to work with closure ([f742394](https://github.com/angular/zone.js/commit/f742394)) + + + +## [0.8.2](https://github.com/angular/zone.js/compare/v0.8.1...0.8.2) (2017-03-14) + + +### Bug Fixes + +* **zone:** fix [#674](https://github.com/angular/zone.js/issues/674), handle error.stack readonly case ([#675](https://github.com/angular/zone.js/issues/675)) ([8322be8](https://github.com/angular/zone.js/commit/8322be8)) + + + + +## [0.8.1](https://github.com/angular/zone.js/compare/v0.8.0...0.8.1) (2017-03-13) + + +### Bug Fixes + +* **example:** Update counting.html ([#648](https://github.com/angular/zone.js/issues/648)) ([a63ae5f](https://github.com/angular/zone.js/commit/a63ae5f)) +* **XHR:** fix [#671](https://github.com/angular/zone.js/issues/671), patch XMLHttpRequestEventTarget prototype ([300dc36](https://github.com/angular/zone.js/commit/300dc36)) + + +### Features + +* **error:** remove zone internal stack frames in error.stack ([#632](https://github.com/angular/zone.js/issues/632)) ([76fa891](https://github.com/angular/zone.js/commit/76fa891)) +* **task:** add task lifecycle doc and testcases to explain task state transition. ([#651](https://github.com/angular/zone.js/issues/651)) ([ef39a44](https://github.com/angular/zone.js/commit/ef39a44)) + + + + +# [0.8.0](https://github.com/angular/zone.js/compare/v0.7.8...0.8.0) (2017-03-10) + + + +### Features + +* Upgrade TypeScript to v2.2.1 + + + + +## [0.7.8](https://github.com/angular/zone.js/compare/v0.7.6...0.7.8) (2017-03-10) + + +### Bug Fixes + +* **core:** remove debugger ([#639](https://github.com/angular/zone.js/issues/639)) ([0534b19](https://github.com/angular/zone.js/commit/0534b19)) +* **error:** fix [#618](https://github.com/angular/zone.js/issues/618), ZoneAwareError should copy Error's static propeties ([#647](https://github.com/angular/zone.js/issues/647)) ([2d30914](https://github.com/angular/zone.js/commit/2d30914)) +* **jasmine:** support "pending" `it` clauses with no test body ([96cb3d0](https://github.com/angular/zone.js/commit/96cb3d0)), closes [#659](https://github.com/angular/zone.js/issues/659) +* **minification:** fix [#607](https://github.com/angular/zone.js/issues/607) to change catch variable name to error/err ([#609](https://github.com/angular/zone.js/issues/609)) ([33d0d8d](https://github.com/angular/zone.js/commit/33d0d8d)) +* **node:** patch crypto as macroTask and add test cases for crypto, remove http patch ([#612](https://github.com/angular/zone.js/issues/612)) ([9e81037](https://github.com/angular/zone.js/commit/9e81037)) +* **package:** use fixed version typescript,clang-format and jasmine ([#650](https://github.com/angular/zone.js/issues/650)) ([84459f1](https://github.com/angular/zone.js/commit/84459f1)) +* **patch:** check timer patch return undefined ([#628](https://github.com/angular/zone.js/issues/628)) ([47962df](https://github.com/angular/zone.js/commit/47962df)) +* **patch:** fix [#618](https://github.com/angular/zone.js/issues/618), use zoneSymbol as property name to avoid name conflict ([#645](https://github.com/angular/zone.js/issues/645)) ([fcd8be5](https://github.com/angular/zone.js/commit/fcd8be5)) +* **task:** findEventTask should return Task array ([#633](https://github.com/angular/zone.js/issues/633)) ([14c7a6f](https://github.com/angular/zone.js/commit/14c7a6f)) +* **task:** fix [#638](https://github.com/angular/zone.js/issues/638), eventTask/Periodical task should not be reset after cancel in running state ([#642](https://github.com/angular/zone.js/issues/642)) ([eb9250d](https://github.com/angular/zone.js/commit/eb9250d)) +* **timers:** cleanup task reference when exception ([#637](https://github.com/angular/zone.js/issues/637)) ([2594940](https://github.com/angular/zone.js/commit/2594940)) +* **webapi:** refactor webapi to not import util.ts directly ([8b2543e](https://github.com/angular/zone.js/commit/8b2543e)), closes [#652](https://github.com/angular/zone.js/issues/652) +* **xhr:** fix [#657](https://github.com/angular/zone.js/issues/657), sometimes xhr will fire onreadystatechange with done twice ([#658](https://github.com/angular/zone.js/issues/658)) ([36c0899](https://github.com/angular/zone.js/commit/36c0899)) +* **zonespec:** don't throw and exception when setInterval is called within a async test zone ([#641](https://github.com/angular/zone.js/issues/641)) ([c07560f](https://github.com/angular/zone.js/commit/c07560f)) + + +### Features + +* add Zone.root api ([#601](https://github.com/angular/zone.js/issues/601)) ([9818139](https://github.com/angular/zone.js/commit/9818139)) +* allow tasks to be canceled and rescheduled on different zone in a zone delegate ([#629](https://github.com/angular/zone.js/issues/629)) ([76c6ebf](https://github.com/angular/zone.js/commit/76c6ebf)) +* make fetch() zone-aware without triggering extra requests or uncatchable errors. ([#622](https://github.com/angular/zone.js/issues/622)) ([6731ad0](https://github.com/angular/zone.js/commit/6731ad0)) +* **bluebird:** patch bluebird promise and treat it as microtask ([#655](https://github.com/angular/zone.js/issues/655)) ([e783bfa](https://github.com/angular/zone.js/commit/e783bfa)) +* **electron/nw:** fix [#533](https://github.com/angular/zone.js/issues/533), in electron/nw.js, we may need to patch both browser API and nodejs API, so we need a zone-mix.js to contains both patched API. ([6d31734](https://github.com/angular/zone.js/commit/6d31734)) +* **longStackTraceSpec:** handled promise rejection can also render longstacktrace ([#631](https://github.com/angular/zone.js/issues/631)) ([a4c6525](https://github.com/angular/zone.js/commit/a4c6525)) +* **promise:** fix [#621](https://github.com/angular/zone.js/issues/621), add unhandledRejection handler and ignore consoleError ([#627](https://github.com/angular/zone.js/issues/627)) ([f3547cc](https://github.com/angular/zone.js/commit/f3547cc)) + + + +## [0.7.6](https://github.com/angular/zone.js/compare/v0.7.4...0.7.6) (2017-01-17) + + +### Bug Fixes + +* **doc:** typo in comment and reformat README.md ([#590](https://github.com/angular/zone.js/issues/590)) ([95ad315](https://github.com/angular/zone.js/commit/95ad315)) +* **ZoneAwareError:** Error should keep prototype chain and can be called without new ([82722c3](https://github.com/angular/zone.js/commit/82722c3)), closes [#546](https://github.com/angular/zone.js/issues/546) [#554](https://github.com/angular/zone.js/issues/554) [#555](https://github.com/angular/zone.js/issues/555) +* [#536](https://github.com/angular/zone.js/issues/536), add notification api patch ([#599](https://github.com/angular/zone.js/issues/599)) ([83dfa97](https://github.com/angular/zone.js/commit/83dfa97)) +* [#593](https://github.com/angular/zone.js/issues/593), only call removeAttribute when have the method ([#594](https://github.com/angular/zone.js/issues/594)) ([1401d60](https://github.com/angular/zone.js/commit/1401d60)) +* [#595](https://github.com/angular/zone.js/issues/595), refactor ZoneAwareError property copy ([#597](https://github.com/angular/zone.js/issues/597)) ([f7330de](https://github.com/angular/zone.js/commit/f7330de)) +* [#604](https://github.com/angular/zone.js/issues/604), sometimes setInterval test spec will fail on Android 4.4 ([#605](https://github.com/angular/zone.js/issues/605)) ([e3cd1f4](https://github.com/angular/zone.js/commit/e3cd1f4)) +* add missing test MutationObserver ([5c7bc01](https://github.com/angular/zone.js/commit/5c7bc01)) +* Promise.toString() to look like native function ([f854ce0](https://github.com/angular/zone.js/commit/f854ce0)) + + + + +## [0.7.5](https://github.com/angular/zone.js/compare/v0.7.4...0.7.5) (2017-01-12) + + +### Bug Fixes + +* patch fs methods as macrotask, add test cases of fs watcher ([#572](https://github.com/angular/zone.js/issues/572)) ([e1d3240](https://github.com/angular/zone.js/commit/e1d3240)) +* fix [#577](https://github.com/angular/zone.js/issues/577), canPatchViaPropertyDescriptor test should add configurable to XMLHttpRequest.prototype ([#578](https://github.com/angular/zone.js/issues/578)) ([c297752](https://github.com/angular/zone.js/commit/c297752)) +* fix [#551](https://github.com/angular/zone.js/issues/551), add toJSON to ZoneTask to prevent cyclic error ([#576](https://github.com/angular/zone.js/issues/576)) ([03d19f9](https://github.com/angular/zone.js/commit/03d19f9)) +* fix [#574](https://github.com/angular/zone.js/issues/574), captureStackTrace will have additional stackframe from Zone will break binding.js ([#575](https://github.com/angular/zone.js/issues/575)) ([41f5306](https://github.com/angular/zone.js/commit/41f5306)) +* fix [#569](https://github.com/angular/zone.js/issues/569), request will cause updateTaskCount failed if we call abort multipletimes ([#570](https://github.com/angular/zone.js/issues/570)) ([62f1449](https://github.com/angular/zone.js/commit/62f1449)) +* add web-api.ts to patch mediaQuery ([#571](https://github.com/angular/zone.js/issues/571)) ([e92f934](https://github.com/angular/zone.js/commit/e92f934)) +* fix [#584](https://github.com/angular/zone.js/issues/584), remove android 4.1~4.3, add no-ssl options to make android 4.4 pass test ([#586](https://github.com/angular/zone.js/issues/586)) ([7cd570e](https://github.com/angular/zone.js/commit/7cd570e)) +* Fix [#532](https://github.com/angular/zone.js/issues/532), Fix [#566](https://github.com/angular/zone.js/issues/566), add tslint in ci, add tslint/format/test/karma in precommit of git ([#565](https://github.com/angular/zone.js/issues/565)) ([fb8d51c](https://github.com/angular/zone.js/commit/fb8d51c)) +* docs(zone.ts): fix typo ([#583](https://github.com/angular/zone.js/issues/583)) ([ecbef87](https://github.com/angular/zone.js/commit/ecbef87)) +* add missing test MutationObserver ([5c7bc01](https://github.com/angular/zone.js/commit/5c7bc01)) +* Promise.toString() to look like native function ([f854ce0](https://github.com/angular/zone.js/commit/f854ce0)) +* **ZoneAwareError:** Error should keep prototype chain and can be called without new ([82722c3](https://github.com/angular/zone.js/commit/82722c3)), closes [#546](https://github.com/angular/zone.js/issues/546) [#554](https://github.com/angular/zone.js/issues/554) [#555](https://github.com/angular/zone.js/issues/555) + + + + +## [0.7.4](https://github.com/angular/zone.js/compare/v0.7.1...0.7.4) (2016-12-31) + + +### Bug Fixes + +* add better Type safety ([610649b](https://github.com/angular/zone.js/commit/610649b)) +* add missing test MutationObserver ([5c7bc01](https://github.com/angular/zone.js/commit/5c7bc01)) +* correct currentZone passed into delegate methods ([dc12d8e](https://github.com/angular/zone.js/commit/dc12d8e)), closes [#587](https://github.com/angular/zone.js/issues/587) [#539](https://github.com/angular/zone.js/issues/539) +* correct zone.min.js not including zone ([384f5ec](https://github.com/angular/zone.js/commit/384f5ec)) +* Correct ZoneAwareError prototype chain ([ba7858c](https://github.com/angular/zone.js/commit/ba7858c)), closes [#546](https://github.com/angular/zone.js/issues/546) [#547](https://github.com/angular/zone.js/issues/547) +* formatting issue. ([c70e9ec](https://github.com/angular/zone.js/commit/c70e9ec)) +* inline event handler issue ([20b5a5d](https://github.com/angular/zone.js/commit/20b5a5d)), closes [#525](https://github.com/angular/zone.js/issues/525) [#540](https://github.com/angular/zone.js/issues/540) +* parameterize `wrap` method on `Zone` ([#542](https://github.com/angular/zone.js/issues/542)) ([f522e1b](https://github.com/angular/zone.js/commit/f522e1b)) +* **closure:** avoid property renaming on globals ([af14646](https://github.com/angular/zone.js/commit/af14646)) +* Prevent adding listener for xhrhttprequest multiple times ([9509747](https://github.com/angular/zone.js/commit/9509747)), closes [#529](https://github.com/angular/zone.js/issues/529) [#527](https://github.com/angular/zone.js/issues/527) [#287](https://github.com/angular/zone.js/issues/287) [#530](https://github.com/angular/zone.js/issues/530) +* Promise.toString() to look like native function ([f854ce0](https://github.com/angular/zone.js/commit/f854ce0)) +* **closure:** Fix closure error suppression comment. ([#552](https://github.com/angular/zone.js/issues/552)) ([2643783](https://github.com/angular/zone.js/commit/2643783)) +* Run tests on both the build as well as the dist folder ([#514](https://github.com/angular/zone.js/issues/514)) ([c0604f5](https://github.com/angular/zone.js/commit/c0604f5)) +* support nw.js environment ([486010b](https://github.com/angular/zone.js/commit/486010b)), closes [#524](https://github.com/angular/zone.js/issues/524) + + +### Features + +* Patch captureStackTrace/prepareStackTrace to ZoneAwareError, patch process.nextTick, fix removeAllListeners bug ([#516](https://github.com/angular/zone.js/issues/516)) ([c36c0bc](https://github.com/angular/zone.js/commit/c36c0bc)), closes [#484](https://github.com/angular/zone.js/issues/484) [#491](https://github.com/angular/zone.js/issues/491) + + + + +## [0.7.1](https://github.com/angular/zone.js/compare/v0.7.0...v0.7.1) (2016-11-22) + + +### Bug Fixes + +* missing zone from the build file ([e961833](https://github.com/angular/zone.js/commit/e961833)) + + + + +# [0.7.0](https://github.com/angular/zone.js/compare/0.6.25...v0.7.0) (2016-11-22) + + +### Bug Fixes + +* **node:** crash when calling listeners() for event with no listeners ([431f6f0](https://github.com/angular/zone.js/commit/431f6f0)) +* support clearing the timeouts with numeric IDs ([fea6d68](https://github.com/angular/zone.js/commit/fea6d68)), closes [#461](https://github.com/angular/zone.js/issues/461) +* **promise:** include stack trace in an unhandlerd promise ([#463](https://github.com/angular/zone.js/issues/463)) ([737f8d8](https://github.com/angular/zone.js/commit/737f8d8)) +* **property-descriptor:** do not use document object in Safari web worker ([51f2e1f](https://github.com/angular/zone.js/commit/51f2e1f)) +* Add WebSocket to the NO_EVENT_TARGET list to be patched as well ([#493](https://github.com/angular/zone.js/issues/493)) ([d8c15eb](https://github.com/angular/zone.js/commit/d8c15eb)) +* fix wrong usage of == caught by closure compiler ([#510](https://github.com/angular/zone.js/issues/510)) ([d7d8eb5](https://github.com/angular/zone.js/commit/d7d8eb5)) +* fluent interface for EventEmitter ([#475](https://github.com/angular/zone.js/issues/475)) ([c5130a6](https://github.com/angular/zone.js/commit/c5130a6)) +* lint errors ([ed87c26](https://github.com/angular/zone.js/commit/ed87c26)) +* make fetch promise patching safe ([16be7f9](https://github.com/angular/zone.js/commit/16be7f9)), closes [#451](https://github.com/angular/zone.js/issues/451) +* Make the check for ZoneAwarePromise more stringent ([#495](https://github.com/angular/zone.js/issues/495)) ([c69df25](https://github.com/angular/zone.js/commit/c69df25)) +* run all timers in passage of time in a single fakeAsync's tick call ([a85db4c](https://github.com/angular/zone.js/commit/a85db4c)), closes [#454](https://github.com/angular/zone.js/issues/454) +* stop using class extends as it breaks rollup ([b52cf02](https://github.com/angular/zone.js/commit/b52cf02)) +* use strict equality in scheduleQueueDrain ([#504](https://github.com/angular/zone.js/issues/504)) ([4b4249c](https://github.com/angular/zone.js/commit/4b4249c)) + + +### Features + +* add mocha support ([41a9047](https://github.com/angular/zone.js/commit/41a9047)) +* **Error:** Rewrite Error stack frames to include zone ([e1c2a02](https://github.com/angular/zone.js/commit/e1c2a02)) + + + + +## [0.6.25](https://github.com/angular/zone.js/compare/0.6.24...0.6.25) (2016-09-20) + + +### Bug Fixes + +* **zonespecs:** revert unwrapping of zonespecs which actually require global ([#460](https://github.com/angular/zone.js/issues/460)) ([28a14f8](https://github.com/angular/zone.js/commit/28a14f8)) + + + + +## [0.6.24](https://github.com/angular/zone.js/compare/v0.6.23...0.6.24) (2016-09-19) + + +### Bug Fixes + +* **bundling:** switch to using umd bundles ([#457](https://github.com/angular/zone.js/issues/457)) ([8dd06e5](https://github.com/angular/zone.js/commit/8dd06e5)), closes [#456](https://github.com/angular/zone.js/issues/456) + + + + +## [0.6.23](https://github.com/angular/zone.js/compare/v0.6.22...v0.6.23) (2016-09-14) + + +### Bug Fixes + +* **fetch:** correct chrome not able to load about://blank ([3844435](https://github.com/angular/zone.js/commit/3844435)), closes [#444](https://github.com/angular/zone.js/issues/444) + + + + +## [0.6.22](https://github.com/angular/zone.js/compare/v0.6.21...v0.6.22) (2016-09-14) + + +### Bug Fixes + +* use fetch(about://blank) to prevent exception on MS Edge ([#442](https://github.com/angular/zone.js/issues/442)) ([8b81537](https://github.com/angular/zone.js/commit/8b81537)), closes [#436](https://github.com/angular/zone.js/issues/436) [#439](https://github.com/angular/zone.js/issues/439) + + +### Features + +* **node:** patch most fs methods ([#438](https://github.com/angular/zone.js/issues/438)) ([4c8a155](https://github.com/angular/zone.js/commit/4c8a155)) +* **node:** patch outgoing http requests to capture the zone ([#430](https://github.com/angular/zone.js/issues/430)) ([100b82b](https://github.com/angular/zone.js/commit/100b82b)) + + + + +## [0.6.21](https://github.com/angular/zone.js/compare/v0.6.20...v0.6.21) (2016-09-11) + + +### Bug Fixes + +* proper detection of global in WebWorker ([0a7a155](https://github.com/angular/zone.js/commit/0a7a155)) + + + + +## [0.6.20](https://github.com/angular/zone.js/compare/v0.6.19...v0.6.20) (2016-09-10) + + + + +## [0.6.19](https://github.com/angular/zone.js/compare/v0.6.17...v0.6.19) (2016-09-10) + + +### Bug Fixes + +* provide a more usefull error when configuring properties ([1fe4df0](https://github.com/angular/zone.js/commit/1fe4df0)) +* **jasmine:** propagate all arguments of it/describe/etc... ([a85fd68](https://github.com/angular/zone.js/commit/a85fd68)) +* **long-stack:** Safer writing of stack traces. ([6767ff5](https://github.com/angular/zone.js/commit/6767ff5)) +* **promise:** support more aggressive optimization. ([#431](https://github.com/angular/zone.js/issues/431)) ([26fc3da](https://github.com/angular/zone.js/commit/26fc3da)) +* **XHR:** Don't send sync XHR through ZONE ([6e2f13c](https://github.com/angular/zone.js/commit/6e2f13c)), closes [#377](https://github.com/angular/zone.js/issues/377) + + +### Features + +* assert that right ZoneAwarePromise is available ([#420](https://github.com/angular/zone.js/issues/420)) ([4c35e5b](https://github.com/angular/zone.js/commit/4c35e5b)) + + + + +## [0.6.17](https://github.com/angular/zone.js/compare/v0.6.15...v0.6.17) (2016-08-22) + + +### Bug Fixes + +* **browser:** use XMLHttpRequest.DONE constant on target instead of the global interface ([#395](https://github.com/angular/zone.js/issues/395)) ([3b4c20b](https://github.com/angular/zone.js/commit/3b4c20b)), closes [#394](https://github.com/angular/zone.js/issues/394) +* **jasmine:** spelling error of 'describe' in jasmine patch prevented application of sync zone ([d38ccde](https://github.com/angular/zone.js/commit/d38ccde)), closes [#412](https://github.com/angular/zone.js/issues/412) +* **patchProperty:** return null as the default value ([#413](https://github.com/angular/zone.js/issues/413)) ([396942b](https://github.com/angular/zone.js/commit/396942b)), closes [#319](https://github.com/angular/zone.js/issues/319) +* IE10/11 timeout issues. ([382182c](https://github.com/angular/zone.js/commit/382182c)) + + + + +## [0.6.15](https://github.com/angular/zone.js/compare/v0.6.14...v0.6.15) (2016-08-19) + + +### Bug Fixes + +* broken build. ([#406](https://github.com/angular/zone.js/issues/406)) ([5e3c207](https://github.com/angular/zone.js/commit/5e3c207)) +* **tasks:** do not drain the microtask queue early. ([ff88bb4](https://github.com/angular/zone.js/commit/ff88bb4)) +* **tasks:** do not drain the microtask queue early. ([d4a1436](https://github.com/angular/zone.js/commit/d4a1436)) + + + + +## [0.6.14](https://github.com/angular/zone.js/compare/v0.6.13...v0.6.14) (2016-08-17) + + +### Features + +* **jasmine:** patch jasmine to understand zones. ([3a054be](https://github.com/angular/zone.js/commit/3a054be)) +* **trackingZone:** Keep track of tasks to see outstanding tasks. ([4942b4a](https://github.com/angular/zone.js/commit/4942b4a)) + + + + +## [0.6.13](https://github.com/angular/zone.js/compare/v0.6.12...v0.6.13) (2016-08-15) + + +### Bug Fixes + +* **browser:** make Object.defineProperty patch safer ([#392](https://github.com/angular/zone.js/issues/392)) ([597c634](https://github.com/angular/zone.js/commit/597c634)), closes [#391](https://github.com/angular/zone.js/issues/391) +* **browser:** patch Window when EventTarget is missing. ([#368](https://github.com/angular/zone.js/issues/368)) ([fcef80d](https://github.com/angular/zone.js/commit/fcef80d)), closes [#367](https://github.com/angular/zone.js/issues/367) +* **browser:** patchTimer cancelAnimationFrame ([#353](https://github.com/angular/zone.js/issues/353)) ([bf77fbb](https://github.com/angular/zone.js/commit/bf77fbb)), closes [#326](https://github.com/angular/zone.js/issues/326) [Leaflet/Leaflet#4588](https://github.com/Leaflet/Leaflet/issues/4588) +* **browser:** should not throw with frozen prototypes ([#351](https://github.com/angular/zone.js/issues/351)) ([27ca2a9](https://github.com/angular/zone.js/commit/27ca2a9)) +* **build:** fix broken master due to setTimeout not returning a number on node ([d43b4b8](https://github.com/angular/zone.js/commit/d43b4b8)) +* **doc:** Fixed the home page example. ([#348](https://github.com/angular/zone.js/issues/348)) ([9a0aa4a](https://github.com/angular/zone.js/commit/9a0aa4a)) +* throw if trying to load zone more then once. ([6df5f93](https://github.com/angular/zone.js/commit/6df5f93)) +* **fakeAsync:** throw error on rejected promisees. ([fd1dfcc](https://github.com/angular/zone.js/commit/fd1dfcc)) +* **promise:** allow Promise subclassing ([dafad98](https://github.com/angular/zone.js/commit/dafad98)) +* **XHR.responseBlob:** don't access XHR.responseBlob on old android webkit ([#329](https://github.com/angular/zone.js/issues/329)) ([ed69756](https://github.com/angular/zone.js/commit/ed69756)) + + +### Features + +* return timeout Id in ZoneTask.toString (fixes [#341](https://github.com/angular/zone.js/issues/341)) ([80ae6a8](https://github.com/angular/zone.js/commit/80ae6a8)), closes [#375](https://github.com/angular/zone.js/issues/375) +* **jasmine:** Switch jasmine patch to use microtask and preserve zone. ([5f519de](https://github.com/angular/zone.js/commit/5f519de)) +* **ProxySpec:** create a ProxySpec which can proxy to other ZoneSpecs. ([2d02e39](https://github.com/angular/zone.js/commit/2d02e39)) +* **zone:** Add Zone.getZone api ([0621014](https://github.com/angular/zone.js/commit/0621014)) + + + + +## [0.6.12](https://github.com/angular/zone.js/compare/v0.6.11...v0.6.12) (2016-04-19) + + +### Bug Fixes + +* **property-descriptor:** do not fail for events without targets ([3a8deef](https://github.com/angular/zone.js/commit/3a8deef)) + + +### Features + +* Add a zone spec for fake async test zone. ([#330](https://github.com/angular/zone.js/issues/330)) ([34159b4](https://github.com/angular/zone.js/commit/34159b4)) + + + + +## [0.6.11](https://github.com/angular/zone.js/compare/v0.6.9...v0.6.11) (2016-04-14) + + +### Bug Fixes + +* Suppress closure compiler warnings about unknown 'process' variable. ([e125173](https://github.com/angular/zone.js/commit/e125173)), closes [#295](https://github.com/angular/zone.js/issues/295) +* **setTimeout:** fix for [#290](https://github.com/angular/zone.js/issues/290), allow clearTimeout to be called in setTimeout callback ([a6967ad](https://github.com/angular/zone.js/commit/a6967ad)), closes [#301](https://github.com/angular/zone.js/issues/301) +* **WebSocket patch:** fix WebSocket constants copy ([#299](https://github.com/angular/zone.js/issues/299)) ([5dc4339](https://github.com/angular/zone.js/commit/5dc4339)) +* **xhr:** XHR macrotasks allow abort after XHR has completed ([#311](https://github.com/angular/zone.js/issues/311)) ([c70f011](https://github.com/angular/zone.js/commit/c70f011)) +* **zone:** remove debugger statement ([#292](https://github.com/angular/zone.js/issues/292)) ([01cec16](https://github.com/angular/zone.js/commit/01cec16)) +* window undefined in node environments ([f8d5dc7](https://github.com/angular/zone.js/commit/f8d5dc7)), closes [#305](https://github.com/angular/zone.js/issues/305) + + +### Features + +* **zonespec:** add a spec for synchronous tests ([#294](https://github.com/angular/zone.js/issues/294)) ([55da3d8](https://github.com/angular/zone.js/commit/55da3d8)) +* node/node ([29fc5d2](https://github.com/angular/zone.js/commit/29fc5d2)) + + + + +## [0.6.9](https://github.com/angular/zone.js/compare/v0.6.5...v0.6.9) (2016-04-04) + + +### Bug Fixes + +* Allow calling clearTimeout from within the setTimeout callback ([a8ea55d](https://github.com/angular/zone.js/commit/a8ea55d)), closes [#302](https://github.com/angular/zone.js/issues/302) +* Canceling already run task should not double decrement task counter ([faa3485](https://github.com/angular/zone.js/commit/faa3485)), closes [#290](https://github.com/angular/zone.js/issues/290) +* **xhr:** don't throw on an xhr which is aborted before sending ([8827e1e](https://github.com/angular/zone.js/commit/8827e1e)) +* **zone:** remove debugger statement ([d7c116b](https://github.com/angular/zone.js/commit/d7c116b)) + + +### Features + +* **zonespec:** add a spec for synchronous tests ([0a6a434](https://github.com/angular/zone.js/commit/0a6a434)) +* treat XHRs as macrotasks ([fd39f97](https://github.com/angular/zone.js/commit/fd39f97)) + + + + +## [0.6.5](https://github.com/angular/zone.js/compare/v0.6.2...v0.6.5) (2016-03-21) + + +### Bug Fixes + +* disable safari 7 ([4a4d4f6](https://github.com/angular/zone.js/commit/4a4d4f6)) +* **browser/utils:** calling removeEventListener twice with the same args should not cause errors ([1787339](https://github.com/angular/zone.js/commit/1787339)), closes [#283](https://github.com/angular/zone.js/issues/283) [#284](https://github.com/angular/zone.js/issues/284) +* **patching:** call native cancel method ([5783663](https://github.com/angular/zone.js/commit/5783663)), closes [#278](https://github.com/angular/zone.js/issues/278) [#279](https://github.com/angular/zone.js/issues/279) +* **utils:** add the ability to prevent the default action of onEvent (onclick, onpaste,etc..) by returning false. ([99940c3](https://github.com/angular/zone.js/commit/99940c3)), closes [#236](https://github.com/angular/zone.js/issues/236) +* **WebSocket patch:** keep WebSocket constants ([f25b087](https://github.com/angular/zone.js/commit/f25b087)), closes [#267](https://github.com/angular/zone.js/issues/267) +* **zonespec:** Do not crash on error if last task had no data ([0dba019](https://github.com/angular/zone.js/commit/0dba019)), closes [#281](https://github.com/angular/zone.js/issues/281) + + +### Features + +* **indexdb:** Added property patches and event target methods as well as tests for Indexed DB ([84a251f](https://github.com/angular/zone.js/commit/84a251f)), closes [#204](https://github.com/angular/zone.js/issues/204) +* **zonespec:** add a spec for asynchronous tests ([aeeb05c](https://github.com/angular/zone.js/commit/aeeb05c)), closes [#275](https://github.com/angular/zone.js/issues/275) + + + + +## [0.6.2](https://github.com/angular/zone.js/compare/v0.6.1...v0.6.2) (2016-03-03) + + + + +## [0.6.1](https://github.com/angular/zone.js/compare/v0.6.0...v0.6.1) (2016-02-29) + + + + +# [0.6.0](https://github.com/angular/zone.js/compare/v0.5.15...v0.6.0) (2016-02-29) + + +### Chores + +* **everything:** Major Zone Rewrite/Reimplementation ([63d4552](https://github.com/angular/zone.js/commit/63d4552)) + + +### BREAKING CHANGES + +* everything: This is a brand new implementation which is not backwards compatible. + + + + +## [0.5.15](https://github.com/angular/zone.js/compare/v0.5.14...v0.5.15) (2016-02-17) + + +### Bug Fixes + +* **WebWorker:** Patch WebSockets and XMLHttpRequest in WebWorker ([45a6bc1](https://github.com/angular/zone.js/commit/45a6bc1)), closes [#249](https://github.com/angular/zone.js/issues/249) +* **WebWorker:** Patch WebSockets and XMLHttpRequest in WebWorker ([9041a3a](https://github.com/angular/zone.js/commit/9041a3a)), closes [#249](https://github.com/angular/zone.js/issues/249) + + + + +## [0.5.14](https://github.com/angular/zone.js/compare/v0.5.11...v0.5.14) (2016-02-11) + + + + +## [0.5.11](https://github.com/angular/zone.js/compare/v0.5.10...v0.5.11) (2016-01-27) + + +### Bug Fixes + +* correct incorrect example path in karma config ([b0a624d](https://github.com/angular/zone.js/commit/b0a624d)) +* correct test relaying on jasmine timeout ([4f7d6ae](https://github.com/angular/zone.js/commit/4f7d6ae)) +* **WebSocket:** don't patch EventTarget methods twice ([345e56c](https://github.com/angular/zone.js/commit/345e56c)), closes [#235](https://github.com/angular/zone.js/issues/235) + + +### Features + +* **wtf:** add wtf support to (set/clear)Timeout/Interval/Immediate ([6659fd5](https://github.com/angular/zone.js/commit/6659fd5)) + + + + +## [0.5.10](https://github.com/angular/zone.js/compare/v0.5.9...v0.5.10) (2015-12-11) + + +### Bug Fixes + +* **keys:** Do not use Symbol which are broken in Chrome 39.0.2171 (Dartium) ([c48301b](https://github.com/angular/zone.js/commit/c48301b)) +* **Promise:** Make sure we check for native Promise before es6-promise gets a chance to polyfill ([fa18d4c](https://github.com/angular/zone.js/commit/fa18d4c)) + + + + +## [0.5.9](https://github.com/angular/zone.js/compare/v0.5.8...v0.5.9) (2015-12-09) + + +### Bug Fixes + +* **keys:** do not declare functions inside blocks ([d44d699](https://github.com/angular/zone.js/commit/d44d699)), closes [#194](https://github.com/angular/zone.js/issues/194) +* **keys:** Symbol is being checked for type of function ([6714be6](https://github.com/angular/zone.js/commit/6714be6)) +* **mutation-observe:** output of typeof operator should be string ([19703e3](https://github.com/angular/zone.js/commit/19703e3)) +* **util:** origin addEventListener/removeEventListener should be called without eventListener ([26e7f51](https://github.com/angular/zone.js/commit/26e7f51)), closes [#198](https://github.com/angular/zone.js/issues/198) +* **utils:** should have no effect when called addEventListener/removeEventListener without eventListener. ([5bcc6ae](https://github.com/angular/zone.js/commit/5bcc6ae)) + + + + +## [0.5.8](https://github.com/angular/zone.js/compare/v0.5.7...v0.5.8) (2015-10-06) + + +### Bug Fixes + +* **addEventListener:** when called from the global scope ([a23d61a](https://github.com/angular/zone.js/commit/a23d61a)), closes [#190](https://github.com/angular/zone.js/issues/190) +* **EventTarget:** apply the patch even if `Window` is not defined ([32c6df9](https://github.com/angular/zone.js/commit/32c6df9)) + + + + +## [0.5.7](https://github.com/angular/zone.js/compare/v0.5.6...v0.5.7) (2015-09-29) + + +### Bug Fixes + +* **RequestAnimationFrame:** pass the timestamp to the callback ([79a37c0](https://github.com/angular/zone.js/commit/79a37c0)), closes [#187](https://github.com/angular/zone.js/issues/187) + + + + +## [0.5.6](https://github.com/angular/zone.js/compare/v0.5.5...v0.5.6) (2015-09-25) + + +### Bug Fixes + +* **Jasmine:** add support for jasmine 2 done.fail() ([1d4370b](https://github.com/angular/zone.js/commit/1d4370b)), closes [#180](https://github.com/angular/zone.js/issues/180) +* **utils:** fixes event target patch in web workers ([ad5c0c8](https://github.com/angular/zone.js/commit/ad5c0c8)) + + + + +## [0.5.5](https://github.com/angular/zone.js/compare/v0.5.4...v0.5.5) (2015-09-11) + + +### Bug Fixes + +* **lib/utils:** adds compliant handling of useCapturing param for EventTarget methods ([dd2e1bf](https://github.com/angular/zone.js/commit/dd2e1bf)) +* **lib/utils:** fixes incorrect behaviour when re-adding the same event listener fn ([1b804cf](https://github.com/angular/zone.js/commit/1b804cf)) +* **longStackTraceZone:** modifies stackFramesFilter to exclude zone.js frames ([50ce9f3](https://github.com/angular/zone.js/commit/50ce9f3)) + + +### Features + +* **lib/core:** add/removeEventListener hooks ([1897440](https://github.com/angular/zone.js/commit/1897440)) +* **lib/patch/file-reader:** zone-binds FileReader#onEventName listeners ([ce589b9](https://github.com/angular/zone.js/commit/ce589b9)), closes [#137](https://github.com/angular/zone.js/issues/137) + + + + +## [0.5.4](https://github.com/angular/zone.js/compare/v0.5.3...v0.5.4) (2015-08-31) + + +### Bug Fixes + +* js path in examples ([c7a2ed9](https://github.com/angular/zone.js/commit/c7a2ed9)) +* **zone:** fix conflict with Polymer elements ([77b4c0d](https://github.com/angular/zone.js/commit/77b4c0d)) + + +### Features + +* **patch:** support requestAnimationFrame time loops ([3d6dc08](https://github.com/angular/zone.js/commit/3d6dc08)) + + + + +## [0.5.3](https://github.com/angular/zone.js/compare/v0.5.2...v0.5.3) (2015-08-21) + + +### Bug Fixes + +* **addEventListener patch:** ignore FunctionWrapper for IE11 & Edge dev tools ([3b0ca3f](https://github.com/angular/zone.js/commit/3b0ca3f)) +* **utils:** event listener patches break when passed an object implementing EventListener ([af88ff8](https://github.com/angular/zone.js/commit/af88ff8)) +* **WebWorker:** Fix patching in WebWorker ([2cc59d8](https://github.com/angular/zone.js/commit/2cc59d8)) + + +### Features + +* **zone.js:** support Android browser ([93b5555](https://github.com/angular/zone.js/commit/93b5555)) + + + + +## [0.5.2](https://github.com/angular/zone.js/compare/v0.5.1...v0.5.2) (2015-07-01) + + +### Bug Fixes + +* **jasmine patch:** forward timeout ([2dde717](https://github.com/angular/zone.js/commit/2dde717)) +* **zone.bind:** throw an error if arg is not a function ([ee4262a](https://github.com/angular/zone.js/commit/ee4262a)) + + + + +## [0.5.1](https://github.com/angular/zone.js/compare/v0.5.0...v0.5.1) (2015-06-10) + + +### Bug Fixes + +* **PatchClass:** copy static properties ([b91f8fe](https://github.com/angular/zone.js/commit/b91f8fe)), closes [#127](https://github.com/angular/zone.js/issues/127) +* **register-element:** add check for callback being own property of opts ([8bce00e](https://github.com/angular/zone.js/commit/8bce00e)), closes [#52](https://github.com/angular/zone.js/issues/52) + + +### Features + +* **fetch:** patch the fetch API ([4d3d524](https://github.com/angular/zone.js/commit/4d3d524)), closes [#108](https://github.com/angular/zone.js/issues/108) +* **geolocation:** patch the API ([cd13da1](https://github.com/angular/zone.js/commit/cd13da1)), closes [#113](https://github.com/angular/zone.js/issues/113) +* **jasmine:** export the jasmine patch ([639d5e7](https://github.com/angular/zone.js/commit/639d5e7)) +* **test:** serve lib/ files instead of dist/ ([f835213](https://github.com/angular/zone.js/commit/f835213)) +* **zone.js:** support IE9+ ([554fae0](https://github.com/angular/zone.js/commit/554fae0)) + + + + +# [0.5.0](https://github.com/angular/zone.js/compare/v0.4.4...v0.5.0) (2015-05-08) + + +### Bug Fixes + +* always run jasmine's done callbacks for async tests in jasmine's zone ([b7f3d04](https://github.com/angular/zone.js/commit/b7f3d04)), closes [#91](https://github.com/angular/zone.js/issues/91) +* don't fork new zones for callbacks from the root zone ([531d0ec](https://github.com/angular/zone.js/commit/531d0ec)), closes [#92](https://github.com/angular/zone.js/issues/92) +* **MutationObserver:** executes hooks in the creation zone ([3122a48](https://github.com/angular/zone.js/commit/3122a48)) +* **test:** fix an ineffective assertion ([d85d2cf](https://github.com/angular/zone.js/commit/d85d2cf)) +* minor fixes ([18f5511](https://github.com/angular/zone.js/commit/18f5511)) + + +### Code Refactoring + +* split zone.js into CJS modules, add zone-microtask.js ([2e52900](https://github.com/angular/zone.js/commit/2e52900)) + + +### Features + +* **scheduling:** Prefer MutationObserver over Promise in FF ([038bdd9](https://github.com/angular/zone.js/commit/038bdd9)) +* **scheduling:** Support Promise.then() fallbacks to enqueue a microtask ([74eff1c](https://github.com/angular/zone.js/commit/74eff1c)) +* add isRootZone api ([bf925bf](https://github.com/angular/zone.js/commit/bf925bf)) +* make root zone id to be 1 ([605e213](https://github.com/angular/zone.js/commit/605e213)) + + +### BREAKING CHANGES + +* New child zones are now created only from a async task +that installed a custom zone. + +Previously even without a custom zone installed (e.g. +LongStacktracesZone), we would spawn new +child zones for all asynchronous events. This is undesirable and +generally not useful. + +It does not make sense for us to create new zones for callbacks from the +root zone since we care +only about callbacks from installed custom zones. This reduces the +overhead of zones. + +This primarily means that LongStackTraces zone won't be able to trace +events back to Zone.init(), +but instead the starting point will be the installation of the +LongStacktracesZone. In all practical +situations this should be sufficient. +* zone.js as well as *-zone.js files are moved from / to dist/ + + + + +## [0.4.4](https://github.com/angular/zone.js/compare/v0.4.3...v0.4.4) (2015-05-07) + + +### Bug Fixes + +* commonjs wrapper ([7b4fdde](https://github.com/angular/zone.js/commit/7b4fdde)), closes [#19](https://github.com/angular/zone.js/issues/19) +* fork the zone in first example (README) ([7b6e8ed](https://github.com/angular/zone.js/commit/7b6e8ed)) +* prevent aliasing original window reference ([63b42bd](https://github.com/angular/zone.js/commit/63b42bd)) +* use strcit mode for the zone.js code only ([16855e5](https://github.com/angular/zone.js/commit/16855e5)) +* **test:** use console.log rather than dump in tests ([490e6dd](https://github.com/angular/zone.js/commit/490e6dd)) +* **websockets:** patch websockets via descriptors ([d725f46](https://github.com/angular/zone.js/commit/d725f46)), closes [#81](https://github.com/angular/zone.js/issues/81) +* **websockets:** properly patch websockets in Safari 7.0 ([3ba6fa1](https://github.com/angular/zone.js/commit/3ba6fa1)), closes [#88](https://github.com/angular/zone.js/issues/88) +* **websockets:** properly patch websockets on Safari 7.1 ([1799a20](https://github.com/angular/zone.js/commit/1799a20)) + + +### Features + +* add websockets example ([edb17d2](https://github.com/angular/zone.js/commit/edb17d2)) +* log a warning if we suspect duplicate Zone install ([657f6fe](https://github.com/angular/zone.js/commit/657f6fe)) + + + + +## [0.4.3](https://github.com/angular/zone.js/compare/v0.4.2...v0.4.3) (2015-04-08) + + +### Bug Fixes + +* **zone:** keep argument[0] refs around. ([48573ff](https://github.com/angular/zone.js/commit/48573ff)) + + + + +## [0.4.2](https://github.com/angular/zone.js/compare/v0.4.1...v0.4.2) (2015-03-27) + + +### Bug Fixes + +* **zone.js:** don't make function declaration in block scope ([229fd8f](https://github.com/angular/zone.js/commit/229fd8f)), closes [#53](https://github.com/angular/zone.js/issues/53) [#54](https://github.com/angular/zone.js/issues/54) + + +### Features + +* **bindPromiseFn:** add bindPromiseFn method ([643f2ac](https://github.com/angular/zone.js/commit/643f2ac)), closes [#49](https://github.com/angular/zone.js/issues/49) +* **lstz:** allow getLongStacktrace to be called with zero args ([26a4dc2](https://github.com/angular/zone.js/commit/26a4dc2)), closes [#47](https://github.com/angular/zone.js/issues/47) +* **Zone:** add unique id to each zone ([fb338b6](https://github.com/angular/zone.js/commit/fb338b6)), closes [#45](https://github.com/angular/zone.js/issues/45) + + + + +## [0.4.1](https://github.com/angular/zone.js/compare/v0.4.0...v0.4.1) (2015-02-20) + + +### Bug Fixes + +* **patchViaPropertyDescriptor:** disable if properties are not configurable ([fb5e644](https://github.com/angular/zone.js/commit/fb5e644)), closes [#42](https://github.com/angular/zone.js/issues/42) + + + + +# [0.4.0](https://github.com/angular/zone.js/compare/v0.3.0...v0.4.0) (2015-02-04) + + +### Bug Fixes + +* **WebSocket:** patch WebSocket instance ([7b8e1e6](https://github.com/angular/zone.js/commit/7b8e1e6)) + + + + +# [0.3.0](https://github.com/angular/zone.js/compare/v0.2.4...v0.3.0) (2014-06-12) + + +### Bug Fixes + +* add events for webgl contexts ([4b6e411](https://github.com/angular/zone.js/commit/4b6e411)) +* bind prototype chain callback of custom element descriptor ([136e518](https://github.com/angular/zone.js/commit/136e518)) +* dequeue tasks from the zone that enqueued it ([f127fd4](https://github.com/angular/zone.js/commit/f127fd4)) +* do not reconfig property descriptors of prototypes ([e9dfbed](https://github.com/angular/zone.js/commit/e9dfbed)) +* patch property descriptors in Object.create ([7b7258b](https://github.com/angular/zone.js/commit/7b7258b)), closes [#24](https://github.com/angular/zone.js/issues/24) +* support mozRequestAnimationFrame ([886f67d](https://github.com/angular/zone.js/commit/886f67d)) +* wrap non-configurable custom element callbacks ([383b479](https://github.com/angular/zone.js/commit/383b479)), closes [#24](https://github.com/angular/zone.js/issues/24) +* wrap Object.defineProperties ([f587f17](https://github.com/angular/zone.js/commit/f587f17)), closes [#24](https://github.com/angular/zone.js/issues/24) + + + + +## [0.2.4](https://github.com/angular/zone.js/compare/v0.2.3...v0.2.4) (2014-05-23) + + + + +## [0.2.3](https://github.com/angular/zone.js/compare/v0.2.2...v0.2.3) (2014-05-23) + + +### Bug Fixes + +* remove dump ([45fb7ba](https://github.com/angular/zone.js/commit/45fb7ba)) + + + + +## [0.2.2](https://github.com/angular/zone.js/compare/v0.2.1...v0.2.2) (2014-05-22) + + +### Bug Fixes + +* correctly detect support for document.registerElement ([ab1d487](https://github.com/angular/zone.js/commit/ab1d487)) +* dont automagically dequeue on setInterval ([da99e15](https://github.com/angular/zone.js/commit/da99e15)) +* fork should deep clone objects ([21b47ae](https://github.com/angular/zone.js/commit/21b47ae)) +* support MutationObserver.disconnect ([ad711b8](https://github.com/angular/zone.js/commit/ad711b8)) + + +### Features + +* add stackFramesFilter to longStackTraceZone ([7133de0](https://github.com/angular/zone.js/commit/7133de0)) +* expose hooks for enqueuing and dequing tasks ([ba72f34](https://github.com/angular/zone.js/commit/ba72f34)) +* improve countingZone and example ([86328fb](https://github.com/angular/zone.js/commit/86328fb)) +* support document.registerElement ([d3c785a](https://github.com/angular/zone.js/commit/d3c785a)), closes [#18](https://github.com/angular/zone.js/issues/18) + + + + +## [0.2.1](https://github.com/angular/zone.js/compare/v0.2.0...v0.2.1) (2014-04-24) + + +### Bug Fixes + +* add support for WebKitMutationObserver ([d1a2c8e](https://github.com/angular/zone.js/commit/d1a2c8e)) +* preserve setters when wrapping XMLHttpRequest ([fb46688](https://github.com/angular/zone.js/commit/fb46688)), closes [#17](https://github.com/angular/zone.js/issues/17) + + + + +# [0.2.0](https://github.com/angular/zone.js/compare/v0.1.1...v0.2.0) (2014-04-17) + + +### Bug Fixes + +* patch all properties on the proto chain ([b6d76f0](https://github.com/angular/zone.js/commit/b6d76f0)) +* patch MutationObserver ([1c4e85e](https://github.com/angular/zone.js/commit/1c4e85e)) +* wrap XMLHttpRequest when we cant patch protos ([76de58e](https://github.com/angular/zone.js/commit/76de58e)) + + +### Features + +* add exceptZone ([b134391](https://github.com/angular/zone.js/commit/b134391)) + + + + +## [0.1.1](https://github.com/angular/zone.js/compare/v0.1.0...v0.1.1) (2014-03-31) + + +### Features + +* add commonjs support ([0fe349e](https://github.com/angular/zone.js/commit/0fe349e)) + + + + +# [0.1.0](https://github.com/angular/zone.js/compare/v0.0.0...v0.1.0) (2014-03-31) + + +### Bug Fixes + +* improve patching browsers with EventTarget ([7d3a8b1](https://github.com/angular/zone.js/commit/7d3a8b1)) +* improve stacktrace capture on Safari ([46a6fbc](https://github.com/angular/zone.js/commit/46a6fbc)) +* long stack trace test ([01ce3b3](https://github.com/angular/zone.js/commit/01ce3b3)) +* prevent calling addEventListener on non-functions ([7acebca](https://github.com/angular/zone.js/commit/7acebca)) +* throw if a zone does not define an onError hook ([81d5f49](https://github.com/angular/zone.js/commit/81d5f49)) +* throw if a zone does not define an onError hook ([3485c1b](https://github.com/angular/zone.js/commit/3485c1b)) + + +### Features + +* add decorator syntax ([c6202a1](https://github.com/angular/zone.js/commit/c6202a1)) +* add onZoneCreated hook ([f7badb6](https://github.com/angular/zone.js/commit/f7badb6)) +* patch onclick in Chrome and Safari ([7205295](https://github.com/angular/zone.js/commit/7205295)) +* refactor and test counting zone ([648a95d](https://github.com/angular/zone.js/commit/648a95d)) +* support Promise ([091f44e](https://github.com/angular/zone.js/commit/091f44e)) + + + + +# 0.0.0 (2013-09-18) + + + diff --git a/packages/zone.js/DEVELOPER.md b/packages/zone.js/DEVELOPER.md new file mode 100644 index 0000000000..d033096f42 --- /dev/null +++ b/packages/zone.js/DEVELOPER.md @@ -0,0 +1,90 @@ +To run tests +------------ + +Make sure your environment is set up with: + +`yarn` + +In a separate process, run the WebSockets server: + +`yarn ws-server` + +Run the browser tests using Karma: + +`yarn test` + +Run the node.js tests: + +`yarn test-node` + +Run tslint: + +`yarn lint` + +Run format with clang-format: + +`yarn format` + +Run all checks (lint/format/browser test/test-node): + +`yarn ci` + +Before Commit +------------ + +Please make sure you pass all following checks before commit + +- gulp lint (tslint) +- gulp format:enforce (clang-format) +- gulp promisetest (promise a+ test) +- yarn test (karma browser test) +- gulp test-node (node test) + +You can run + +`yarn ci` + +to do all those checks for you. +You can also add the script into your git pre-commit hook + +``` +echo -e 'exec npm run ci' > .git/hooks/pre-commit +chmod u+x .git/hooks/pre-commit +``` + +Webdriver Test +-------------- + +`zone.js` also supports running webdriver e2e tests. + +1. run locally + +``` +yarn webdriver-start +yarn webdriver-http +yarn webdriver-test +``` + +2. run locally with sauce connect + +``` +// export SAUCE_USERNAME and SAUCE_ACCESS_KEY +export SAUCE_USERNAME=XXXX +export SAUCE_ACCESS_KEY=XXX + +sc -u $SAUCE_USERNAME -k $SAUCE_ACCESS_KEY +yarn webdriver-http +yarn webdriver-sauce-test +``` + +Releasing +--------- + +To make a `dry-run`, run the following commands. +``` +$ VERSION= +$ git tag 'zone.js-$VERSION' +$ yarn bazel --output_base=$(mktemp -d) run //packages/zone.js:npm_package.pack --workspace_status_command="echo BUILD_SCM_VERSION $VERSION" +``` + +If everything looks fine, replace `.pack` with `.publish` to push to the npm registry. \ No newline at end of file diff --git a/packages/zone.js/MODULE.md b/packages/zone.js/MODULE.md new file mode 100644 index 0000000000..cade0d1ad5 --- /dev/null +++ b/packages/zone.js/MODULE.md @@ -0,0 +1,139 @@ +# Modules + +Starting from zone.js v0.8.9, you can choose which web API modules you want to patch as to reduce overhead introduced by the patching of these modules. For example, +the below samples show how to disable some modules. You just need to define a few global variables +before loading zone.js. + +``` + + +``` + +Below is the full list of currently supported modules. + +- Common + +|Module Name|Behavior with zone.js patch|How to disable| +|--|--|--| +|Error|stack frames will have the Zone's name information, (By default, Error patch will not be loaded by zone.js)|__Zone_disable_Error = true| +|toString|Function.toString will be patched to return native version of toString|__Zone_disable_toString = true| +|ZoneAwarePromise|Promise.then will be patched as Zone aware MicroTask|__Zone_disable_ZoneAwarePromise = true| +|bluebird|Bluebird will use Zone.scheduleMicroTask as async scheduler. (By default, bluebird patch will not be loaded by zone.js)|__Zone_disable_bluebird = true| + +- Browser + +|Module Name|Behavior with zone.js patch|How to disable| +|--|--|--| +|on_property|target.onProp will become zone aware target.addEventListener(prop)|__Zone_disable_on_property = true| +|timers|setTimeout/setInterval/setImmediate will be patched as Zone MacroTask|__Zone_disable_timers = true| +|requestAnimationFrame|requestAnimationFrame will be patched as Zone MacroTask|__Zone_disable_requestAnimationFrame = true| +|blocking|alert/prompt/confirm will be patched as Zone.run|__Zone_disable_blocking = true| +|EventTarget|target.addEventListener will be patched as Zone aware EventTask|__Zone_disable_EventTarget = true| +|IE BrowserTools check|in IE, browser tool will not use zone patched eventListener|__Zone_disable_IE_check = true| +|CrossContext check|in webdriver, enable check event listener is cross context|__Zone_enable_cross_context_check = true| +|XHR|XMLHttpRequest will be patched as Zone aware MacroTask|__Zone_disable_XHR = true| +|geolocation|navigator.geolocation's prototype will be patched as Zone.run|__Zone_disable_geolocation = true| +|PromiseRejectionEvent|PromiseRejectEvent will fire when ZoneAwarePromise has unhandled error|__Zone_disable_PromiseRejectionEvent = true| +|mediaQuery|mediaQuery addListener API will be patched as Zone aware EventTask. (By default, mediaQuery patch will not be loaded by zone.js) |__Zone_disable_mediaQuery = true| +|notification|notification onProperties API will be patched as Zone aware EventTask. (By default, notification patch will not be loaded by zone.js) |__Zone_disable_notification = true| + +- NodeJS + +|Module Name|Behavior with zone.js patch|How to disable| +|--|--|--| +|node_timers|NodeJS patch timer|__Zone_disable_node_timers = true| +|fs|NodeJS patch fs function as macroTask|__Zone_disable_fs = true| +|EventEmitter|NodeJS patch EventEmitter as Zone aware EventTask|__Zone_disable_EventEmitter = true| +|nextTick|NodeJS patch process.nextTick as microTask|__Zone_disable_nextTick = true| +|handleUnhandledPromiseRejection|NodeJS handle unhandledPromiseRejection from ZoneAwarePromise|__Zone_disable_handleUnhandledPromiseRejection = true| +|crypto|NodeJS patch crypto function as macroTask|__Zone_disable_crypto = true| + +- on_property + +You can also disable specific on_properties by setting `__Zone_ignore_on_properties` as follows: for example, +if you want to disable `window.onmessage` and `HTMLElement.prototype.onclick` from zone.js patching, +you can do like this. + +``` + + +``` + +- Error + +By default, `zone.js/dist/zone-error` will not be loaded for performance concern. +This package will provide following functionality. + + 1. Error inherit: handle `extend Error` issue. + ``` + class MyError extends Error {} + const myError = new MyError(); + console.log('is MyError instanceof Error', (myError instanceof Error)); + ``` + + without `zone-error` patch, the example above will output `false`, with the patch, the reuslt will be `true`. + + 2. BlacklistZoneStackFrames: remove zone.js stack from `stackTrace`, and add `zone` information. Without this patch, a lot of `zone.js` invocation stack will be shown + in stack frames. + + ``` + at zone.run (polyfill.bundle.js: 3424) + at zoneDelegate.invokeTask (polyfill.bundle.js: 3424) + at zoneDelegate.runTask (polyfill.bundle.js: 3424) + at zone.drainMicroTaskQueue (polyfill.bundle.js: 3424) + at a.b.c (vendor.bundle.js: 12345 ) + at d.e.f (main.bundle.js: 23456) + ``` + + with this patch, those zone frames will be removed, + and the zone information `/` will be added + + ``` + at a.b.c (vendor.bundle.js: 12345 ) + at d.e.f (main.bundle.js: 23456 ) + ``` + + 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 + +[![CDNJS](https://img.shields.io/cdnjs/v/zone.js.svg)](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: + +[![screenshot of the zone.js presentation and ng-conf 2014](/presentation.png)](//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 +[![Promises/A+ 1.1 compliant](https://promisesaplus.com/assets/logo-small.png)](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 0000000000000000000000000000000000000000..e1344e25a199c28f8629850d6a7ed8b0c0af93ac GIT binary patch literal 26146 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 T=8xdK@{gjNnrxBG1Hb 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 0000000000000000000000000000000000000000..eb984aee384358c4761e0bc18ce7ebf214c82b6a GIT binary patch literal 33058 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 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 0000000000000000000000000000000000000000..330359490e55c3db39977c191600ed346f7e6f0e GIT binary patch literal 21724 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 rRFoqKpNqc@!XIg&k}tP=_vv0(lF}3I6~} 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 0000000000000000000000000000000000000000..2fdcc1127bca36091a4b3eacdecd73b0be1fda8d GIT binary patch literal 31203 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 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 0000000000000000000000000000000000000000..20136425e200af06304f0381d2c0fe66a8c4800b GIT binary patch literal 31190 zcmcG$bySpH+dhnif`rnNiYP-!NJ)3sfP}Qf5RxJx4JzF*bW4}Wkdi7rbUSo+NH_fU z=<~eme%|L@-+$j)vu>Gd_OgD;8s0 z>-%iHl%SVpSG89T|9*~v@jls6RnK-zQUAzTP4u@W zYBA(mef^UJ+lKDQ$|DQPcg2RoiREPvOPQA;njGApS~Br}MCsD9nz#CEKMX6^SWjtE z+cTS58Sd0NdPAV4$`Q?sr8pJPb9{Yy>=oO7n+ohk|Mdpw?*|h@EmE_01ho3fo{kS< z96hf4$&YuWy*1h_c>!t8lBjf^zkgiLl>OkeU;T*LfR8NV6P*u4wq&k++}AKxoS>!t z-P^;r&p*iVKgPmV*UDV#aozexHitP|I6q0PTJ7P1a^ZGtZ zlQU9Gs zX_U|N2OfyFp!wu;-5Z!dZn$+9d%7sKuCx>`)0Zo2DwnRewnFaW{vqkR!6F! z8&nsxOn>zD?OTg1w$3=#-mWg9K#5zoZoyzMh#&^+M!Mj80sYL=*Crm`j(>F6S= zTZ}++_GVnrUAd@*MZT}EWTxvtu5#MU%uIFli;*#iEc(T_b@lZW3@vNJg}1I>v$VcB z21XK@S4em+$qNgo;vZ>i^RM7utGEWo^=@V7;n7q=d7K>{?C$O!9N^;NDapv(3zW#C zz6O>~E@T;#ZQK^-k4Lq8c(}Cl{{H>@3hAM{H^FbZYEEZ|n+(mSH>@+D5Rzb(RP-<( z;}a4XayNuLPND9KNFn!QyVtK@>*^-U(22QR2fK=EWNBHLj1WFuc`TH+y}9YJ(wC7b z9iGbP$Vg9rdl3sPjWfXC-`^goU-5<*{%5IjDk_or`4%1?lNFYl!otE33K0~Bd6J;p z;gBYZNuy{+^9Jk&=-YaJVIix!go=`q%wT@!u{YwHIfk5^+{VTR(au40AYlUI{ar5d zlWPzPZ@)(z9D7N&4HlC%70J$KvCLXOuWl3Y=x3u>jr&oCPEJPVcs^WssXJ-3#GtX# zdi;6LR$oSw+jf1G-K*#w!DlijH9HPfBvnWYP>F?cfcXyX*ql4*Z9?AB( zW5uHzt&m{I@eqN1W`y<~YihfWS6A4}JK`^HeshVE+@F8FS#YjeD+(?Hcep{}lug2zVYLNf)a zVWNkgOS_trN5YG@a|l=`k%7MIpZVowZJ^4`wx{Zo62u2k+J_A8X6iIiM{xeg+Tdsc6Od!CONK;^q-#{ zZG+Q;UdG18va_?lkOE7dF+j$uF)BK5O}OmLwIn1YWcB2~%pK{ok(KTERchRLd1`5I zsimbAT5=l`GbcB9@LXB=!l5gPxwAmX;Q9<0#l( zV}#uY8x@gjB`qy&dOGvt<7ACvVeuF_@nh9>!Ncw8=}c+-6Qrq(l9CMyksTLDs%TK_ zRIHLIb=~J76BE;uCoS#m5Mf~&f2=>d!_B`rKW}Vfqt6PX5%zR%k9c&Wu=MNKyq72* z+bK>C4xM^eu08grPyOJ64l8}LSNCXW!kf@KZKz};IXU^MVxokU)BvYswDdq+{SutO z0;IOC&V8c#EfF*>HdfANV5L8k;a$%C+4;6`8bQ}je@29BI!Q@Mr3rb|IB)md9E!S1 zK*3g{EMiYcZvDt)zPx#H{V+jZVH42g3Ar!!gW{MP&Ll>g&O{IAOZzW(QB0AK$< zF9X=_|GW&~>wjJbAkTgL)WKWrDo&wS*Fk@M5xQe5TJBnO>Zn^gtKX>Gpc3FEZzi~I z_uc1Z#>L%?N$!e!X~cqJ_km)=z`M_G$8BjXg*sBsJ*61WVwv>&iPmWob8~AKCTp+X zxgod}BT7yb{r5tP7%-$~(YMz|MN9vOEgy5yG!BBBA!%;ic++}5msz?4{Ub{hn=x0% zX~@O%Q+bAVD|bW*+iDR{*8c7LV`D@QiQ z=PvW%hjSGanRk;f2LyOHvg9-et#zH0H(l2s%%FFl=Xh6W$)p zQ^^#*5-6H+cGN>HlN2v-QLSxmcF*t7($WqL3^W_DUq??wok5}NA#56JFXnOjV&^im zz5oZymRFO%4$#pSdrP(`FLkx#tJT~VkF$oU9L2)>kEe*S4VKD0YVZ0E7i(9nOgA1? zP4kfuHFH}SEi4_XZ+0=Q~tMew>30ulZ8Ck9yg^exnG{pozkNAW}Du3bjjvONtIYlp6WR{K_!}n zObvN0b<0{$LV1`!XL`-=dD%?X#E5(ios7bx8rzzzjk1py_L7@#4b>_QqrMR8HoTDk z*6(b|q!XA76N2IX&Qd*?tnTjY49=&{5UEX1$5=+66*qjc!DGBLF1&w#F!yJUV$%8L zSvaSjMy$l!{ir8p`VIAZo~a7JU)modehg0)R(^C8xvlS2&vCXW;J~dRI*6ZfCUQSm6-{7N(L*%ZrN;iAzki72BTZG^>&%X}OrPnH58m{sb6|+D zw{tegnITVDyp2~*$+adZXQFJmeocKl*Z?=@L}02{)iRk_rP7|aQ-1)xJ)a#qwTi%#Kf)?{=ghoeE~V%A)5!!EJM3? ziHgt8Y+@}<>C(Q0!b4WzntV4z@;^jV^c z@yT-U%M~+kE{11MPu_NsSxUF&&0eha2p#^aGpw zIw;bJU}s zN~Q28aobG%>ae(;Y5TPAa##NLDQWgdvCigXt?DKDYH(*PFSrgx>V+ix(IWBa;zt)J{B zuLN@#9YYya2sGXG%2Q#p#r%O5uZ&`E>Jx)TYB4X%u0}_uqR(Qk%NowxQkD=ibo?O_ z2M@i8-c%736kK57MrWX%0SE;a+M@sK+wVEwV~OFsaLvkZ(Xfaiv>3xTYi^-iKFqE10r+1de_h|=5ZnMc;)RorWy|Pnek=y0+EC&Xw`5ks>i*Co^!etgyJ8aLnu;x z|If$&GlM^I67tV_)2-;T2fH6`Gs0}AZv7c8`opn9l)NXLMxd}2{>@k?2H~~$3EkgN zhjBkuRHtJ}{qG|3mvhdAE>a`zLNtFTO`NTK5$c~?TjXap9Iu3PieIuU@n<_OnnMl9 z-tc0js%tXVd-;i3O@(a#agPq7%lvyS_b)#V&3`S{R;?NLD+t<1X%t0E!P>G`f2K&Z zN}y7(q0aDNG=FY*<)ydnW^)PVpW%cP);*VbN?C}oXLN~QZVZ(aPzz}I|3dGtPtNsC zLXv2uSHA>3>`2DcE96WL?OR6$sCzp**MHA8`S|z%CyzGrju|;cR#UJ(|xrsHgdE z-C{~z8KLtWnJakYD&4hscA)}W@2-8{?Z2n(bu7L7)~oJ7HhnXWz0>pC&%3M*%#5rq zA2@J^}gsXDDEP1kJwbBVR~ z^=sBrEy`M+n=?Cz546dSV2k8bH8iAnwtq(V`!p@7P0m-5QFFmWN2aK%luyy-Le}|b zc)Ni}?*KU5E~;_b&E(bPV&u;omrYkLRaqq-9v=0Ac)#^8yobxZ3h|Xfb?sUvk8H|+ zYxO@)wn85PR=Ff?Borc0x_KmQm2wpkfu|bUyv>%#V|dML9x@Dd<09wWC+D7|yD( zvqAFPO?jz`@`qbX;%N2hm&QP@tB<`WHjQuS*##51-N)*JuafAmrq$H_z+oI8M-<7T zI2CWbcRybC$uSNiAD_VCODSs;ZtvQfBM%Q6$bB&gH1d$`W?vt4N}2ycs%w^u;wjg_ zd8Ti7hDW1|2G$;%^B4{V7meu+&AHTsUSd-E|_UEQUT*{p)^j)&Bj+BAN#*&bd zO0#E*Jbd_I=c=r{052{Oq2pmUwEkP=nW{Q#mwKLEUO--YUUgny9++-E8B-?{?d@J( zl=E0)P*wWM1m`HUb5+s;^#X-nx$)-Q90*zz`e}JrCh`Z{q`e;evekxrL1Xy%LxkAP zh8Z?hiNDwrKX()pyG~3*OiD~nOhx?gWt7(wGc1L+5ti9HZ?Z%$i%fb8sy~K&tU1Mb zb~mufG&%9GDJDcdE2}`m(ptF2y+KZS?9)h6lRBj6>C2ZU&loIYhqA}z_U>AeZcn=$ z8CjF6M4cJ{y$7`gTlt}Tv!wbA@lGZsVI9xhOWwSmPup+ti88h6g4_kE?=Q7@wV!BT z9R(DFn%Dvrf%$VjVF49;Q;~HQ++Cr7Eyh}<_E8gSdmctv&;;%?5xIrn7_xWWBo!g2 zYaX$M5rRlS;R%+0IM7siN1^T$$4?OZ{ZOTTTNsCMG^y^9-@em#wSuAXYMf2e4OtMeFihYDfsku{_o7;q;Y{*?(APYPwMX%tZv{~Omy%!F z*n>*0&47Po%xxw^eQvSlNgaE*5w` zG|G2m-uXSI`&CjHc@Zw`hWNfC@^f*`Fwr%nADGZnyBDh|$y6zdS#A2Ma1i>;k-p9I2Z*o<@3)aAf*^u{4k| zgY{H|bQtC8(h`V}u3x_n;GSH$B~MS!M)~=j`L@KVOOC@%Hcoyxu7mf&EuH<^k33YW zmBGQCb#G3Owx>&t+gz@G&-}QdXnSScpCS_7zfY=p6OLOgzf^O>bvhJ*$B+a=Az&jT zBgoB?fpKLO?v}A?NV?T`6`Nx}-+iaPv#Qt!urz*Wo342FtRdivPuDt!pJ*ByK5F@@ z2VAVlnuFTy2B!QvTng^Ks3t#Lc{w?wrRhem=^sCQ?7x1y7V7ciA z#YEGX$wsd$0I_fkf(ch45Cz3n`45n)P5>!(b#>L!Lgl7;@)@_{nC1+n7-@*o0I)c- zr17#wAEq83?_)X!<({F$9FC*N+`tz)_I}I6e+7;Wz)G}fBVS(GIXIZ);8P3yD%Rm~ zGV~FNh>3}biZU`Wfv2T;t&coQlx;21V^_^ZWSi#6w6tGdoVo3<{7@L3VlXAP9R1>G zZ+{AaUna)2ivY2eI@znSC2^F9KLZm4yCNAF=C?Oc22!Yn3U=dGPK$vBe;QB7vuD}7 z0P>^j2FTzT()5NQ4Gqn^cki5LGk}wS<$rv9JeaFI*v0R>1<61!ROJvzmOsve=U>V1 z-VKgZ*Qsy{(z@jl_gvU{E1950@)7q%@>NW-FB5PSag|{%lI7i`K(&$gu*eH}h6D$% z|05DbYe&q$x>QKtP0U)ZAQaCTsdvs759F@cHTK z{p%%@HXKq)(O?+)aXCE!XdLoX3HqvcgSxnH;(%cC zzRLl4DgqH@Gz_GX{feCH%@8=#9sn~8qEM(V3W#cqVhFWhVyxMlN7TZe=PN4ezT4@d zA5D8xF90yPu(04f<>Ie?NT23Dx3cm|Lt|-kbF$J}C*KjJ$^kF13W}3O0far528*7a z-szdDGuVpmEZlWeS+0pk@LY3q&5i^f(~6WQ`{H*Vtrz|+U&I)h{GFZGCS zi_3A_Mm;q(wP`WqRHyS4XM9auuw$4tMz}V0JJOrHa`XqpQv}! zw8u**ZKd=J3c{8|6sKe`W8b}-aOT}M+&;}vasHiR4EF(07-EJ-Qt2J$;^qVN(x`NO zpxSU(Hy`5h1_RCNqk$Gb?y6pVWzMcie?-iZO?kvtSNkMd*_mUR)4e+H>SOmN?(fLQ@M0-@!7#~jcOQGux#^GBMbOHc z07R>4AlKeJli_0FVMuw4ckZd{$rWH(>sG<=QCv6+|liE}p20 z2d1o@UI{pk92YX}8I{18vStNj+Re>zEY-Fo37Ai6s6b9sxYP7CEmw^+d- zGB9|1Bnd|OJF5NRv9wk%$HMGglS!9$749vWd*eX(y?3pCm3lW>m9Pxt%m_EC9^g(6zyP6o-+L z&EHW)6%Sz3!YOOERv$+XhkAgGUh%_0VmjKjdOr9<$g8VcpQfOA?V(oE)ArHxoTd}0 z5hCKxc0=+i+T0g>wCF8X*lNl71pJ_v(!9{w-${6P*Lgk`DI#-@PhYu>A2W2SJWuq- zCbk96?Evvl(d+HDNq z)pfSzol5`dc(-Wf{!~gn>m$KjYHL26?R`t1#;CFtl6#97u#a(BwHpfhUnMtDmnqKS zTW_{oa#mn9)niT5zogu0^;wU?@F;Uz_pz(&#fqumxCPMdcsJ+FXw|q&$^kcmm&3tX z>kLB!k0x6#Y|6hH_6{TqUV^((t({@@d+x)!ens6kXVydauJ)5*#TfsT(zLYMN};3g zTLM0#lVsFUJx}=TG>^8kK9ccVzT-=Gc-2%)X?wecYe-951Nw=*HQnm>=emVJZN8R< zYEy#)exLa-Zkx}%Xp4#zBnOftZo}~`tB2_w0_I!2 z;Ge2>i2fUCj(ecw8c*!H!8Ptp9jzK2B}uCyD(W**l=5v$1h|gX_8C?;o&XISgb)8& zZ0fgvd3;i~cP~LSo8Pcu|D}if7Gnl9J~}#+%knD4S@|^|iLICz%1_47$S)}6aB@Mi z&oPPq_khawd)F^}zh7m_;aQHnke1jO{2BBjUWuQ1hKmZI<`EGQ>57(Zp#~qQucupy zOB`Iz6Q*cjCZN{7IGFZO@>mS`7c9KCnX>4NeR7~3xyH1pba3b5^;x&xeM-tAMa$(| z&A^xZd2km-LDFTHS1|Ef^*!|;)M=Xx=`&sr`x}Y&bKR#X**+96|$rTl$7&5DYklD^$Le!utPkLS58_Du(C5=-F8l0~CX8^x@ zTb##gb>Z7{ZF}+zWREGy$jC@Z^-`qo-P~~$Zf^&#=V6iNn>Cs)g{b_nc*K0ic6f@}{L&gjS|qWSz|7t$Ds3d_2rOnfSy){Ney{oC4v&LSuUxu1Ci=xf5j9hiswv*7N|TL@JS!})VO8r658CZ3N7zo`WR~2% zy*-2l+ne+}I6M>r4o!civ~mhx3=oYJ4ql5b*AUiguytObZVWG|hNFn%V!E?pn3f}N zcBCq&o}-Gi1_KF&p-@zIwt6S4HfLt*Zf{yHK)t`NWwp==hdnUfTzQe25Ed$!9)w`1 z2Lb}bnqv?okjkwraA2e94~6vf=sCO*yvPS|z{*pC6uOtJdVHO&tw9MV z7e{5Gp-EUb$B~`AFYl$qwnp1_0U4>Z99?RUOp`g1lr;PFGY;1n9N~DbvNLu$z?mTt z^w9|QF5$~#$#81D5`Em9ygGx%R7S=m?sSX6rv~n`UrjoSW}Avvsxq8f!hD#L?o=dy zG0HW?KrXR>AodMqeqv`hO%f;jb9M{C_jZjOW&#NbCR4194(TUw?JAoJ=WV%@)~q|O zsOPzW2vYH8_%pc9`1ov9-lX!-fgzk5&Zm+(w){7{oB{JfE? z>|3bWo9l71R;X^(qN z>>R~WrM3^*!ntLN9Ti%SAsM+4`@warVx9wWQXYRM5+@yh&0@C## zO~g?$x=<&Wi|=;_D*$8Y&O<9N0S1_LDD(iJ%Pf#27nw{Wo}XZINkFtx@XMA49@?x0 zo5l+g#~*`M>We-)1kNPa=%ds;@`D{Dw{wk6Bo0S^e~D`4daR#1_uekwSf~if_2;Cy z;ld2>n_g}=H^(ai8O*ffcbc3<2!q!S>=2za83+483B|M*3(R_7P~%lo!HbU`n10Ue z^@W^CI<|SPS>L;rzTxMyMKU~2N#O|QT-fQk)7!pIwhugH7m2AyJ1=7`X#s{G$dB|? znUsWqd^U>XDn2A-c)-e5H>=Lr=DCMJ zT!lz@TKpcD068MaD3^w<&<30@ds5ySltE}e{F)nbm^<%cDe2JVOj6S zZCuxA^~1glTY7pL!?r6ykJI!l^=#88@+c74lfCF0=qmbHwx?EEVm(%JG`+2S))~g! zW$L-RU<2%f*HubhEjaQE@+`kZ84yvHIR~&mCN*~;u87f7c`GC=93*SphDXJNu zM|YR0kf)uT&OzvE8JLIOv?F7M{$icni!;sW+d%8Ls^vxr0vk_E(mL=dZqzHm%B&l~ z!EI9sGRf;yr&~I<2YgN&qagWCLZbI8W6;tog?lx{`PB1xHa(a$d;oo|-{3SB9#;zGZ5(*a*LZPGW4YP|Gg+Q9}YhLh1kJzuLswX$S8XJUe|+%hNU zDHfK0`~}E^jpx5?jEQ-o*GLV5j*X3nQT%6?8MVlsgBP)!&Vo1?uyrzRvI23XmiCe2 z>4YR_TCF}#*{^^KRP?a@?X>55d;A9yW5@aVveN1om&2MmIk|hXG&VK{-5{me{W~{} zgvWAZKU!IegNtBakDFG%)M(4=lEd$5mS+kjo!K3hma5`IQ<-k9A1SWDkdi!0oUXpF zU27x}hFFA$p9kGP-8AusZw%(14P=+mF*!l@n*4bhPE7G_w-0Lc*>~sC{EFEy;KlNA zVj_zSV-783Sl~&@&tc&!cht=fMus(!W>>X9i!;2G8qO1=5~!v@h3-p4t^yuj3mwRC zMxj*wh%i|UgmPP*1qDY{n!6`PM=$w`FJd(HA4$q_h0Tp?xN&4H70oC`t|%&K%NKXX zW?G#rzSKeO7jlD~rQ6wkBG~rSCh}5F#YV5TfrO+oZqjs2twswt7BBJP7uQQf#ZY|y zV{JGubx~C|zV(ONZin1yRBvJfs~J!z+$*5qv&Nn=bhNdv#Ee%xHo^KJ)+zdNs%S?~ zbF{bde8302SEd7FFs_W3?&EK4{f3f>C-S?xya84t>`vC}US3)nrRD@OwWgmA08$~t zsN;Du1a*=NN-KR^mYh7#X>G~>3FyaT2S8xZsd)24O^4%PyIfjHiEv*W_2<5_f3|J_ zF_0lPV{H+$MSH~M-jZT$T>*pB`bcqf?HK~3h7pJ`6*US1gnYrI%o7K<%gw5=5IJgr zfXv1?mRsP|6KagH$r!9m8qR<%W@!5G;TJF+;nWwt>?)fjg9_U3$2iB&j*QKt_GZiPfW_Qvu{1)b=j4vj1=E2c_}A9f3SJR_aKOSqw(q@ zacO^h`fRV)K;tG;wY4fwrc)307E!jxnFg2j*rx`0?AQ>5g{1XZ`DYy2xe6L#xo)tg z)wb6weN`5+V>ySw>SD)${Lds4d3fBI&YzcH9rtogD*`scJBeW4Qg zk`nfDeVpYCWZZ)YiM68UG&*uj_lp6UV)(t0RBT3k=~uSOo)QiQVPUx*o!}rVoy~42 zof)r=Bn?C2mwLM54^GAUS{-s$Co*__N`s4w%Llu@jwF9nJ`ON()4y@&(-Fh*YA$Ct zWW&$M41ZXn?HN?%l(WxdAA~Tc#T$7L{Geb%1&9vGpK0OlHW6}NVB(UZGuxwB`X4_b z9MPaRz*^SQ5WEA?22D>x50`VjkUdjB26cOz0CIy5G67FC_x2S~l&c*jZq8+or9F*= zzv%LA&xcDxE)V=R?Rzm|E;Q{ms$&R~F<|fF0Jsx+SUL6(xg?|PS-@0lSUbTX0@IAu zG@qq<4Nw_0g0jlOPXR@_PDH1ECerX6B82Y9z(dj<5U)xEo;vFu`!nF>mC^ zcIvqd3b=Z9GvBqJ_Tys~THQM9vi#0+g!2*w+z61J?cG||?I?N4wWpP=Du0lCflJnb_C_oFFYi0ET7#GXuia2U50EVyHcZctC zM5LZCsD(iOPAE6S;q7>dI`XnRItl5iu^%u;)_x7SM62z#8o3A@0=+Mz^@rqOjR<)0 z`!pBaUyG@Jsy_#Y1s+=prS&Sp8uO@2Yaq`kJiQU$e7Ox=;kp*z3u&^LPk-EV-Hpz) zYWceiPIy@38I68#pst1!ND%@4U-}(Ll8*UacAJ;~;dIK^uN-Y=LVM(2I|UW9 z+NzZLtx0T0j&wD+TZum+z6F5Z&z*0%M$9ReIo8t)haM*6uCg3)M$RpJws9bqHu>9O zFyXFt+=^s8NfFe9SQ4wC9GcKI;AX!tbCpgnEH3XWCK^*h&pe~qqy-}cSF#g3=rtB3 ziOKvVyNx`cP8`zLQ0~Ivy33KgztCgxW8LW-FY_eTzq(|jdPm;oc8e%TC~5ZX%7<3~ z##yJWkw@p1mtDdm^kh?3I3mpik{ew}=PjwjmlqcukAKg8|Nb2i+9PF}^Y7BVoAkN+ z#qAOHzlIw_W1|$i543bTOSz*C2UFOlzaP@5pPV+1zcM&GJ&i)F+JZDR6hai-l3XLQ zC3Tl#VO9W+o0WbMN1biIeVFn)Q2u@BR5;~1&3<}uiJozr@(H3KFu+&PYuWbP>-XLs zzr#vIRTa-SevlF7;3!K^_kKAlCa>1@RfLqIa zMdUOg%Lxq89VDsK;sD(MvciTqkXoOPy8(e|h-CipCbe*o7GN8}!@k&b;Xr`EKlZuO8?8mx4g49s*DI&f|HkroG#<^!I}j7I2f_$1qFh$@Q+ zQtHtPHWBRISK9kXZX)tJ%lp8KBLM>O`V%>%))4X+9H{R6i13-4!J>mtzMcprk@^>! zKey66PXo~r7A5@-<<4UbmvsKnwE@M0I^DR(VK3_Wo)&oxpxm@G!}y)@sc0O(SXJee z$c!2Q=nw6GQ||x7hFK>4Y!0zwqKDsrn|DFLLIZ}@wmmkUq zw+?vSYz|cRZM_#}8*%8IY%vzI{dsby%6_i>mm9QFG=lBj0q!&#dT97O5W5gZFmU+S%dmj^Af>?h{v8=| zgc=0B=b_dw_=`aHxEg;{O2HR#kO07AHUuUdS8di%iiLUxzDU>ugA5~50NE$MoiYYUERJ)z%)NI5M9~?AYCt3R1jg-tND|nM z8+Au_U^go4qDd3j-f@BL8)4>gZ?a1T5yZ28{2Eq21eX$k646jM9(XAd5L5tl8J$%q z1KBG(9_;{h4q`3&YrHOC%z(eSWYRlUKt=_ny}Xw<=ZTdtCbs60?FT@l{x7F7*vnR5D0NGF;!L7H@*0G?wF5$Ns-bN z4WZ;6=HVA=Wf?fe?3CWNi-S)IHAxMoo+u1!F*4EZ;ZLV{Fm)Ot%s)%mgCFwf{ z@b(yu^Y&CXvlbM}=CZqRxH;Zwb)MfU{^36Jalp1=X3 zTVrl-troyB)vkCWd9uGb&I3wi)DQh|DL^HFkkzlRSy^;?!gfI3m0l^dV&T9nKOsJR z1_^n{Z5AfCjbEjp$f&1aDKlIMQC60jkeZqb*q7)U1%dI;l6P||e}EJ{3Y8#HXFJ_+ z?FQ!8NUOFmswyi~RCM&-`N`fG4eEvS`SA{*$YvISEVi|+Z7-wFn@VO-e)O92Vt;iI zkBSe}7qB-n8F*cAeS7sB&ceWuF`03)PXX1?Xgx1{x7-s<%G%S_m1S@X(fEa?8o7(@ z8J+RMC&MZf ztyAbF)4e+~TMP^gSv}|H?oz=9o+sHRdJ>`GAC_UCIp5hsfcH0Jeo_7ISvPAlFAl7bMqqNO z8ewc?BohZJSPV^7f6LR%&lRK!x*wv<&_xwk^75VEzl;1M&rMpf77X*C+mrhetX^4U zr3J)yywX}eSmk(QcDB_0WEXH**SZooYynER$%V}C1Xd6fppAY6lwI}=E5)}qiA;Vg ziwFdR+!H-1X=x4)hLG%V%@Tc>f}9WctrnuLC{7C{8yl7j9$MPZQX1HdNk+?mQ{F>R zFyNB}ZtFx~7+Ts*+{ea1j+{jc@kuW@gMlJ^A9U~Y$d&iY2B;~*C+A!lR2KHwUr|v} z=?7ao?Y1qLgPnBQaCy33QhzvxjA26AhEfZ<4&|vR)E@!@!>UdYQ&PHPcdFpokB_%I zIy$!N_V9G#o^k2O3J3}^^i@_?=By$Rh}dU318pH8A!CKdTeaH|rV9-;(T9xfCUWG? z%G0EI4Cv`%#KI)}aLGcpILo>$8@*zp^!G@rt zEC9zqW?-Ju4>7C&EJb4QN)W;a?=36$6Vrw3KwA}?vK8=wEU;+Da2>#^&?>I`wF#iE znt#}hq9x5h;V5Kc@&`2&^$M3|Ne%VnfcZ7$X2GfQe1J6 zLssr<`Y)`PtMY&ebhVu$)l;xu!J@YeazC%Ur2nkq4tEvnw?u#YK4RGN!UEv0d|H+3 z=NR@i zr@|z&le7>WDnP#rPD4;Grt6@AThB0yf}QF_K90!0FBc4qzo~Z(_Q)7X)#DWddHo}! z9UH(&z(Z70{)`B($piFkki84&b9o7vdT86rDg8N`0#m~mF)R)kHpdtK2p=gRKl%$) zAApq}7KupEv+#U`_Lmap0r6CxLJ%1E$^zi9NnWjktUl+8=}n{tK5%h?$w9jkBAE-W zluaTY&`R>}uReno*U8MNKnvh55Tr~1Cycufvhn~MJkip@)3cil2E$ylG0U)`dI;+u z0p|VBtgvf*!RSpO@$Yht9SAaoIlS#N|N9<%SfLL{hWQ3Q`1=rCxoah!Xy-!x3E*E) zg#b$R?fb}9i<@@w->(SGpVt(n-vQT(3BP5O2=>S8xzz{bh8Ht}E&%iJ&wQ@WYA69- zDf+&+>|Qrws|GGr`Hh+`KhUY$ZB|&n26|5A-$CXEgvH>M%3WQ%$)t+Kd+2@9#?Uhr zM-@b^Ecm?xtdlXHE%SGW$LOIJMHk7<^83rv{Sf`XMMy>CP+;fKE|5JJ{^EC07VCZH z2Ii;&K$CC?8O{&ZeS6eW{R>(tcT6#k-d|q3d#4P5T8J)hJG)#}OS}S77$mbSg6>BS zV1!VrxuMXqR*$G9 z>xWv>_$kU>3{>0CGad?2wOj|Hgy}LJ`l$JZGKdSOm_|ZMKGrG+0b&d6&u`e;fonI} zX`Y$DJ$6Gacw8bO_t5EFUQ2XdUNJFr5hw$Q5K;U9o<8#I8(r{ju4aJ!sPj0Y$;TiOtp-$mEQC4w%x z_4;OO1|~w4-#+ehUDOSmFB`8aZpQ&w80uI8~>goVabytH=yVK-B?7D)4Yrxsjqfb9_AAzC^Kg z-zCFgdXN8`>yV5E<{nxmeY^T!RdcmI?TL!`s^NF}P?+&!rV=f|Ek*%4#C`WE;Kc&? z9QOV6@DBmva~-4k@_M-4qYD2`Yq(Za%t@wq2dP0&zNVS{KXr6 zIy$g4%cMH*ctCnn!E>~a2GXjP`d!{}$K`(!8UBwNI`q2~4)eJW1C^LQc3BM*6!s+Z z+6-jjt9SgEs2hxZCeK;=neUxvJh5EOPEsxHix*@gexOvu8sw0cTezqpw@=h!HE$+{ z5b&{nL@;1Y#k*(q(g_PU0&Y8~swW5x3J!i71!_zxxxwbd6@}VI^n&t?npS)K4L<}! zicWdds6!rz!o%Bw0|I2^m4n;bJup~sGE8TKKDZYdhHyewG_%iE1x$jYo(Ij`v_K3{|jCqN^1PxUZT=&)`7ovmY27!H}ybH z@#15G>~pT?uCVoyZA95eTG~UH$uY;%&GDmys;RXKv+%;CaZTVG)OZSh@;C)v5J`W3 z29Ps1ZQ%n<2~Ph5)p!Y5= zEX)Y}aJ&t(qy+kaEJjWbX*z(6*=zr4()*9fw7iY~s7zB(RD2aZU=&5u7ETZ7EGaN!5=6?Y{ZJ8UWl3LyGeA z9V*<}|4j_AR{J+G09}9viLgZt#23H4+-YM42}sw2H*76Epn?;AaI6xT3@!%+*zW@k z6etVjvM@3+8gTtR;|VI_o^y1B8+fjY>@if>OupHkirI}(e7l8!DS@In%i#jaV3KZ& zj=0S=7RrjRD`I>(kmFya22ToEfplWxtSDZ&?sZcqBwPc4@4r_SJaGOKP*&g{YZ#?u z`c6(%RF|jg9vqbx^p6L5=lzi_>CQOtfdQq0yt!f-XPGHH&xhy#oZKubl;P;2Uy zK6HO(`>#o7SUrpb;0$mB&^`ciyFUg__m;hE8K39@Mnu&@ez>N9i)U{}FEmHHna zSrAFMe||I6#xy?Bp&_G&r;c=*L|Z9q$GsKiiA^=?wQfvGX)2ZGrgn{i#UI0Ind4q%^tkFfu1U>k4E zU+Gp)WVOn<+-~E88UC46@r~C-`u|)Ka5!LvNr09J7T^yh@Rgmla1CDav9Ac%r4s&Y z<9Q`iw4DGH2DlG3oju@Gt9R~-KcoLKntjv@4d_nVp@J84q7w(8Y94rk&`Q^8if17d zjdXw=5LCMj@-1Cm$%5|S{|iEsMa^Re%dNG}D`=lO0S1k8WuFY}-mtj7h(dMDOW>>5 zChvzl2fJfp+EQ|N;}Aqu3;S2y)2NBDsNAZloJntVA+ znQpFuLfBrx+LSH|)_xc}2H?V&a9#U8f%Y*4qB0}!HFxKNkfJjLVEiE7*BHc(M454&)siZWLLrFJC;~-tq z4BarKbW3;rTkn1Mz4fo>TAamloa3&ucBo1N)_?6=y>K)a`4#Fp}-^O`a}ybP`97#&Emsml*L?(Jze{IPHB{JUI0 z!{q^6ptPcPRHa+jZp>>pBlk~HVBneknR;Tet{@_>(Q@K*x9>f#*ETsnKI9ByS%ATb zTK#!V1`(R)6&M1plq*4ufjUZ&Clk;!=0i>VsRj zYm95WD`}G^*^ANuc;-$LQLaf4R3P<-L0+wuUuBgtQ&?~?CJv7Ak}_LuvCRJ|Umml4@V04mgT*}G9PD8&_$|yV5*Dp=-IV#1DC53t0~Z_M zC5})Ge>S^H%@Vo82AEL#huKLYq#x?1e;?KuQW^#&7n@)?1o(-d{NID0DC=JtC3r%C zb+GV7&4dNOwn0NI$F9A8s!!R&=WJpiF=Z{XL$y{6xFXG>&{QbuNjK5|BYqX@1F z5}?*h@H}jvENfJeSWtc<^M7BJ!~w?)6X5rdgvsfb=Vl!<(kg>PU2Jsx-lZS4QD ztw6@F(J-nm{u`V_LD7}*C@{oF-XH^WtwRe4O-m!dxnnPKJY&UDzr40_df^D@S#^CNI4%zm*ay;N*;-G2!lA5C;vhLZDJ@n~KfmLzc| zvEp6Y>vD%gu#2`kr2X4q-1g0c-93k)z)#ZM;xu%W_(d6wM8rg($G76W2PzZPE>5v2 zFUnsP9H1zkKgfi0|Dq9g!3?tlY)3v^R@gc6ZIe_{M$>rZ{!ap20Edyr0()6-y zn6g3Y&=>rNt5gAr?K${-ww(#$aTq=4{`OEn^DCq>12@gl_uSphI z<*G73Wi8Z1kk&++>k9wdi#Re()1%+3;pX-An#{ z-pS>sJ-%uy588(u{t^U&Wjml+z!iP&Q~bE24R`@uIMdG7ecWl2m^v64^~yjfp7=~KLCf6Ms6Wc?Ebc4bOL z0``l)2R*brqhWa>H<_gR`Ck~!0dN=SUt)S=jdE|#G6F1@2l$V*C}K+(!J*N!jlVc^ z;+B(+AM}=;hj}bGRo8`x3fTJt7ROlzh`?5CbUq?t0F6>3I~KtJczzW;U7ssYGcmAI zIRhOcCrX-Y26+MCYL&qUlN+8dzJ7tnF#=Qqee-aafrC4z+V2@Z0W$}O_w9Ak5()XzHE=#>QOzd#o7t2VGiWLG&tk{mt#gt> zygnlJ)>&ZWHwgL=_`pR9HU`^(vMuEV?2~q;r@sC?*s4!O{+F(Z@6GAt+X#XuvP%Q* ztyN&b2Q2c^|KYk^WRZXqM3Amo{)e@&n&MXJ<+E2rauiZM13F88r>6V0lte*X^BLB0 zLynOcOHmchMog>|Gyhk#)B7Zz&!;;8JLS2{-2^tL68*+HU~Kb4$L&EN4h{}Bfq$#n z>qbmW|FvkN_g#~pAL{CR3ky<0!nQsD!R73^ZjB-O`mDjx4;athug%QNsNwYiZuATw zak;jBkl68tk4JStCbTXt3brqTP6Mhc^n<7;Tvbr{ z+Ax4L1Bensz|08Lt3(5nNCMYzxeziqw8ox>XLoB$PK9yQ9&{@qo6gVgbw)M+O|SA9 zOZlId@8;4a3DMb0G(OS=^msk0RVkqa4Bi4R+o0TgpeCt3dEsV$<~^J=G@_OaaLumv zO@LNXd0%s7f9|LyNd|1Y|7cSt(37@(#RRC5d5L2QCzqetjv$pcLW@tR(Q=#xDTNEmeraK)=TGiq8`m^3=qT3YrB41-xtjS9}(Xcla-xFpw&g zYf5Y766;r4$$JCbl1_3In5uk~&Lyeu3;8X_8276L0W3F7O6O-mpViG)%hEH@-%lSc zDIpV9!(l0-^Ld87IcFgOPwzbXt4?3gWm27n_-?J+&fXL{#o3ThU^ph!zcXEEK| zTcqCu(%>?1P*G5FOhNNo*+$yhiPhC^fb-EdnVdqGHNXA;cm-nx0_u!sz<^v|Ul$S(0J&mq(*c08I@>|UG_|h2fx4K0T(Q2QKrcff*PWMp zRL@?yXE@osxe2!!Dx!_;cQHx$hX}HqZ|VKN>0tgTC%|ZB)$0Ec{9WUx6X5Y9RJ$+6 z_1q#U=X$HCOa}_(q2VJSC!YaD0rGAji9Z3xQ^PntBXS7A*N&g0MAub-4TM(P_8rMy z*8MMK>x)@jRZ zWv%$V73((eFl#JBV+AVqmn~PUp$-lQU@l^o%DYZ{-#nmTS*GOjg224j4t6dkMW|3v zLcWP^2zX2EC(kni3fhgCldWvttR8`dp}3hY1$3{@-wZJTgg&(mw2CSlfBifsg&|}I zr84<_k0q^aH8n^~qd9_$q*TWBSMp^XA|Ab!Sl0!}EwDOrboVKVCjy<8@;e8R?B%#d zO=Mp5Xb2qjReykAlu%&E)~Tk-@*Z6=imX!r2hUd^EC zz7%v!f^cibO`{zJpeRC!wOre(?w1hajA8#R9g$%W@I;%kj`j5P0G`>Q(7JY(MY|D0 z#w(_sggfay%ZP-kUeAOx`H&z$>eE1IEJlweF)nUQ<_*3OHMNh;vo3BV-)cI}DnenI~+-^N(m z^PXpVBqStKNWc!~zWmw`DAHAtyK5fBA)E7WTt?=kSkb@|ekP{FGc#>f&=^*{)(F?3 zx0@>Wop@*JqRRtz8;20OqwzjjgBP5Igd0Hgnsd6#)o2K!3 z2*2Yt<^RX~efhtVjP=Kiv()~N)P^*UuvMjaMptt7Q}J37&6ST1_Qf)*O$j|LNX}Fk z(-tm;f~|k)1KhwNrLnIQ&QDz^L!pEu$j&11t>(>eeoTVyV4_#hWmS9QKu&#mzyy0J{i~QL;db` zb4e+9d{~-pENt5M4P+W#@3%ctz|n^yRg)jev(x2&EKAWu_(w)Y*4EahRLba`10q}X zOhuEKxeFZcL(fpN6)JsN+*%6zBQ`Uwg{2$Wu7L_jC5t{Lr-aDvm%M`s5Dy?}P#|%s zDJcqfwA!N-69YIPq=r(+b+*FntqoG2cLf^;6E2tysp$7!j4?#R+y59-f|ntDb50_6FM*f#l3pGPXd_mLS2>QVD5E zdZbUt)bs)PUBNo?o*XC@tF3|alBl?#)EW%Av{xVDiphLGA)AgMY!lYQz^q1*6c^Vq z0PgUJZ3x6xe*XM9R6jA1Hwh`HCm(K-W2>K90;6ZsH3KCGuy8r9Ft)L2bX@JZyV>*p zN$MykOMm}CyFXN5-0jr@m@&X=Yei2_pQKi$)7dbI7HapdcsC1hA5-W(ny7$>lc_9R zZ*#^d?u^gJ;`^QZtJakVY6wKqAQ<60*O3ZuB}qJ}eNm!)cwUWa@)^s#N}*$Tv>Dht z0Ydj8HwOzi2FA`>Izi4sX9Okdg*>xM!^eW2AD~N3uLfW1OWdPc3=)j`HWICB%lBt^0T=QUk21QH8p**_wl;DSf>K^fbM9z zqxC^X>2gX#bCkRmgU0zldV-2M#>S{QFO{vDL%87UIO}6s+W66h0@{A@+$uCrn)+qN zf<&efF?FTfm^DEIKLGidWW>Ohf`KaPC0q@}c;;*rHwlwD#en4lOdpF!gCYjCjwZWO z%$Dn+^>&SQE_vUzEj?|+Prp`n#U+d$ScDH~t&WL|Az!I$$}4O4#E*Z1>$6E%-Qj1c z0Az@7jJwAdn9`=}`!H}(ZY~3%0?bqD=?$98Y|kVG)l547bC1EhS|4FMrB>-0A) z<D*U)M;B(*TcmOmD0AT$tCm#u9RAb|CK%ZeAqk6O&!a>zjOn{LeggWRh zKNHiulKjsg_$if%*xXZ(Udx2*{P%vLpm@OSTl!S#5x5_VYa->yjIV8OrnRT)>DR6G z5og10QcGl4m)W1$T2&4G?3=6PqS<{gT3H~tD=!a0D?cQV;VC4N&5&LI53!gRDF$ow#4MJ$v1!}#!@M+u8kXH z!VIXJRSx@uM#L@Cz$ex%(q9aK0E8)mNi=4|5|7{>E2SGD=QZo zA`#k?K4lOR${-7ovouc(Y>o|0aZ+!1|1Il#SNj*vy}VAvc2agcl>4EB>YUXSXY?vSpv_IWHE3BPJfLt2$Io?bOwL9NPeb~7@lDbHvi$r6DO zXj@C{^o3ST@oe(CkzVNIEI31@{kv%6UUsH(Q?*~)x5&ZlrS1?10h zJ{<8;%2x$cGH_mt;MnnAT-7*@DeJ}WOf-cf0c{jc$i6RzwE^}k1}@Id?OKd)(_SdM zzb6WC_*Pge-Tkzl())$ki0T#w%o^88wGk11jWpn&8JBk?%sSM5S-inhpYhI?X7)l0 zJE^3wo=h zowubQxA{>zW$nfKGAtQi!(vITV9=3-sF%%O=+07H+^@d=irlWDg=gHri6?1Ha}eHj zS?Fn(4oOyMDo7y3@~uT*c#|g_qZr&Ha=_g6w*>ox&6f63G1HdsLQ8^9=ZB6x%CjKupBPN#^V;@Hp~9E_Yg$!9SSTDvhPru3d-_k<+8G*vkDw1Bzh+`UYv_ zd2!7RMh;d%x3Pv$X75u~8(E?C>X7*nc~}Ws5nEj+&k^r8*;?Oj;}XGjih?S2dnZP& zTZowPkt;0 z8f^~WgjKH&qcM*dX)G6H_zN~|N64F`l;N)o+O5(VV{kglYPMOnDoqCAjX1~D%*st> zE}P8z`>_sxZrPx_uC^Z+hYJxYl~Ed8pxHXv;@N9@e_F&uUFxY$ukg2AH_4cS!f7z% zRz9L9{f=dyIzCGC^S=+XW}61hoG=mKaP2dZ2rIC}Z5C-YdEIN0R$yYO=(gXWWuJKO z4Xi|x6-lP`yF%3QtGYefQe1vYMg*-I!iV-Z)?-uEj}}YQ0xGW^VAi`yJGg`e822Tf zl&U@&9wKAlP76HHhQ-x~pryHJ^{844Z{)-nTTQ-UEWn_qLGE}^h(QWIN4~Y%8;#jE zzQz@QQTs5*@-Q@fM`pA1k!%%vaZ{b9Txhq%iAL9bF;Df=Ry+eb@fmr@G+ypYk8w_L<{x_Ir^%cmYAyaU%KO=s%pt_Ps5* zBl>>Pc>UD^HGj!~;SU3yRVWyJ!OwLQp|hBnK1IpHn^N|=PxDn>31HTgi94e&DkUeE z1jA6!jQWGR+&oAc}fKN27fq?w-!FwX&w4%bp~;EMgJA{X7hZVi-8VdTZnkA zh>F!wMT(beN^!`>>m&x^MLz3%7WrIkxAm()wvqAP`+T!fu4B47($x5?O)S9*P|9-f z%;u!!`Q|>l4I8#Xo96e*F|$X!Fa@Jj>^Hs%W8-CJMgScrgEjBY?RYY}r}A+Wr)<@q z;#aHwJKOC87d8<#!Z^-?YgJKo!61XA?DwHXS)~J|6nEwuvHamy&4?YDKl!(TsTIdH zSB9TRab>F_imNt~c4kI>o2RPi9@j-=GFx-w>Ug^B?)pDBcLP(_xvA_7fp>wcd{??# z2+mmztaz3xR%7U5Jk?mFLGQt8VGqj-(ICMtH!`+*uB!h15lRe&inmWOARFNc({$Z) zbPq)Kl={x_m_#}01#ytrhIyWKZ&qM8%l=6H6*16_Sp8I( z+9l8&RWE(0D4k7Co)(u+!Md-VYAu|0?@j60=#EG&ez|z#E3_W^dY0|J;o*vQE1GFs zkw34CUfSDi{$Z<@Ty%@X>PBc|KuY(f(AWp7z{omI3@~$7{@Iv2+jwP=so-MHFKil~8c)_g| zIr{e<8RO&Q=q1x9>b z(6RrL>k*ajMR_{f0XX&r7EOaO{e^Npp(KlNTH+Blw9(1z-U;fq7JuM99Ykh*%|lvg z_|dCoxu4FN&#XheFE=p=85-nosCk7^Bs$$M80qOrOD)rn2p5zk6(ktd{U~UfIIUzj zKh4|?d)R*YhF~X>-B?Ny`OGC+$o!1*#>M@dYzu>#Apy$x^j>^i9H>3% zblb$T6T`>Rg0W&_LW8Fwf~IR;to;A(!Yj$7UiTT>g{rpmW~HGFZLJMBnb1&L0D>7B z8VY3DxJ*M{y4t9&x(h!-l<31HDi=vmG*RErqH)FOHTrw9jAVt^7{s%u3V7eiS+g8ZL9dpVQ`KnXs+#-*h%Ke%WXg&swoJ!h-OB{c*9vzEtuj%o^P-6oGGZz8fL zN^k6HVTv9f(v9#5SFUU*KqWrDYQ(>q6=vc5(|cuArj1K!Nkw|mJXuO_h&^yqtR3@i z3cQZtVZcJU$e+ow!3@zd5VZH4)1JMl&VepqPz!eC9O|{Uw>O?EzaCD0!>%WHPXYSV z{Kp5sGJ><~{&UU@)=zro?(Q#nF2yNS^z>|)U^c}&ZyH7lHF|&lmdt1a`6s~pT24@o zW>}^xQ2wd%EdWKgwj5)Zv~30M->rjL!gQuv#eB3{#iYwDs-O;c z$ZYz2hFKo~g&Ta@l)R_afWI!=RQquo#KIh2$UXkz=zY2|TmX{0FD{kLl0TT7RDWwd zr>J?n;EQwqBzU&otxN{e)8F4Q1U4~|x911UXdUeA;>0)i3~{A$a;(=nD`p+bL3H6M zzV_OJ!*WWVl8XillA)L6H5!)+B0foCIo=vUg{n|)1S6hc)PG*#8rK@er)MNrGWPX2|TPOe_$c>Y3y zx8eoH$yUIz9in`v}@qi~{WGu#7?Kd{nmnh?}H#cXL`HJMd z9s@noCFF>}M`Xv-hpQF0Jv{cVBAuQPBm^mVDjlg*X<>A`7?cg+e?rpb`yeqec+ZXR+lMOcZ30o+&$1U5=!4Fv}B1b_V+ipFo7^V59T@al9GD5g>5T7XXE zCT>Z)iUNHFIL*uL7CxDOve`YjzBaF0yF{#gd+(!Oa;urvMO(LX zuxj?9oUaX}>gpun81GH}_yS3WK+k+ZTT27z;&-Z$n{oUx+dIw@n;E{S_ix6!%;c_4 zZ4w1=@m}?^Vfdadb)+#Bg6?KQt1n_hN)3|0tckDW4RJ%BJ>wA|ZNZb2h0DTEXlJW! zbA`S2yKP04!@~Q_{AVl^=)B0TEbh~~UDG#38IzaO#XrK*vA=eoiWDjJI#R5s5#P!2 zQ3ADBv=#KALD-%O@JnQl=GWi%0F0ABtq5@N7{~nk zG5-6DF(DJ;Z+*c9C@H`Xt~$K=qVsI2<9uyY`s5_POrKczBg}4t5o$&`6B{Z6523dF zEZ&y5JsH!}|(eirE$i>rdU<=2T=!}QQ4T3HRSvBtNgPb(^DNlqk%1|qsmyra-K=Cu zrk`vK_aiTCUUZJYXJGU^KPN#|dcC~4w{q{haQgh@2X zac0YJ*2L?BT~*e|$LH~{RCmZpNM6~d5cZ^ScLdmk%|6L;bvug&&xB8Cm7PHZ8%QrO zG0^I1wkbC5^%64bJpcBMdd*BjBk?8n#{F9Q3Z{$Ad^RwpQN8X{8?%eQ5Iiz9a#mf9?|(}9vwr-fXx_Jr&mbtNM5WCuk0rXm*o??7uG;c z3i3G|o#>FJqE@au?|fsh`WBPIjO^_4z!2O!NIiZR3b@>dFBDI3h&CxkvJ*jZWrkc5$ zavRE+#G^O|uaVk)iu>b|wO)w*#1G@FjdQm(;GOl|YQhU^1^F4xcTck|XWk*=Z=SSq zX-$4mAm1m&U1K6;s&!=}aA#qf%Kwg}u#Xk){oXrK%N?zba&rNh3N$o{zwv zQS{yV0-rh>0v*LK zvCc0~j%I#xrN8=!!}F)DY3ISh!ov1)eM48U_e>51x&_6#H~eZ1*UYYw z7e4_l0+yXMf%|lbpVpA=h`39|>Cq)*@DRM=tPaS35u!h=B_T)&{9OVxVZYW|hmN^j zINRceY8oM(&QfqEgq+c0OI<7MxjlFriAmzlqNw5!Z_TM;M$4Z7m4{A`B(S1YKY!Tq z`MNk`_sB4Xa#hxasx#~Z|8kT~G$;z%w}?kO%dG~DM4>0I}ph-ofs zw?13-n)R zks`bfA>LEv{B_QoVao>dtd+OGZbAS1r*8cE zR3TLLEzosLyQ+=$5;4_OR+hIqM2Pj0PGVxxnh4nzoy~|E^unKpVcCLd8t=_y((W9v*nBWCLGP;ga6OY(k-cVLYet$*PWIxcA(3Fg3-ZN0nOec7yf5)(mV*vt39WF668b-uKftm5n*odD7SjZ*t|1RV(>wlM2pAmET2C3Q_AI!Q@ zhR>m8tmeHtY)%2@-*{QL3dSbVyZLhK>%p@0;euXs$bk$zTpS*P{X%7amcIT1^3aM_ z;UxKKBxD8?l3W8|q7R7D!k(SUvdA2sZR;><*{A#pA5C{hj0|7FcGjqop_pR2)a)D5 z@N{7zHSNTfnxbt^FvEdo_Ng?C6Ftvq&3$LSG1MW-KY)3#SnrcUm^5sT0s>_h4aRPN zaPu|{4TA#?-)hNuz?t;?lMnMt^c-%y<6b=U+6vcAF0Qnek4Rs8E?Pf6^40cW$iFY~ zUMo4~sD6==cXb&M*ilO)pwpNb&aSr)dF{f|(+%Hnz(N0S3H^?+GZ#V#F2uk2%DF1) zb4?pWKVkc9u8Ky(j!!bSKQafs$Juf!%p!B@2gllRqPw2C$-)Py~o&`pTUGko7%=JH3l8{J(cv`c=8~Eii_91a`A?5Oc1SjkWH{ zTx;mOp?t;B^o>dT=P=}<8k*Xzc0Jii!qdUu*T178AMjC;l^VAJTX( zH@eDg632-k{9oD{BP{_6twZwW)luF&uck(>< z-j~mFAL9265tVoK9Sl5Iq9cE{%1V#v;9HXcLskD_6NKG)@%=ecWKVXb`FyOVVuP2< zDxr#SnKXXgo$c{YM+M&Dc zZFZi;%TGqdKGB^fc|^Y}y9=Yn(ShGl1hPwJgo~m&`0D!6RBaGzwY+(h3r>I7J@DYz zyr9R0g7hHQ)?3{{VXRAW$|j8S`3NTDN>p3YBVUCyLM?rMkd8gds{QR}jt`o~Ma;A zeBLAZM&;#RFMyF)cnsgjI65**E8w~louA)lF{F0&e9Jj%@RYJ`i&weEZBJhU|D5%4 zh@+F!ea>Hf`1Hvix)5!N7(QZXq z2>j*Y+uPeT`%xEtZKPu9ajkB74`(9rwZxT5r;LPNcHpMdd?Tl3o`BK4K5fDx6uoB} z$b`HbjOgo|+W+p!lfclxtI<#z&-tdbFCbOwq12~`9+Tfi@p*DRjnNkB^4 zWcBvAbN32WJeYFjS|IDOXrDXYy`!TeQe7!Y$;?@xc2@VXR8EWvQ}F%q4(czCX=({zApPFVmSh*^b9ON7x=F z!cG9*=$OEBU((dn)Y7t02GVLm6Aub<|Jl8V87RLvf1pyMQIm%26AE=ZE8z1~W`-1r z0?Rx*cA~K{7S8@XnR~=W#8!qFi4hMC^Y~s6)Mewf@6-S9v*jm$KO4X_JjEade-eG4 l%JTjF 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 0000000000000000000000000000000000000000..e673d23ff3d949077aa729c03105077e938ba8f3 GIT binary patch literal 30867 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 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 0000000000000000000000000000000000000000..ba4cc71c6e7f4965b17d6b9a2793434190a1025c GIT binary patch literal 33185 zcmcG#cQ~9;_coeDL_&}Vq6^U(qW2P__Z}li^bx&x5kd6cyNuo$L=U3((FvmW-uv0| zzQ6BW=exeY&UKFK$p~hiXYaMwT6?YgzIT9bITP@*{)O__)#3 zbTZoN?fj_R#>p37qpvRbn8Gd6`+_P{c}-6EU#Ic26k^NHeA1h9_os5fFm$JW`5rBV z6HUCEF=O<#XP8>WVsu4TB;N8q1(ti<$CpszdugnWAv&2GK6c& zE?K*TsbMvmW9+4BbQ3|_S3A4c*R%%trDy4}@usBi4=b~qOg*#d9QI7S?mVO{%T?x) zQz-Xj+bmnFZV@iZaJ43jBaskoF+$~LA{)42>I*{jJUUZ-xc%Cn_wIRpk$(49&3S4o1>-%g`fZ1@^1eoHw4t%7 zncA4FabRHHTRMqZwwO$gwuS914qZ7l^xJG{X=mwZWSra29*gJUb%TMT%r?r;P=`jJ zMWjDde*5 z-_OpR?d5jPL{d=(cgDF32Y6x@tY8^HM z0|VhIFb4+*1B2JL*mtAYrg--F>6BQBZbRes$#h{MlZvLexVVlE=?E$q)E6#OQ`0x> z?Chqr1|6XxAt87{ZH3k0QFqTWP5$*u#0RSkHcS&C_1hJr8UJ25{yid-=C7!vQL#lu z7E)4=Hz9xK9)QV+ECv&>W*Q)gxGg`3h=_ojnwy(zXvC^9h|^O1``FF3eY7N*3l$w5 z-J3T`ubxrS(y}+>AH295qfYc#IE5Hpuf7i?;GK%HviH@)yLpnxa`d{6!7UUmg$X+v;F-Fqy`Q9?Zk2&uLv>3X77VHuaN=E-yD~CVG?mjwGIFN zJXZMp1~U%>`z6ZV0trfGAqxu&#r&~~KGYmcnxuK0&HEE-Vv_P5EywBIto;b1%hY#6 zB{cX%M0+>vTC~6@Iw($18)XC?71g(I@1QsYa+Kmvj6S;Fy{YBHf8L~`ATEx&31L%} z9Zzefid0R1dLKI-b?ROT&3$0M^y%D!0)qxTVq!*9m#lsG-@kuf5w0mB@vgCTA3Onz zFT#zcU07XSULF}0B@I8PMV&apEa-=@DrI{JgX_NVEeZtu7$+T4vJ|y!xDJdx@ zCufoW0Wj!aFE}+L9@KNItV0NRcxEPLWJilHhIT*>aNV)7leF6_+R3c4va->RADNlx z=;)@`{JZrP_UkGxurB7?6K+pWPmhbEuFlT%7(;$q+ML?jT93;U!P~3Dv(URmRHBBU zp`jU{|84eB6xjb(Zw__9X4tx(Zt}X#RsNwS8${Z9i71b%5lvS$HVDxzH9A6B7d>+y@O?gQ6%qJA1Oi zL{({YaPXCz%-Gl%!p>Ogx-W^_>d#N_k|)3`zl6#bl$Kg=O_aTs3^n}9FnJ68X`K-f zF$6}AO~UiWeoe`k$*<)a*l7S)?Ra-??#wEdMNeT<+SzWke|xlGTvbt!(|qDh+56!C z-0)VWz(7q+jV?JkFFV^$%j@ty>9mW7$93iB@jcXkv*ZM3`hOi0|6jz|?oaxLd{Jo8 z&C7fL_L=0#4Lv7~d?n~;0;-YcP$%VGeB1k894@nFI~G>Iheec`Ue8Ps|Mw5ym!9mF z&+5aErxjhEOK7-!_6X;_o6s754)->#W?N_}t<}&2LfHwo#L_rw)KCB2_<}Cw`&wpI zU!>{C%u>RXgYg7^)7UlhEj_(U7`0MkNO^dH!AbEEOBTT}FBnND{=Z>F@Q+r0C5s|7 z2=Hr+JwUp3zWDd+xRJa90$7`H~mB)JtY`)LRlw6^Ls&5Z;}E4M(eFMRZCg;E&(#EHcwOx_ye{tv=749DFylrPjfWY-Nr!j$A^)GT zj%-%jbj?vWA z9G{r@j)MxO!_i59Dri_}$KUWNJv2-B04vCE@?GKtZnm+8nwp5t-E36Vew##=9gNks z3Z_Vp2ffH7uM8mJWZ5+gRbwbBF7~Si<`Wr@GAv{A?A<)wxp}Z}&!!{)l=_-nw@jHb zB04s7c zyorgKI>#b|SgYAyB7w6Er`ZH;ZMWc96qM@dn6`a&D8$dx^(M?<)z{aTnugMlsj$l-_SHanbVMxd*-M+oqo!#>Ky-3&bfx6Eia@in z8aGKJohi|q8~Q;W_I;wc_TA)jP=;~3pS{k`7OOTtmvX!|NrQN8vA$rhn0GrVj8hMa z8g5}}Jo_>etUyoSrKt-gov0|cH|yYzF7Te)#>T=54GaX;2PmT+SKY0+NnUqn*2Xck zR>Xlv6zaNDlXJ4;K)bKhYrn5AaHw}g+qIr#HI^zsD4KMy{TUP3P zk&}jnk1sKpcsiFRNlxYUp(&NV&JHPEqATdMb%uab(eS#ESCj?5c6H_vyhdn2r%pxZ z$9*1QJ_~T4U?=1|0*w+T!e6}r|7dNR(lKF}ga><*6+;O3)lj&TRV%sYS#m5fv7!o7rE%Z1S08NT z2xIh^+yn8h(JdU=;8vq{sTy*-6ctwX;|&p!XZF3GcH{CyN%;2qcieW?q`s8TmG*g% zijcDXSKQU7hWz^zXL6)42lIh$oMn@q_fNyDq@WZO6rgHjWGu+ce6Wmp7s+DKa&Av4 zTHJd>qBb^&!h3X+QwlE2F;CF5+Tgu%w-K-9FHgBMQ7;aA6X>RVs;dhUB@L_6-ZZ<9 z6~@s=2s1&_tP;f6-IXC3*e!5^lG5=FJ++Znk04wX zvZ<q0Fz398#~C&;I9T2g)PvNRo~w(Kr8LwL zP3pQipZXk&g=H7N92|_&U5mzFY9e(PCe{+rXpQ1zVaKgbgN`fdk9r=*tml7!2Z*dR zVUZnejhF0>7UUt4lZf&0t5VF|p|Mh;(%6L?H`lvPA;A~l-wDafH!1xT8%o-EU98un zzxUVQ^JZk`;7NOt&L{<2qqT4X12S1{`-sPX*xC@zi; z3rKMe{~9yrDH}HzT%}xv=DWT#lN|JNaeYo>)9aeEtVU}e(KKG11v4E8#|LG=-$+C_ zxYJ!F{r0~V_P@$|kaLHdGfpyFPc~}s0UY&TrH!Sug-7LTe-c1Vj5mdW{EE2SFD;c* zw4Mq6_RAmoep5nHaaz_7puyXGag$~TFqWl;94lIoekK;G4#q#0$kTo&C78S}XL za;=vNL2t@&2DI!rDGHXROJlVT!a2gfF^>H+ZaOIk_n*8z7Wyzb;fhh=&y(5lk|r3g zayM3OQwAy{Vil$thO43ZPw%T}QgkuYjg3ZaMVxBPeg2XSDLRPBis@`};S|+L{#19p(D#QTty9coWl>0ZmHFoz&SZspK>ouGrK zuOPp;rMN(?LS6Te7Qe0|v^3}aMts9&Leb4Qk}4n4<{leTjsE9EiIYg!jAC2fkyuKW zA1T96a$BLOw30cn&{0n)xWs8@l5UP7g|YY0%tsF&zEry z-NnsGT))TWB#^5LpGcvr)Fw~vrY7%(3!X&{^H_Vnu!hfdBR`)`_%$^aTcS}el(-?h zg$dUdI7h#j1EH@HOrzjOa1niS=2|{YU3g|z|1Pgph+^{ORje7}bY!n_I8@*oKS7R& zK=CaBJS#x$Xqn5{Iy-$;&w$fcLSv|P)fh58l0S3@c>H{*{t4I;C&!}f_10nsQ|9*# zXd+zp9reH@X@((FHWudY=TY0Bf&s)@^H_MSA0?#um*&mom$zYERFJ?_*;5H6v5dZA z9Z-p-i;bkQ2(OYRep(cLi+3%Hr-fE~GvZHZ;ynL%K6!MS@=<+@P!8TFjt0}i`%<#1 zGd^i<{!<>E_D}t=JvoiHGLeF!2O1Z0CJ_M^!Fegp{X}NjN}ZQ(3apfGz|?2HF3<*M z=JscP4_sgfEM`~;`<$DfIiq2MAC9q3CygH}+DdOf2s|w4W>;kC7ol9MuuZ*~!Lfrq zwwe2@_G&j&#fc|wa#-Bue9JpMo{}M>mC@%%q^v|gR%C(cdQmq`lX|}5ni=NCuMmL7 z)-*hEtA)gbB|ipPsF60g;nh$o_d)_b#y9$fR@0nt<6)K5=SwojqmNg#+O2MiSuruvliiKK{kn>R#vvD>n+jO>ojj!WHXfLZmn5Se}j&JZ|r#dy^d@K0p}OG ztIXJEyiBd_zTGG>=pnN&P{Z;6(vC1^6H&E~FneTIaJ*?eJS~W)4Wj?>8nr#NG>n8t zOu$5bXaxlrXPd}Vf|W2ueX>!?*Z=#QM2){yPrws#Pm$VRhpgzL@rMi7BZqv7c0QD2 z{dg2eQ=5Jtj_UkCT?8~bd9jRLj9_|vlPg9vW|{3g+e)T9zCBSY)GTu}-nhH}1PpQXY+iRFu!Q1H;wI)N!Te(vdt4nThh1T#gvVQE{uVwGy zk5LKytLS#ds))vB=c9uU|0sg?1#u*dFUf^%9g{9rzO1@I5Ek9xlF(&r(H3TCwZ!IKN*&KtSMZJ$4Ih&oxvz|J@4Uw?QWz z~gDWN5R~c;y+xR=CKk$KdOK z%2bs+HEs+pk4vz~Ie$D7^7v{?orpxBUx;fdX_eWlmXA-e9n~-fMHBQ8p>t1jG`LW; z^M*-wW;^{n`N9L)WABbFSm=Uamm*%%Rlf_M&pP<#+y6y9m(JBYs>HG13Kg{Pf+PRQSqn#p+a-EWpQ zQ=SOmi3|)>lE}<#W~=q}4Q_~ow_Vw8F{7ktaTZ4Sggq}!scD>IV9JXpjnuzfZy@vI zbCqlwe^E?%>35{2Xs3UWr=&DB$D#RtvnWu$42HtgZln!xLq5r1NnzMa7U}5p2#jPZ zX*!r>$th8DBuARnM8=+a5~Q_c2xTWh zBqzHtVzLxCm>mv<(M=Dj8tThpfe~)yT5Ed5JvQkbLaj>zrw*EEu&=Kve-mVbGR;9>{Ums;Rb2mqKtwy`PX?~M@ zTfm4qSwjD!3YA+)9LLxSf(aB(8;+LIJ0Y*w@!U{{*kJ4BuY6nRjc2(q8!iLIW+SDj zy1Kdm04(?P?Wm2C)3fqVZt>?|U(NWj`2$!Yusnz@q+DV4Jrn1o?~@hT@>q)n4@IIE zoUN!^6Wi zebzQi%82jzi+eliS&}<7+^r78-6Y+V-E{wE!DrO2|HB2nhAwosC2o9;&3qU2@T+n8 zOHZS4d{fCU>WFiZ;5?n3j@i4NQj=PX$MM{a$mMSlfGGn5(E|x`@%y{GGdIpmox=at z@Vib0{;0(?!UDJnuF*>)BBK;^gEA#Sh2=ejf0}LxsY$z~QeglWB^-4so2KrK=TXZ9 z?W#C-SpexLukM*RFQ0pLA9Qdzl*HwR#9&T)hvT=>I!hY)(@HB!_{tJ_d5p1O!v$f8 zd$c*VqznDx>00^{jvB3^=eWFfg#FD=^>uI|mBO?$*I9gr6v`s~tCdsE)I+Q+n&{bB z>A(gM+e%n)ts>@0YLPTFc-HtID>LRWImNVfrZrw($EAANw~}w3#@XTVjhfas^?rs$ z5~$W88=hZW=|oi}DAyhNG%!guE8-!M6@we@F5c6Y7x$?`oxZQ#cAsA>cfMd&L7d;C z_V|fRLKc8B23KRl#i);iBW$t`8OA+sgvPf=0l85Ho=LwCGG|c3R|Cn3%`O6nRL~~J z=v+Uvir#3!R<2Ot4?{_2iBP|}kdah^9*19#7}R~yayuka=j$13Q0{kj#cxXRn2(JQ zU;MC1947O;cn$Gm*S^jp*gV@bM@So~aMcKOPe2zCnBFhpZD>-my#2bpi>JN6@eK<> zal~f+;mr%8@lUhKc~+VpprV_2esXc{T*pRK@Hyc*<53+=2!+@yh=OPzvvDoES#X6Q z10vjTUMhsGp6@<#fq6M%eFRHvz>#rH(u1BO!t-yyBB|6iLE(LvjkG4NY+L+P%oa{3 z{)a$BE<|Gs?;F!s#7`{3AQ_$}T86DR>V!3Nm?D@O_PHX^THW{T<{uV1m>6b=hw@V# zfEW9|KTaaP4{xIYWxk7Wv!zL{uwLqBIm*UYgne{v1rh*}aK`+$rMers+RQX=el7mL zXd@yuu{1-gBZ+=zjGD6P^wD$t}zp?E^n2p5x4D(Ey?G8=Rp9d`a3B1SaL z?oURa#&wHt^Z+xAr@Yu$oKi0G$1OEe9k0Tznt2%hg`5HAs+7e1=ROd=rHB;o)K9g&f#{NQ3-Aj~GOnEus!Iu@P&1YHJL z+})IAgiWKO${X=Tf5gkX^E!J4xTN##fy1kyUmtPU-cAm96IqY^71KbxlnX_LIV6M8 zZ(z@Zjw|ZZ;#bLX(Arsp`rS}JNTCHw$w@>+UXK8-Q7r>pN~{RK4{6Ka3iWiO043AqIT((FhKlPwJZR7ifBZ z7(7Gj>!Y%;lx%Hn)j93X*4X8gl|=*)oAv?xQ@oPuAn-}m1i!X<=#Z_Ajg5*5@_2K+ zGRgq(>_WCWb0TygN-badA1%l8vhstOJRN8AB!Bh^Av_ zuBl1CEig|t$8w^y8B_o^(s8Tr3wm}-5te^`mZ|tXc|k)!K(Mr8W)(OD{6(SWQWW}k zHR|ha2KA1dS>j#(W31j&yNuD92x3AYWvtep|BH#atF1v;QQp8i?XJ>*_j`)7+`{Cs?7 z>?p(qpYM{jdL;^sep$msJ-V#YJ*|#S#_>sf8u=H5Cm@)CMnD4@xy`9x8)KRQC?@Ed z+9cqo*Vk|e|3#><^zox0?d6f|ryIdzu;VXw>WYVjca zq4Js<7gtxl%LqZY6ZiAwH~^BK*&ZxOLu)LitL~#b8jM|R4+#nhi4hzCJetjI&q`lX z!XaD-lbEY_WW@gbU|H>L!Gv(ep5YP<)F(~lb(P#pcELYg4zmLSx{9=`4ef%lxB<@z zwK6bhB~zG$(Fs_b_n5%5R@4#Qjwj}%BqXWM3jtG8P(1$~_s}lYglPFbBO@{BD~RCN ze6r)qAQ-#Z?%LrCFA)u>8e=7NP6qmWXQpmlFW)82bt4Zjc7JN@R(liJx7z1CPDv~3 z?N${qFfh1YIWI*>*4Nk5t^9P_)h!T)X(}ivsD&MOwgvD=6=vss8x~qWwOUnW-ody9*nJ1H0+6*UaP0YK-_Y+G4= z6NB>jc!G<=gXNy?laehka?qG9Ad1vOmseM#9(6L}+LtqGTT}pkSNVrTnHGf?5rcwu zqnpxof$1^i>ilo)G*EJMifs{vti*q<&6*Frkz;!RqfgH$r|F)k)v4yrOI1SG^ z*CBE0=q$H)4FYqvp&%n=vk;LhWZ^Jc1Q)-RordS+sH(@r#pN_246PbEZ@RY3x})ib zZ)gBNJjw+y_SX`@N|I2uhQ!1~%PEAk zpolU+SfB09cL9>w`Ab4Vg1ZKS57RfNXdzW2Nt410C0OBLJtqxdv`rPkW*i;3Dgg21 zZ_y<;=JQ41ig-H$#N>E29@OXzY-Q>b5p?{4)up9x`1le7chhG0cC=2o#b`xDua}|} z*QgS7UpWYkGE=^O4I-)VhMxW#QGV~og^`gFekXg^9dg*mE6`RcqzTDm;^HAQZ?i1A zySsDq^*0tYhuIx=hAM*vS!2hNfP;iF}4M>#O8=a{X;2*&uwrSHmk{ zn9KLJE!0i7!np5+qClC=V!JQK%f{m|Z7;{UpC*%Yy_+RX3LzR+R=cOOZdUsXZG4PH z(or-Gdo38Uu}p-#woAXfQNqZDXCX#nDW4XC5%G@$8fX=RiMd0snTm2uleAJfKaU*_ z2;cUwG!5EHOFwyG(|Ep;goT9#BuU=h-q^xdrF=-r!lELWRj;z;Y`x23J0>3T;%HbA zX5g#5yER#n*Turm^tFE@D^AySHO0v^&Hms>y#DH-Cm0#MtvBCAw`94E>D^4F@-oT* z=vks`+|O+<>~5~k?P-o|xH=S)dEWZ{bl%rDGRpe#fP5e~*&6q1Osa&6=w}RLtth)BfQO9?y z9T%6VbAMP-7{=ETZSxPk!Gvc?D)r);a5Xhtw;V-%E-a4G8wg!!aM;A3?#|{^LsQq( zyqz5Txq5=N9W&?C@)ogW<+tERCg^&#Dtrrbm_m0NEoN=DcNme#hQQ(l2_dvUAqqRb@~>$ZSXy`2mLxOWxO*SiVIJ+@0_U{7K)!;T%PBM*{me5}kPksopxr?aW#gh8Q-dAvNq5 znlzBHAdt0=SUio;dCMjmdRd5g$EZKZM*R-c`n@>bC<47*j&Jx}+!K1@xuIg3S*=u8S_fh@18sv1}jPb}W8AaQL|+jMimSVXnj z<%C^D4>4MmLvBo0vz*7=#BKL)ME+ppUtlb$Z;3Byo$(RzK>y=daEmLfl=J-8zh`@O zh_EhT*+0brcx|+N4kLfyDTv4sXDbI3>EnOvZBdDKjJ9>N-$7o|bmQ9Oc}*9u+!^=gKo`6n=m*`QjyV0n&@`Cf$78Uuu)#{9)d!Y%$epwqBak80_W+m}d*Wb=zkk0o@+lo9&ykHl_kE|OrG;LZ;Yw9qZfCp;i->YP+3FHl9;24j z6dZZagfD#1`5I^~)%BnwErQ$g;|a*lW61?R>+jJ4JuIO4=%{_h;P0OD8U<;V!9M&b zd%#}9hB%n8jq%xIIeB^Ev!v5GFJUe&4PXgJ%}7a@ixb=dp~k94=3`!KN@dH5I8`yR?SxBmimj;IVG*vZ{qD6N}uv(^IOf1OtR3Z=~{pZ62v>;5`t=LAYNrn$U%Fbntp6{xY!dO4zmYgW(Xly{E zrDboLON1Q`B6y6{OIvat;pyVKeFvF&xLk=&EyVyJ|9}sRFxXg)Pes$dV1`^Y+aJgZ z8t(#l>xy!sF6WCp`AbN6%|x>9tLF{zqR$Oa@R;K08`1C~3SH@Vcq!BVn|j zF0yVHSR(iekt*N>EF_@AHkP<$nz#k@9{}AT_;~HP)Q6-LopSJL_rwsaeNd$H@bNX? zTx@RCfA?=;@-!~`7(3O=XBA8o|EuC5dnkE*eM;0<*A}llRR0>*ng?o8# zeHAN)DApnYz!f^n@(KG*U-&gfkTZ8LGhS>^hOcbshh%{>3d^+_C0f7IiMfz% zVL`h4ifUMHUNZ~n-4smX{<{>9^HX5g7%cVaTdY0+PRDD& z?JO&X#OA+-O3CWA6gGKE?RTisFiU!1sySgZxa|cof`_}CDNTy!iGhOcB!sGyH3a!y zhuFxxFAR%9Fh&Cq#l`3q@joH$BgO1tM<^cQ_~BV>^huta0-ip8#dqbkurc+RBuppZ zc@DIAYsw?l_S9ncGW0$?YXH#A5CGH(nPQq{s~V6Z7pMTHrgOt%eSK~Vh($3YOi$u) zeE2900Hm%wl`||OKQ_W?>YgDNbmPU5$ls~-6D>i}_rMzfK{(Pnkom7?ftQu$681$_Boroo}{&Dj2C@q*5i( z%H#*20YJ2LlN|t(NXSmV%&qDN?Lmgy{KdaF0o2y$Z6H3@yLL*e43vk+2jxm%@Pv(( zRj(}!#ZOy~=Ls$-Au8WHe`|aeGtYYum-`Ukv#o3h^B?VA`-6_6bqZDITOT?7xlz={XhOsG2 zJt}OS9cb(T0F*cSy>M_d1WV!DIH0Zdg|1QM~m4<-T-1d zTxkWYI*wHFEb9_Z54K-n21{BI;GF^Z z4!vSfX;=#kNv+=d1#&|nce3EMl}P#qQ}+bX;Wts}Rd&rhR@?hGu{&bOg$1$4kLqu) z!)fhTdFKH63lL6|s*6Q56xcov4xSx3Mmy32fWg-LH;e;W0KHHBw3w-yGoqvwWma<# z-@usN`vu|rC}tawPXsQ_c=_xYwbx@$;G>BEd&tEZs;tONJL26P1k}co+Li6uxB_zE z)N&*x29Tfw4wLr{Y`%k?vcI7=b^xibOZKB;2A4Zav(0YI5HkJyczpVv7SL1xW8k)A0HTVuv{|40*%M7o}EH4I;%e6WjeQTcu zjIUh;M-m&D`CFnet8C##SC63~k$88DEBXlcIiRvtMyT~j?O6&lB7D4!zFz&#!ZW^lqcM<6N#sTrT9hxTS`84vUjV zpDY2g21__<;b#r&r!fHfR&4IX{-mGhTinfnxa~E)VQX3g`SK3dbz7CZOw~T*-B*SF zNy+%;J0%2?37ol}J4{SZ>s{0Od_#^(XUtK{v|5`9^I0%?oMZKggmW6qpWLg#SzBbR zT%IIF`aaz^q7Fd1P!w6xSQRNc$!``vaWG4|c@y8F952xqlZpo~$7K~1dY>n}RUkCoc= zMo>QL{VO^5(MUhGG|s3;{5aX&So>b1(M^QVaDtd!=7ADat%4RNQ73KmjSa8N~YiH7Y0ltowQ+Zc0HSqAaNhBpGWwU`_oV?Wgq}{ zWvk=3SLw_ecAs_NwZY$5%drDxA7hE5r2eqR=09E@uh&=uVl>|AF*@>Y%xkj0EUdHM zvo>&mG&(7?7S(h((?KpM08zbH7tXp7NM7;%>5R_6`#bU?J+}X0o_e>_4C&BqO!rdM zho*4Z4C$8C*cRQ`As-7+ZE5%&Hv!08rPOj@(n^&1qC@?+k)!ddnllggCaGSH?)$q` zgWTZy$%AUxk=w-#m-$xA-7zW4UtL$6mC~zy>2MYAVs1zLBrl4-*n7+?0SZP8>BuXXP9*c^|Vcd(o>7A$=#I8a7$NaP&vT z@o$~2@~=w!`#36SIC!atmrkm0aezh`9S^UF3Ik29Wm1Pqi{~a)6^M*bfTSon>G6-x zC?#jCBbGBdAi?>%32m2!ZA4EzuV8s}#x)cBrzy?<+lX(#zZ(Q`=fwk|!q+52z{U=# zqRlP9Um}tI#L#>I&*=a(mCeQ_e^~~)wnWMjc!%r%$fxR<#HDkKqpjCOT+)ov1b}0-}rq@F*aHaXz z!|GczKXFCSaQWsX;NjsZ#X07F7iI>j7>VR^brzGo{>2E=KqY+Y7(F2HOpZ-s7V0zt zmzt9MKWULU`hbUmPFzZMdhHOih$~N6&(pZTE7I5CDqb95-6Mn+@&8My{M8Isx@X-? zpN=ygt7L*MLP|zfb{?{AuWGYkNm&B-C5CR7Yc4pWL6x}X0P)Q>O0=ALuc3(h`kMRl z+{NA)n~Y4MtLqYB{Yp1G4WAdoRgstSdvAC5*J^|aSV@K6s#ZqWyq|`Neu;*l|9`~G zM>WU~Rpux4F6?|2{?A|gxwsU;#=ZHxl!P2`R4VAr+b$+1-aE@;B!4LLeNmD$ABM0N zD`@=_tqaPGbVq!#&*(GpBy4N_N1``U7~BZHfuaM3G2M|)2>%`_Jj)11(UtvznfPHt&cjEK z&>wzXT3P}S5P%pSY7>2JVOpV13&*!KwF%2V(vyN}z7RXt#FojDoOK(R{r!7l#D1;O zgE%3kudm73txo82Yt8feB9!df`Q2}3vNuWb1WfYN@NuJ&r#`BpXe(`IyUK5S5- z>i?iXj*0yr1+w|o8LPB(;{g&{ZF$~7&Jv}g;ru{e9)gzn!ug=9V@+giEDGp8m*(Oe zfIe9b3#DnA{C`e@0TBJUIqZ8keCsLkODS2m?{elgVg)_$m(0bJOj=sb_Mow)<#9XpV!Pn^A3MeEna!Q`^9*nA=C+{y zt;az*X5eQn`r#H$8F;zKiczDKfKR$V7c^SjFZ6=pNp1ks<7I zrW4qeskkOl_UUy3k8*4YYDZ~bz523F1^-x3TT9fWBoDZv?f`zKNA+DhVBe;54A)2s z$c(L?t9orw`uTsUkKIdV!YQ!R1c~K)oOYyKPr0t<{Ra}Z(VnhJwtVDD;vY9#SJ5^m z#tBH8b}OIpF?Sweh0zOmgGJbxn=|c8ti~hzt{5Qm@&BMs24fgh^WwMt`&3F+Hu_xL zE|VJ{xp9K~yzGb7oDfT+se%noh2K{^mG3~4yrC_>gdONSu}v%YCXI8`NQUaEY|pB8 z_A5Se*-g~1C3v&L8}R@&6H!*C@(^G{)-`Ys@GwA{U(QlNs{EB0VisQJQ-Cg{Zc|;- zmA0tRDCsBL^lSL&c3!#&50K&l7zl6+O=|FmN|FJP^HG4T5b9oy5wr|ZL)K%K6v3dO{POxOKnvZ%ZU>TepF_Cf(u_DNwq zw?1U^C#;I(2%B_vWabg@4U0cN1GV~l_(z(Wp2Jc3!kqgP1J4%k!sRWLFB*7gv`kod z!C0}!G{^~B=eu4_pAAPOsSaRf7rqbjuH3!NObY)uIdZ^K$){M^xT#Q5{>ZOIQM6)o z@vq}bs}qH8-H&sX!_tJwWg{jl210k>h_#1c6i{P;U zRHz?*xOi3i-ger_C_8EKd6$O8{@H&00h+mTzh!Rfgj$4SmkKY%M*1RRjuR(A_Hi1x!$4wBpj? z$+RQ9AW!wS%yD~qbd<2ZUQSB(63J)0iw=Xuf|DTN2+t7(wY66#48Av64=ETw|p}n z!6<;_8@X@~AalUHnYnMFQeephKm!^cYdrs(vF8=!{OEZ6b6_B)LyoPvxq!<~@>)M> zKxmpo$AQJJ?&;Rn-<=CSz6v{FM-gZub-%eMXm_L8`=vM6bNcl49ji`Njf`xJjaw&2 z>Z<3zqE_&Ev4OpEK!Dw>Jvc5n1g))YdxzN|&yI++r+R%@@9M@aEYyQEiZ;x|&TVbb zKVf;MPI&mp2@{CZ1)??VT2mgsX2fMHmI})69~`tuDd~*2KQNR6$3c)@X)gm$l`0;-kYWRon*CTztkn@sV7$D$HbMwKrUn;KxYO-1?k~1d1w+hzheAH zOEbQaE*Qn-Nj9%tHknX;PxGh#A7NNp6)P6TS!%9^CtLKmtno=nPaZ;cSFaa>urE%w z4<`(0z~&$w!=`R1d~>E=pqL^|{@KWAf3FqgCB*f(J!Em~28IpxC{u$;OT_+SN8x)` z#mV{Tl=v$Og%tGN$qHI}r4yO;is#SySNbb6J(zXPTT_Eu*x^5>rxnQs&M5c>!LF*U zUT2n?)G{1CS8;JJ!I-0P-2t+G(`o9pEQ?^P%;)atm$tT^hOzXTUKi`>g_^iKG6@LW zShU(=>8SSM<6>xKdJH0z2(YQg>ArPr6-%D%GD%m1je?J2J;wJ|(@JP=uWv3D5ruo!+kC&Lt)>YHk0)z}LyBD1 zD+UBl)$EU|LD|&X>vws6t-jdfRbO)+=XGW9Czy~WMTk`4?!baFSI3ltWs!o|eClWu zo%S?6IFPe+aV&=}A?>zBEpX8J7aRmhFzSAf6Y$@eFZH0LoX4{&k_cW1Rx&W_2FJx{ zNJ~Fm4>_iWXZ4(ZcmUo~)gpf1Pc>6sII#Q8e;b zy?qwlc(Ji4kTmBxz*{j_jJdm9`GWNJ z&gaXk@xL7(Rn@+>J+MOj~j+G<$+owFTqY;OYuUtaFWXdpo$ z=E0bsU)miIRgx70I>8MMU~4HxZtmXBTt(RWCumh!LZK*>R^g$_cO`X)Xf2ot_jW7x zoFTkztp6ZQ|B=Rrn#jTnuA8x+U(^XSeHVh*ZEY=$^yg`(ghsS70wBLvRw3B2rpHR|*<@Kj& zAXPNT;{Dpd!MeM1H#Ht@fBb(i+S(^$fJEa8fy@ki5RzsT@*FS-OHgWUDt)8BH4y<0 z8Y6dWH3t0Ox#H@k?=SOKv5mO-uHj+ z*~4y1T2!XuUE~p!-)be zSH(J@$Y+aV@%o{dHd~HVsbcg+s=Oy7B34WVv4mkL#79?wX1r>D6OFfIAcb+9eJ?vq>m2LX!-2w>&U254qy?-4mKa-`SX8F;Ditvlyo(~~D| z4W17(q??m2@wA@9E(o>Rk$T;8^auy`B?>^_}e-%zE9H5R3lbDX+?E zt{sHfdIc55q*M^r(V!TLsXv_c+?xR1DN6~8-h-lL1`c{6v@dwvc0*>)^Hk^UO{;p_L=Ma%xbxWrM$)KB*Y7Nk`w*Y z6;lL>XUw+ljaJ}tmyS=b5Nc>-=f`)6rr1RZV>+A^E)=dQ2IekR!^H-(9J(~qO9cjP zRfk#P+XI5Yfu-h41P5v*s=nN|QYV55A}F2WbwchB(F-Vb{$FK%Wmr{RyS22GC?Y9Hhq5UN>6Y%2O-gU+lJ1c12I-O% z*hmTj(jc%wx}_TdY4|2S?|Gl^I){J2T5I;4cZ@M6FsBE8vt+%F;=DfWuACI+v3hfU zu|4|p*7a1e`vL3_r9{j5R_{PJaU9T!fd32L0CAq*6)G_2eW#rmLgBku?DY%_cmV8- zePD02mc_1VwzNrd0hj~v_t*sktq6*O7)prSN!Thl(2EESbIH2|BNiF~p=kvGqdg7Z0 z;kKBsTajR}m&C>LeZaCtHtYY%nTjnG`LYI7YC3gQF9<%CrSz4o$aq4c@qAn5A{Bf7 zh9|#(3wSp^LrlT&MhegpG?u3kvCqQ&wCA~a`kP*_t4$m`?W9Skz7TqHTurdUfB&w8 zVK5pQ351{oY~IdOPG|gvTS=ts1690rx8C zY~ass$||```sSaXSrSUqm>;0n*8!A}a#8jq0JcOR?x#SsEKT^msW{X^GnMz>b5SP!sW@PAci^C%E{kA0BsV zsK+>D%oQuw4tTPkX|(-5X&+`Ae?wam6Y~(OX2T#>Iw3JpQZg2c);lrHm)cKluLW2~ zoo;}}@X1;ZGMmTrNo;*6DE?{Sbzv<$BsQjzI_R`6EajqNQ5;WOulGGpwK!V}TlnAi zDFH_j1aHNmF@#FZ8Y(KWSy@>b8S+1+hDC3`k$ZSwU8Vn*93ZKhl0woKn^0KZ6zG9> zzvBVsEfGTF%^Tv!#_V0W{!*RMSX2=Oya1$BSs+6R%zmwpf%u_;hnjYFY#NLJ^rBv) zd@WWR@vvuv8Zun*^ioGl9iH+iAfl>xF~?YW^e%E6im3-2rzpjC1VXh-2LSkUa&pG7 z9-O6%P-4hnrw+4BE=x#;V-Y`j+4V;KV3@sF#U@b;6Sv528Q3AiLbfUaJi@{T%!J)t zU8*lqU_@%+ukQDVBE;z7*w3ZqcNG75_qmn&iXpQCK-~={!h$L9UIdUN4Rv)kstMq8 zKgP!g5VP8l4!@_6JRA{^Y=4Q)yFep^{#$PQYsm*)2u{F1`tAsTrZI%KKP7RO0LlN| zqi1LiG4L47U4Jb6Ck)TkhZB?T@NK_#(7^G0rKRQU%%#Di`Uy_(-}SZOEZ+HK^&H!je}!?5KjM%j{|zYo{q>bdLX|TJYy;<%?UWL5S2qN}31TEQ z>A0Oib*HM6U33?)u|Q^cAXjRVOlez=hxPRCF&sdPSQ=-8%>uL*{f5@Kx!urS~R>-$?aG1$WZ-+>V*0D(e59k<6!h~nTN%OC;( zXf*e6i?SzyUWIRzO10d#kys@3;s^Icn9we|LOb_BUWf{^`-s;LeVu~!PUnM9Z0G)E zU%nLx>wQlhY-Rcy7I#<0X>5&50CRh4eF`YVfX~=k&nzudtJ^ygzg_|d2`4Rc!t8^) zLjJ_njep4FfxludKTiDtz{`xDcl_b6Aa1T7`bZoK1$dbkb^4ER?w7fV}E z(^YWlA-=gL2m@y5FbhbGF(H=s&i^Sc`c=)(tWnmH)e<;JNvVwIUEP)So+`5@xan_M zAWf~vLwHA;{sWSak3oA}Y%T2HK$Fp!z^uxrlK!j#?gb-HsyN{z>tbb7Z8zf0ZpmQ>t^C2KNnjo32M%#K!u?=|*ZM{w)dTSDdsVj0S)>+NL zM26T}v*5Xlt$>FJyblE%o`aCTTR36QoA&w9z^}|q_r=$I_m)pHmO=99@8p@2aW+Gm zSf@d{0yr#3*ZohSLo9T*v2eyNd|WTw@)yHj39N;_rTK3j&5KZ>ov(F}d&Twc4=D#nWBrZT z%WSTH^G*YE^g;!wN0MkZ^C@AEdmI6E2|vH!1=u3wGQQ;K`hOu!Z_OXlJy`@;;_e-- zC+;>({0DjsjEm4l-_cN(rK_7cC*Eb={n3IEGp#IpT3c^@f&S8&!$nQqflGv+P5q``VNBB$=^G< zIQ2J)2W3r!PE=rLPy%@*S%L1U$nM5>iHv{3*wP4J&hDvd>g(z00kt|=|9J;M&$zt+ zZ+Rnu#dwvXjP1l*Gp}VAb*D(?|KyCkUJ}Mv z5HUyyzkAI4PJh1lDbR7z)5EphB&$8A1E7NQ(-g|}0_TDGCoY+O0y2dCfMD;vzsGvP z1d#=K51E+{T2=|tv_JsmAgRTEx+6N7_4`sY901>bohuB7_4}KYRQ5|EeXP9rU-=eZmk*Kca?kiTzWXnsvDQRkXsMGl39n_W4@W> z<5Wk;qmJh0af@XU>%DV#d*ojJ|H%$jQtHLRUEN?A$>RW(o3-YT`f~rkD;N;;$o4{otN*^d%h=C+S>OjzAyuYNg7q&!X;Q7sl+xsddpVq() z=4?3BhxkOkplpNcT_^nN)vM>vWtj<+lai<>mwCWG=DAC@5()>$E3mm=%b?+aEnZ#0 zK#r)mxKH@PBJ|@N9D(TQj>cG7B{$NHQ*=MsY5>t6Iy!a`j>5vGAx@4UJ6&-c60CKp zYyxQppmsvU+Ew$5GShpS8M5nnkzyMQ0A zCFL}km*#)ObC?iW4m3`sN+KfnMrEYiG=crnhZ)V=V1#XOiS9;S4%7nsYfSA6G z&3^Cu<*LI}KE&2!W+GkRhomZEiiH@s*uRWrv_4FO{~SL5lnqXCfHukdjC@k?<%p(R>Qy9ofW*h?2ehrf3rJos!~w$nVNSn%$5UsSe0O)>L-HLz)G-H z9Cu#z5})>!aEUX7ZVa>a=x%i(`uh|D2QS zT5L(tyG0}j&RG6`Q#prW|4HSD<^E0OY%eT?d_6ioUoVx4bicYfe-oCK>Gd97?MM7q z+9kz?VP3fI-M8rehpCgxXksE>Fokrpw-%HAldi)c!bicsHdIC{?g^x1ph_^1e{5+X z1LAr{(!? zCJY{%N-^9-OaCJ#lO!aLdxz+7UGQ8Fg;$dc0>dIg75m0&T@W(QAT8&^I7b%SU`W2t z8qa{>tND4G(8MYWxw8hp(ha_=6>2UDWk5U!YA+xvT|fxjfh2FBuqJC_REk$=55D-H zko_Vsk`M@`u&@5dD|8A1vEzV~2XX>l7N)?8Mj-zN_TWT(n+1Av1HODZHZhQ*`IIKK z{!8J63x8ES96tM%5<&r-m2zP{g{#=4?=YRs`TGP0%GF10RvywEGWAA+`JKi9*1$SN z=L@gb)Y!!bN-83UXipBiKc%t!lZ}x$_mvJ`0Qr`Uj$hg!EddM=GfGsS7P!|f8C_pB?<_!k@B`x<1C@MT6Rcz4EM}yfX0?z=z z(^L}fTnPjM19T1Wr#5CZz~=^bbQt^YToBkM3cxU(90Z3T4OM)|m!x_r3nYZ@ys2Ok zf50syK`e6FYs!wy41rW@?tz!_A@?!mQy^qS%cfxI@O*10#rF zkyE9zA={mu;LcKxJjAMFW3hWffmp^J2$u%XN?-Vy`ddlBxXrQKet8@p#gUn~dsZbx zwhe+P?HFIF;Z6iXAX$b1hSy!c^-viKjD?G%HCT2;?x65}T!=CEcHc@&to8=hQPwad=z>IR|n7; zMhab8pcMe*0ujqpAlS#46B4lO+u^UD)El2Pe|1tUv=P?cH&7=`Z;V-3`7oH=sDl~t zL?(PJ@qaWXBCFeYfp_z3U~PNJB*2}j9859-tDJ-YK&8)k)*f_S)_$q;#OAVO^Gjf5 zW3C*3&C$&LU)npwF5_=NoT1t)0GLM!5Js{L8X$LZ$LYoJmfZ%laIixh?D}_gbpc>W zZ!Em{Q@@V;z=eXG-lFbKQ>g&=2CzECKXGu$FJ1JrgSf1x>Q09A00e3L$QHBprXsNl z`I$&RAU_e9sRS#3$+of!2LhAz8bju=M<6=}V;7y$XC{ybKC+0*-#$uB31#TSr7 z{?<6V;#$Ls8oOGgH&f zU@3BU)6Sf3GYp@4cBee8lx`k>*5;th1qYBARX-cXSt^MvpRiq^xtcdPYmKSsQ62DN zjw6JChzVjv`muXU30>Ebbo= zfO110Llao{1Kj6!*A~>Xdjks2Q5HbHUKrBN4%2y_xVF9?-f0vjd`A!5UYa%PSM)n% zzR>EVcT#bJIr%xII#oIKvJtufnl{iJx$*JPLMGCfI3&DTpARAlZ`(g6UYqNhtX=$^PMe6TCFE-trF3da$gCGDwHxzY_>4`o$>s1D7ZBMi zi>axpipnEX3V2#kVIiWLq7oJ|zfL~Z|J-4n3TBWUo-3D!o!_4yULc2V3}XqY3aMkm z;}eByy?0%2+zW_uCe(7d3?iK-l*}OF{h>@m_Y9t`Mi^J=*Tehd(N$l51PTM)O(IJe zOC-xfr-IFTvSd;22#hf2Ah`5G@n}JMF$dcU1qX&VGS76QL zp-gJrcpoxuzYLkvp4f0dO#h%sJqqLTUL58{^g(N-k{9I4)r#>6yR})A($>l>>mu-N zLH*l*)Z&&u5tod{)|~GaV=#^-*+=evS?zTZP@MC_Xxkkz*V9?HhXElXb|Id*z6$ zzSgt1Mp|hpL7#?I5V-}B)bFG4 zi_=W}{Az5NFm0SveMPtE6{qo(VlK--u*6t`#&>NN3;^Lyqtov~8>T`~$2_8>op?at z`jL=$a(1oL@Ohg2>o%RV5S<$p#%MsOcYTf5s%XhKX|x+{Ltw<6)~UmYl_yHsC6;O% zGj2eTGU|?DYRS?!`kR!<5(E5~>1dYCmJ})XMKjvN{GYyCaSkmlPZG*=48v+^u25|^ z#nOVpbScY|LpW-r9*HSLaK{z$TDnxdf8x6lCXL3!IW$uDfDpla zFaKmhvQ9j?jy$N3w<*P}$u*d4sOy^J`8@?LS8Ny|(C}d7O%D#!pG8$G6LkCGPGE^) zyGi=op@L=*876~%&S0>BrQgQ|dNTV>6R8{aewPL-qo#No8X_(+Sgi%E+>s%o=gpUw z?cX1;HmT9rN~U>Z&5MD5FEg^(^1F}hK7{{36X2S z7wUa(ThN&rIoV%;ekpw@EvKKI86S52a=>B@j7FG)N=#C<3UQps^IrtEp`~U((Vviz zkhH|2nFmN92rPEF{PcYjR+yF~?3Nk%f|a$%)ot;cpP>6OF|PxyquuSzH4q8lVPpIJ z<%^<{l5HIPi`CTf@-m{*K=|?F_~Bt?l1S;DDC^y}0E}Z^HnxY&1rHM;PJXs#F(XVO9WJ9Q8Ub3_gql)ajEAyg!wfe|1>Ja*P>TUte1r9~<-hGi45F z$DnQz5fK7{fS^-ShoT8#SK;4HKyaMDjKx$8?h4Aq3a0eEl z19~GJH+4Bby_Tb$T?)G~y145iq@_gz%+75MGuvAZd4cykli8vH`L*Ak65L1Cs?-Nk zPwyrw^kigu4F+c^tY%L@WnMXNjp@|c*h`E^XqgX^8$v0|&1k@m8aBto6f@vFC-tcD zd!iyK(44aP)c4~DA+I-ZszFwy^%NOcKnii1znsf$7g_+=E`BluzE_L}#n@D0Vq#J* zu$q5Y)py4`#;k%9rUV#wMUb5GfMnPzEf}lEdJ3r=1waJC!28@3FxuRoD9k^4Ycz2| z2JnWBAv4V>jxQ&~uEWsiD%tZ08HThMW>0wt2?&O<#E=|*1)QIs!^gjW|2}LE6gtde zHMO-#xQOxbsjD7^&Q(EP@$&=y41`OzGWx(wri5#0=W}BjIV0?Wwv7#o(2`hDD5B9U zCLv+0NGS{GqnBunoMQwSLz2Gbia;Sq`X*%&F}j9^qst6e3<_zSK)@ZFh-G*kAYX+& z{zT*&LW2QATU%9id2w+V(ADW>`lqKGb8~ZBTD(E;0+D_1vx6M?>+em*Iv#+4ohun0 zj6>3HC6DBDeN9_--?moXlo=*7>YteSgxs9_xTK^+I`nr;61-yQBR|lymf|a*wE_zz zvOCX6a`c_JW5{KaC=?=|nfSik8$tKY`-NfPLuSg%+9AkowZR z<>&{$gx|2O@%+2ywJ-9fk$1f%K?Nwgwi6X|fE|5#vMnWpF!*haC%bbI$um!hOeWyU z#&ity2B^*QWF>F+ma;epNrF)k?|3VA4t~5p3`B$DgCp^u6E;7_X!mq)?y@Fjw=NsLtHR=*1-IIQZN>#nu-Gv0D7}P;$o0I=X=GomU~2B z-XCC5#Aud^piA^3F=ulB9iFh8IeVvv#rZmoNh5a*8Mie4YER^#05W-baZ3h;~@hO%} z#$!z1x)ljGD8*p3m`xd)2$WScLb4f+KL*L@Q)$z6&*=)&_=64761hGa1z|K>8HtQl zhgltLgen^-3ujXZDJLd^j%mbQ1MKwOK?Nze_dpA<#PKblpI;&&R0S+lUSrCI0z2=K z8Rro&*6ajCDCQfhJIK`9d_P6%5ZF_w-93iYtoW-z*gm-l7;D7Xi50$PhlC{qY}w5( z!O^@1h=BxP+g#n&p-MGWp<|A!(ai)SeX4;xxLRJb_FxfGXrX2)YmZD>PVc+iz`Peo zkBMXDeR|NXeh(rtm&dT80EWove6kq{#zX`xR8zj)@k5#7%8Ef~Ed#JGs3q<-|1=V4 zR3S!qA=~HzF^#z&?Ga{MXEK%NzLvtCe>M>O)$+H&GQ9IS%^SO?x9H$%{|y$?2Fs0C-0m_qSGdd zO;SA6EbGL_LdtQENqU|gP&r}5!V9c2)L`E@5vKzFX6JMw$MIRDQ>p;C`UhZ8Hpbfl zo9uPfdlA!SXPY(XZdBw}sU;ICv%-LXmngJE{7ZNdivVJX(6U=+^Pa|UO)yN5wQ?|m z8>BgwF-fv2Ng1Ul{DG@5x23Kcl`?W=LE8d;n+eT%cilH(E{_A?V<^z!7Y-Gc_H4xO zEW+I_d>chZ;yC9K8xypJyjDNkW&l_v;~Sj|YovFVSlasNVLt2!Tz@fD$``wtPM=EN z@%lks1NaQ4LhA=*BNAj_3Duhw#1>|Sp}s*zNx#^FMm7=t{sczLHI^9&V^AUx!+fvd zSg0P#o%fzxVvSQr<`g8+5zE z5+tmG7{NE!53Sk5?{k{|2!_KQ753>FIB_^D9xMe7xqNWCjuChU9v)mSR~FX^Bj|Ms z;EPa5Icq44xDwSz>I@?F8l?f*Qaf-qn(aP}TMu+f7qLsk-skHYov8sF3(XZ?sMG~jW|9__)Bmf(#IyV4Y#*l}509&s&H}nPo5#@7vtKCy zjkW5qjO~I@Z76eHz%rx3(#O1#?P#9^KUOm^|cXt01$ z>wOZyH;;QxVNU5M2u-H<_5_eYO$P^w57D*%Ec*2oV zv-T|hkXAt_Ans|21n{TK#>_d7?34b$DFi*-km;Y65$Uk)3P`_U6so z{LL~c9^<#JF`BYp!pT%bw%)T=G?belo`*>nE+MjWc%`>j+kW1**#|;k6$);>+mqBr(x=a>n^kRuORzAYr6Px?IW~D^e;b?Jy1y?kUCc* zcIvV>2AU<_-a@W&OTcrX`TSL~v%rp+Ov3Lnkg8H~C0cKQsV-3jTIV(bZ!uJQn)~Mb z=a0~dw*#!rF=J!u2T4YUl$E}oe{yFCQ?y@n&UmCJC-blQB`%!0b2f8z9Wm_Hm!l$N z9nYSR4UWSK=V4|?_w`N$!x~}Yw!c?XQ zMQasnQ8Qbi3!FU@SqW8Ekbyd`<^9&t?sdz7fROvaLqg|WWdM-X)6)|Xfs>>BMq3-R z4<-mUdfFjvBosAfSX(%W%x9=)Ur!RhO)0}oj8un5Wi*9v2_;r?f`-8%U&P_b$?Bo& z$jAtY69K_0=`YYw;L`q{B9r>l_U*G&E>6QrOC(Oj-P_(1%#oF^`V7LIKYL#<|12d! zMcv`x;`(#!R6qmZ!gJNDrrA&fpSFR4m7OW^$kvR6D<=u_ur2%r?x&>Rv2Gx?HKLu{xD^oU0hyPhVy-t)S`4q_32cF-i)9} z3nfEjlu$-8kvvAPc29JL9{*SCba15;`HL6v>Bk(QyY)@1WJ0sASoWlTA5#c6VxneN z`-~z?Sy>3q?E0*o6X1-@jUGHvg4F47_Y?QTjQl0^;pvD6CM$*!t>guS=NU%_8=M&omVr`x1 zq(5l6FMf}Pgj`pDl{eyd`m9sOBJpy-r%0(TLwKQ327%CKT}zwUVuXcS>Gac=cNx%k z6eyQ!`^5dJ?N1gj&Wm{%-L+ORmr7`S-3*~Hx!%zXP-a9u^o2_VRuleyfQN_a5_o;F z%`RJ`S@zj-M+ngjiWz7-oqzd}bJGj|H$QgxBZau6AMWfEvmgn=jcD}Z597nao++}- ze^P(qVPIe&EZmx%oqcw8c6H_X5Kmg}xcgR4bcLKcT3*m2sn#xMH--v@DKRvZml_Wy zWA%Zz-g&zZOqB>_O9%}_@4%>{0VryS7RpzBuFLB3RFoGeAw^KTfng@&Ve!)5d~vBh zQSlK_V5GyR-fvGr!{Zs%B;)~ILEDm zN_I9x;M@|%CyjfsONWQ&J72qRggl~OL9uP&O_|Eq#D%HA>tUm+9LI?-gjpExGqCbG zm^=m;A{e_2BJ2quIn~i@j^vR!23)3b{rSk}Op8z5ZSRmaQ6R@)X08;R4Eijtyw;Mu z_>;|CqcV+?d$^3;_E+Nk&bM=%jakaLc=Ls4)jH1rvM9e(^~dgv%21|hl@ud;Zoq)K zYy)&wQ9LrYC^g~hNb3RU7YUXePn(GfuJs>o;GU~5V4%r)5$fMR0kQ3&88d1=nzg71Q1H7CrUshd zNdQ~~`mnxSOuCd&AhqkXsR#p?09mChMvv=F>x@lI5;9U&&_=>cSlsx@SJpmxQA*i( zk!}FqLPHk99$~6`(hsNpF?v?9Kc|+~6CgtH!bGbFPLKs_GpA}?`EsU!?Y{s&NGC+~ zCu664cvlzXf2wlNlMorxCIBvXdfzP1jX*;>s*jWDUO;h$4Zt+oE zIn&28X;A;kGA~qm(<1OzSVVH zx3v33==oA4^Kq^(Y~l^KR?7Yu2tM+O44eQlS1*)h4|= zRmKaZmW^tK8oEzQ{X;}gGD~CgYpv5qJ@!*o8LOI<8@OT*5#^!r{Gs z&(nNPx2-XD0;JS5(j!+~*4w#iQ%SJv0rC(r_l`3MM_PS6L+c~n@ZXklaz|PQyEf)L z=Sv+&R+nS`i9a!pSG#7A&CKYdqH6M;8LSPL^>mww$G%wW{4VIyl3ho~*iRLji<{2i z8A>CM`2I+B|D}1<{?yTvt&M}FNGFq@CFlD3dxPX;a?oWTJ-Eedk#8((mIdIBL$oxc zP)H5oc|R*gS-F(3cLu%nBtqUGHwh;2%+v6-7etRw>a`Rg$i)%PYAV)QGdo1>{@E?& zww|VC@CYY!t0~kop~c6TrG+ugIC90o85eWqvj`PXP_D<+h2LXY)Cz*-NAhrrM^c2- zU~R1fw!D$<=eGbl{k%QMx3AB{EJ;q*h%QqSqgPfyU_2?A0fF%>t8(hk2OJV&{N=Wc zo}RVA{@&QVxywW$xt$7aJ!V~tnFh!1uO52Z3Zo6St%AqLoBMfrcxrzJIbzN>3Eq6Y z-206A_{M%y^&$D~X=~nwq54=(S()!zR3shbuvPWK*7#*G35r)CQolti^Y4ojVWNhnq98EvK_H2F~9`n;Q73`*?-VTmeOn)^y7CG|SeJzQGlb$@sCzU$FoTT9DK zs&H@pyQy@X$l9wevdV^fW~*jQn)yf4;lE}A>~w;fAcHtuEHl%lN=;|dCi0&$W&&h? z>qgQ-I6}^ktQq6!=!NaRDR@4I3DR*X=Zl9zJP1wO2KFnc$5|Z+S4#-on{RJsB0e`^ zQJ1)1aK@`HqG1-}MD4{t%q zsqP?6Ij}meFUgc8Vt{GH#6Y)(7!yHN+^+o4J#9Wz&G<4qCFn;%>-3aDA(z&)HfK#j zl`83vft~A1ndOCMhae^P`=+&HXf#eq@V%8C|LJ`BjH$Ik${?{PYqLO4P$j@5fq)7f z&8vGu|H^4?V$4&QE#7-mRlI0L+T=nWdzsu1{z@8bi05-)igUekRdSq5a;L2n@R|R>tEeFPnj*nx zx>JgJD50d&kt-Vh&hlkfgqxH>E5_hSp@JL}iuF~0YY26G{8vC6kKq_*(c`n8Z_YS> zu^9({LGifCe8j2sa*u7xpB{+Z%Gqmp-Cpy3z3tkWdR*YlU`_<4_r_!u%128h4h!H; z)Y(XKtb1QoC4TzE?iJr?X!|+#ReQ$M`Oj}F;}wQ8Jg-mIolN#-Wy(_fgt@2kCB=Tg zY=km?hU_GT&gi=pkkwSt*iYjBooM6WMl4(sju=IAmleAAsEB1!Sex~#i_XiJ@jf?> znxK^-Z#&SXj8;K9L3M&gD$U)kfyWXw6|wTSsVA=^e*aF6rujTX*Auh6{3eW4Lbtg0 zG1Ice`1L49V|)!Z-}ZFvD0QnwoPO`qscmbAKsvfAj?95%AFJn0??|Xs`i~nb6xo$7 zL9ED=27Oj@%BCMDxPiUebd*IXj!^!SsvuD<1}~)CCj=iEfr$DLzVzi`OcJ`ZRatTz zh2@94)7Va@9-v;3w=wPxb4rN1+XS6Ts_K7^;Ce_&bDr+8&r0nC4p@f4*ZW!qkW_Z# z=BuL8Iy)c@0)YEA*maVLE(zh9DJTE@O-JUxe@hbJCK+cnzc%9{w84PcCxJO_rvq}F z;A#B^i>`}g);fN7Cv2J(kFt|~?N;r`pczqU1|6gxGD|oUz(`b{@OE%|iiNVS_T54JC0G{v_CoEm{DQt+QSkH^E(eC$Xtg_+#k9A9vn6iw9w6l8lji8tqHg|Ntl7(ut8<8%6 z7yu}|`uRSh%;4r?@E!B)JhP1R)OSnsN5sVU5vq4_o46rFpZ`Cv&yVloR?(Z!Y9_!z zY)-t3)ncT&)??TCS{PEW7_tB10qP=&@g`OGpWY>(wDxNd=fdgVD4Eu zh}iE8*7iti6&9%JYHopc>(xp6&o}jY-k&a9JUi$++MU6hG@~#V>C;0Hpt$SFPAn1) z<)W(QO~nlQa^a|;u-l4ohmoz4-}8MJgMBQsEUmQs-ffoVZTy}%`33n|wLo$$=&s70 zeV~Q79s%j6c4_d;iJwv_15qR=n0=Ds1azw;{^(uqk1GA(34SZQ+e}lrPJ}ktDu!uv z8@3%UKlU@#{fu{2-yox6w-ax$kj7q-Ijr=butxFFL36(ZF9kk-YcG9@XAD^r_DVso z3u2T&Xq6df4kPX%sPlQPFs*D4_ZPE3T;hdKAC}KA8vS*9zotJg3x|FkdU|^P*QX5> zqhgezVhnqwBt&KoGw`?-z`;Yvo_keFbwm%px2#|LqAF9`(#ZYp_%;S_!>|SIZW^z`(UVY|quqrIJjg9BV2{f#HAy)#R!$Gjuby8k6Ao3M2M%G4;f_759e z-lQNDj5$rEt_9hBf~@PF)L@F2TaTN|F%Yu|)?Po)rgW>mReQpL#0q(q*#{8IpUFI44-&tl567YUh1_4&Fri3QtT1_w`+C8V!}c&( zsE%TF@?0BFF!ONMhUD} zPN9aU+mue^;}JZ0vbwUOEGtd(hktFi$^5o=Z~?8}r+a33#7+wnwY@Lu88RM)wbfzS z90n>XTN|nIg^8kxx(U>f+4NxC=e#T{Ye=d4>XbsL_9u!EcI<*9@AemW2b)(WCMIk` zc2{Tijp)FgZ6t1_ZTu{9F9vyhuh%d|Cz^Pa!Lhw7yP5}csm52cu{=}2kBsgvJr{v@ zaPHYhNY-B0x`cgC^&2TB2~H|53Eo|~lf>$O=&3vjhh@P`Ucd-3X5x{TmBqrwrdc*E zPLZC(yO-}f5D*GV=&|Z??FsCO`Szw{2{={RULd&Bil?jzgvGxH+8!Pr#>U1X`}s=0 zy8ri;h(P%uoZ!&0|Mkj*5f9}aNEFlIM`7G!qCBFcp;ANAv-fLUtnnqRFV_#B^Hr5> zr063#LK4C15$KWbQ5VtD&1L}wqS=&U_@ILe?K$t*SKtQ_hrmx**DVUFunnG=_I}sh O?=q5#5*1>G{{Ihna-tRh literal 0 HcmV?d00001 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. + +![MicroTask](microtask.png "MicroTask") + +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. + +![EventTask](eventtask.png "EventTask") + +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. + +![non-periodical-macroTask](non-periodical-macrotask.png "non periodical macroTask") + +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. + +![periodical-MacroTask](periodical-macrotask.png "periodical MacroTask") + +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 + +![reschedule-task](reschedule-task.png "reschedule task") + +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 + +![override-task](override-task.png "override task") + +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 + +![error](error.png "error") 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

    + + + + + + + \ 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

    + + + +

    With Zone

    + + + +
    + + + + 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.

    + + + +

    + + + + + 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

    + +
    + + + 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

    + + + + + + + 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

    + + + + + + + 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 + // 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 0000000000000000000000000000000000000000..3952ce243aa14e6cdd4a477cc2a23fc6e751e447 GIT binary patch literal 107844 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 DQJ$(d literal 0 HcmV?d00001 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!
    +
    +
    + + + + + + +
    + Module + + API + + Performance overhead +
    +
    +
    + + + 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') { + (self).postMessage('pass'); + } else { + (self).postMessage('fail'); + } + websocket.close(); + }; + websocket.send('text'); + }); + }); + }, (e: any) => (self).postMessage(`error ${e.message}`)); + }); +}); diff --git a/packages/zone.js/tsconfig.json b/packages/zone.js/tsconfig.json new file mode 100644 index 0000000000..7a3828c43b --- /dev/null +++ b/packages/zone.js/tsconfig.json @@ -0,0 +1,40 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "noImplicitAny": true, + "noImplicitReturns": false, + "noImplicitThis": false, + "outDir": "build", + "inlineSourceMap": true, + "inlineSources": true, + "declaration": false, + "noEmitOnError": false, + "stripInternal": false, + "strict": true, + "lib": [ + "es5", + "dom", + "es2015.iterable", + "es2015.promise", + "es2015.symbol", + "es2015.symbol.wellknown" + ] + }, + "exclude": [ + "node_modules", + "bazel-out", + "build", + "build-esm", + "build-esm-2015", + "dist", + "lib/closure", + "lib/node/**", + "lib/mix/**", + "test/node/**", + "test/node_bluebird_entry_point.ts", + "test/node_entry_point.ts", + "test/node_error_entry_point.ts", + "test/node_tests.ts" + ] +} diff --git a/tools/gulp-tasks/lint.js b/tools/gulp-tasks/lint.js index ddc1ba0574..ff9c36a215 100644 --- a/tools/gulp-tasks/lint.js +++ b/tools/gulp-tasks/lint.js @@ -37,6 +37,11 @@ module.exports = (gulp) => () => { // TODO(alfaproject): make generated files lintable '!**/*.d.ts', '!**/*.ngfactory.ts', + + // Ignore zone.js directory + // TODO(JiaLiPassion): add zone.js back later + '!packages/zone.js/**/*.js', + '!packages/zone.js/**/*.ts', ]) .pipe(tslint({ configuration: path.resolve(__dirname, '../../tslint.json'), diff --git a/yarn.lock b/yarn.lock index 7424e2ab04..3c7173bfb8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -427,6 +427,11 @@ resolved "https://registry.yarnpkg.com/@types/base64-js/-/base64-js-1.2.5.tgz#582b2476169a6cba460a214d476c744441d873d5" integrity sha1-WCskdhaabLpGCiFNR2x0REHYc9U= +"@types/bluebird@^3.5.27": + version "3.5.27" + resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.27.tgz#61eb4d75dc6bfbce51cf49ee9bbebe941b2cb5d0" + integrity sha512-6BmYWSBea18+tSjjSC3QIyV93ZKAeNWGM7R6aYt1ryTZXrlHF+QLV0G2yV0viEGVyRkyQsWfMoJ0k/YghBX5sQ== + "@types/chai@^4.1.2": version "4.1.7" resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.1.7.tgz#1b8e33b61a8c09cbe1f85133071baa0dbf9fa71a" @@ -1587,6 +1592,11 @@ bluebird@^3.5.1, bluebird@^3.5.3: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.3.tgz#7d01c6f9616c9a51ab0f8c549a79dfe6ec33efa7" integrity sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw== +bluebird@^3.5.5: + version "3.5.5" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.5.tgz#a8d0afd73251effbbd5fe384a77d73003c17a71f" + integrity sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w== + body-parser@1.18.3, body-parser@^1.16.1, body-parser@^1.18.3: version "1.18.3" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.3.tgz#5b292198ffdd553b3a0f20ded0592b956955c8b4" @@ -5031,18 +5041,17 @@ gtoken@^2.3.0: mime "^2.2.0" pify "^4.0.0" -gulp-clang-format@1.0.23: - version "1.0.23" - resolved "https://registry.yarnpkg.com/gulp-clang-format/-/gulp-clang-format-1.0.23.tgz#fe258586b83998491e632fc0c4fc0ecdfa10c89f" - integrity sha1-/iWFhrg5mEkeYy/AxPwOzfoQyJ8= +gulp-clang-format@1.0.27: + version "1.0.27" + resolved "https://registry.yarnpkg.com/gulp-clang-format/-/gulp-clang-format-1.0.27.tgz#c89716c26745703356c4ff3f2b0964393c73969e" + integrity sha512-Jj4PGuNXKdqVCh9fijvL7wdzma5TQRJz1vv8FjOjnSkfq3s/mvbdE/jq+5HG1c/q+jcYkXTEGkYT3CrdnJOLaQ== dependencies: clang-format "^1.0.32" + fancy-log "^1.3.2" gulp-diff "^1.0.0" - gulp-util "^3.0.4" - pkginfo "^0.3.0" + plugin-error "^1.0.1" stream-combiner2 "^1.1.1" - stream-equal "0.1.6" - through2 "^0.6.3" + through2 "^2.0.3" gulp-connect@5.0.0: version "5.0.0" @@ -5111,7 +5120,7 @@ gulp-tslint@8.1.2: map-stream "~0.0.7" through "~2.3.8" -gulp-util@^3.0.0, gulp-util@^3.0.4, gulp-util@^3.0.6, gulp-util@~3.0.8: +gulp-util@^3.0.0, gulp-util@^3.0.6, gulp-util@~3.0.8: version "3.0.8" resolved "https://registry.yarnpkg.com/gulp-util/-/gulp-util-3.0.8.tgz#0054e1e744502e27c04c187c3ecc505dd54bbb4f" integrity sha1-AFTh50RQLifATBh8PsxQXdVLu08= @@ -7840,6 +7849,11 @@ node-watch@0.3.4: resolved "https://registry.yarnpkg.com/node-watch/-/node-watch-0.3.4.tgz#755f64ef5f8ad4acb5bafd2c4e7f4fb6a8db0214" integrity sha1-dV9k71+K1Ky1uv0sTn9PtqjbAhQ= +nodejs-websocket@^1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/nodejs-websocket/-/nodejs-websocket-1.7.2.tgz#94abd1e248f57d4d1c663dec3831015c6dad98a6" + integrity sha512-PFX6ypJcCNDs7obRellR0DGTebfUhw1SXGKe2zpB+Ng1DQJhdzbzx1ob+AvJCLzy2TJF4r8cCDqMQqei1CZdPQ== + nopt@3.0.x, nopt@^3.0.1, nopt@~3.0.1: version "3.0.6" resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" @@ -8574,7 +8588,7 @@ pinkie@^2.0.0: resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= -pkginfo@0.3.x, pkginfo@^0.3.0: +pkginfo@0.3.x: version "0.3.1" resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.3.1.tgz#5b29f6a81f70717142e09e765bbeab97b4f81e21" integrity sha1-Wyn2qB9wcXFC4J52W76rl7T4HiE= @@ -10381,11 +10395,6 @@ stream-each@^1.1.0: end-of-stream "^1.1.0" stream-shift "^1.0.0" -stream-equal@0.1.6: - version "0.1.6" - resolved "https://registry.yarnpkg.com/stream-equal/-/stream-equal-0.1.6.tgz#cc522fab38516012e4d4ee47513b147b72359019" - integrity sha1-zFIvqzhRYBLk1O5HUTsUe3I1kBk= - stream-events@^1.0.1, stream-events@^1.0.3: version "1.0.5" resolved "https://registry.yarnpkg.com/stream-events/-/stream-events-1.0.5.tgz#bbc898ec4df33a4902d892333d47da9bf1c406d5" @@ -10755,7 +10764,7 @@ through2@2.0.1: readable-stream "~2.0.0" xtend "~4.0.0" -through2@^0.6.1, through2@^0.6.3: +through2@^0.6.1: version "0.6.5" resolved "https://registry.yarnpkg.com/through2/-/through2-0.6.5.tgz#41ab9c67b29d57209071410e1d7a7a968cd3ad48" integrity sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=
      +
    1. Tracing user actions with long stack traces
    2. +
    3. Counting Tasks
    4. +
    5. Profiling Across Tasks
    6. +
    7. Throttle
    8. +
    9. WebSocket
    10. +